# Fumadocs (Framework Mode, Astro): Obsidian
URL: /zh-TW/docs/astro/integrations/content/obsidian
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/content/obsidian.zh-TW.mdx

使用 Fumadocs 直接算繪 Obsidian vault。

`fumadocs-obsidian` 提供執行階段的內容來源。它會直接讀取並編譯 vault，因此不會產生中介的 MDX 檔案，也不需要 Fumadocs MDX 的 remark 外掛。

支援的 Obsidian 語法包括：

* Wikilink 與嵌入。
* Callout。
* 區塊 ID。
* 註解。

名稱有歧義的 wikilink 會比照 Obsidian 的方式解析：筆記優先於附件，其次以路徑最短者為準。未知語言 (例如 `dataview`) 的程式碼區塊會以純文字算繪。

## 設定 [#setup]

安裝套件：

```package-install
fumadocs-obsidian shiki
```

### 產生樣式 [#generate-styles]

`fumadocs-obsidian/ui` 中的元件使用 Tailwind CSS 撰寫樣式。請加入以下這行，以產生這些元件所需的 class：

```css title="Tailwind CSS"
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
/* [!code ++] */
@import 'fumadocs-obsidian/css/preset.css';
```

### 設定來源 [#configure-source]

建立 Obsidian 來源，並將它傳入 `dynamicLoader()`：

```ts title="lib/source.ts"
import { dynamicLoader } from 'fumadocs-core/source';
import { obsidian } from 'fumadocs-obsidian';

const vault = obsidian({
  dir: 'public/vault',
  // 將 vault 附件對應到其公開 URL
  url: (path) => `/vault/${path}`,
});

const vaultLoader = dynamicLoader(vault.dynamicSource(), {
  baseUrl: '/docs',
});

export function getSource() {
  return vaultLoader.get();
}
```

此範例將 vault 放在 `public/vault`，讓您的框架能透過 `url` 回傳的 URL 提供附件。若您的應用程式會另行提供或上傳附件，也可以將 vault 放在其他位置。

接著載入並算繪頁面。`page.data.load()` 會回傳已編譯頁面的算繪器。

在頁面中將其序列化，再於 React island 中算繪：

```astro tab="src/pages/docs/[...slug].astro"
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { Docs } from '@/components/docs';
import { getSource } from '@/lib/source';

export const prerender = true;

export const getStaticPaths = (async () => {
  const source = await getSource();
  return source.getPages().map((page) => ({
    params: { slug: decodeURI(page.slugs.join('/')) || undefined },
    props: { page },
  }));
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { page } = Astro.props;
const source = await getSource();
const pageTree = await source.serializePageTree(source.getPageTree());
const renderer = await page.data.load();
---

<Layout title={page.data.title}>
  <Docs
    client:load
    pathname={Astro.url.pathname}
    params={Astro.params}
    tree={pageTree}
    render={renderer.serialize()}
  />
</Layout>
```

```tsx tab="src/components/docs.tsx"
import { useMemo } from 'react';
import { type SerializedPageTree, useFumadocsLoader } from 'fumadocs-core/source/client';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import * as ObsidianComponents from 'fumadocs-obsidian/ui';
import { rendererFromSerialized, type ObsidianRendererOptions } from 'fumadocs-obsidian/client';
import { Provider, type ProviderProps } from '@/components/provider';

export interface DocsProps extends ProviderProps {
  tree: SerializedPageTree;
  // 語法樹，在 island 中算繪
  render: ObsidianRendererOptions;
}

export function Docs({ tree, render, ...props }: DocsProps) {
  const data = useFumadocsLoader({ tree });
  const renderer = useMemo(() => rendererFromSerialized(render), [render]);
  const { body, toc } = renderer.renderSync({ ...defaultMdxComponents, ...ObsidianComponents });

  return (
    <Provider {...props}>
      <DocsLayout tree={data.tree}>
        <DocsPage toc={toc}>
          <DocsBody>{body}</DocsBody>
        </DocsPage>
      </DocsLayout>
    </Provider>
  );
}
```

在同一個 vault 快照中，`page.data.load()` 對每個頁面最多只會編譯一次。Vault 內容會以純 Markdown 編譯：算繪時只會將其對應為 JSX 並解析元件名稱，因此內容中的 JavaScript 一律不會執行。媒體檔案透過 URL 參照，不會讀入記憶體。

## 熱重載 [#hot-reload]

開發期間，Vite 外掛會透過 Vite 的檔案監看器監看 vault。請將外掛加入設定檔，並連接 vault：

```js tab="astro.config.mjs"
import { defineConfig } from 'astro/config';
import { localContentPlugin } from 'fumadocs-obsidian/dev/vite';

export default defineConfig({
  vite: {
    plugins: [localContentPlugin()],
  },
});
```

```ts tab="lib/source.ts"
import { watchWithVite } from 'fumadocs-obsidian/dev/vite';

if (import.meta.env.DEV) watchWithVite(vault);
```

由於 Wikilink 與別名可能會在檔案之間產生相依關係，因此任何一處變更都會讓整個來源快照失效。

## 靜態來源 [#static-source]

若 vault 只需讀取一次，請使用 `staticSource()`：

```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { obsidian } from 'fumadocs-obsidian';

const vault = obsidian({
  dir: 'public/vault',
  url: (path) => `/vault/${path}`,
});

export const source = loader(await vault.staticSource(), {
  baseUrl: '/docs',
});
```

## Schema [#schema]

您可以使用相容於 Standard Schema 的驗證器，自訂頁面的 frontmatter 與 `meta.json` 檔案：

```ts title="lib/source.ts"
import { frontmatterSchema, obsidian } from 'fumadocs-obsidian';
import { metaSchema } from 'fumadocs-core/source/schema';
import { z } from 'zod';

const vault = obsidian({
  dir: 'public/vault',
  frontmatterSchema: frontmatterSchema.extend({
    tags: z.array(z.string()).optional(),
  }),
  metaSchema,
});
```

預設的 `frontmatterSchema` 能容許結構較鬆散的 vault：像 `title: 2024` 這類純量標題會自動轉換為字串，而 `aliases` 可接受單一名稱，也可接受清單。未通過驗證的筆記會被回報並略過，不會影響 vault 中的其他內容。

### 額外功能 [#additions]

如需啟用個別語法功能，請另外傳入對應的 remark/rehype 外掛：

* [Mermaid](/docs/astro/markdown/mermaid)。
* [數學公式](/docs/astro/markdown/math)。

```ts title="lib/source.ts"
import { obsidian } from 'fumadocs-obsidian';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';

const vault = obsidian({
  dir: 'public/vault',
  remarkPlugins: [remarkMath],
  rehypePlugins: [rehypeKatex],
});
```