# Fumadocs (Framework Mode, TanStack Start): 本機 Markdown
URL: /zh-TW/docs/tanstack-start/integrations/content/local-md
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/content/local-md.zh-TW.mdx

適用於本機 Markdown 內容的內容來源。

## 簡介 [#introduction]

`@fumadocs/local-md` 是用於本機 Markdown/MDX 檔案的內容來源。它在設計上採用免打包 (bundleless) 架構，因此能完全在執行階段運作。

與 [MDX Remote](/docs/tanstack-start/integrations/content/mdx-remote) 相比，它專注於本機檔案，並負責處理流程中更多的環節 (frontmatter、meta 檔案、搜尋索引) 。

與 [Fumadocs MDX](/docs/mdx) 相比，它不需要型別產生或打包工具即可運作，但無法使用建置階段的圖片最佳化。

### 限制 [#limitations]

* 不支援建置階段的圖片最佳化。
* MDX 檔案中不支援匯入／匯出，但您可以在算繪時傳入元件。

## 設定 [#setup]

安裝套件：

```package-install
@fumadocs/local-md shiki
```

> 之所以要安裝 `shiki`，是因為打包工具必須將它外部化 (externalize) 。

建立 `localMd` 實例，並將其連接至 Fumadocs：

```ts title="lib/source.ts"
import { dynamicLoader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/local-md';

const docs = localMd({
  dir: 'content/docs',
  // 選項
});

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

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

### Schema [#schema]

您可以傳入 `frontmatterSchema` 與 `metaSchema`，以自訂驗證用的 schema：

```ts title="lib/source.ts"
import { localMd } from '@fumadocs/local-md';
import { pageSchema, metaSchema } from 'fumadocs-core/source/schema';

const docs = localMd({
  dir: 'content/docs',
  frontmatterSchema: pageSchema.extend({
    // ...
  }),
  metaSchema: metaSchema.extend({
    // ...
  }),
});
```

## 使用方式 [#usage]

`getSource()` 會回傳一般的[內容載入器](/docs/headless/source-api)。每個頁面都有 `page.data.load()`，它會編譯該頁面並回傳一個算繪器 (renderer) 。

在 loader 中將其序列化，再於用戶端算繪：

```tsx title="src/routes/docs/$.tsx"
import { createFileRoute, notFound } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { useMemo } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { rendererFromSerialized } from '@fumadocs/local-md/client';
import type { MarkdownRendererSerializedOptions } from '@fumadocs/local-md';
import { getSource } from '@/lib/source';
import { useMDXComponents } from '@/components/mdx';

// server function 結果的型別層級檢查無法驗證語法樹，
// 因此需將其註冊為可序列化（若無法解析 `@tanstack/router-core`，請先安裝）
declare module '@tanstack/router-core' {
  interface SerializableExtensions {
    fumadocsMarkdownRenderer: MarkdownRendererSerializedOptions;
  }
}

export const Route = createFileRoute('/docs/$')({
  component: Page,
  loader: ({ params }) => serverLoader({ data: params._splat?.split('/') ?? [] }),
});

const serverLoader = createServerFn({
  method: 'GET',
})
  .validator((slugs: string[]) => slugs)
  .handler(async ({ data: slugs }) => {
    const source = await getSource();
    const page = source.getPage(slugs);
    if (!page) throw notFound();

    const { serialize } = await page.data.load();
    return {
      title: page.data.title,
      // 語法樹，會在用戶端算繪
      render: serialize(),
      pageTree: await source.serializePageTree(source.getPageTree()),
    };
  });

function Page() {
  const { title, render, pageTree } = useFumadocsLoader(Route.useLoaderData());
  const renderer = useMemo(() => rendererFromSerialized(render), [render]);
  const { body, toc } = renderer.renderSync(useMDXComponents());

  return (
    <DocsLayout tree={pageTree}>
      <DocsPage toc={toc}>
        <DocsTitle>{title}</DocsTitle>
        <DocsBody>{body}</DocsBody>
      </DocsPage>
    </DocsLayout>
  );
}
```

### 引入 [#include]

[`<include>`](/docs/mdx/include) 預設為啟用狀態：

```mdx title="content/docs/page.mdx"
<include>../shared/note.md</include>
```

在 Markdown 檔案中，請先設定好 `remark-directive`，再使用 `::include[../shared/note.md]` 指令：

```ts title="lib/source.ts"
import remarkDirective from 'remark-directive';

const docs = localMd({
  dir: 'content/docs',
  mdOptions: { remarkPlugins: [remarkDirective] },
});
```

### 熱重載 [#hot-reload]

`@fumadocs/local-md` 在執行階段讀取檔案，因此預設不會監看任何檔案。

開發期間，Vite 外掛會透過 Vite 的檔案監看器讓來源失效，不需要額外的處理程序或連接埠。請將外掛加入您的設定，並連接來源：

```ts tab="vite.config.ts"
import { defineConfig } from 'vite';
import { localContentPlugin } from '@fumadocs/local-md/dev/vite';

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

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

if (import.meta.env.DEV) {
  watchWithVite(docs);
}
```

由於內容檔案位於模組圖之外，外掛會將您的內容目錄加入 Vite 的監看器。由於沒有任何模組失效，外掛會改為直接重新載入瀏覽器。若要自行處理重新載入，請傳入 `localContentPlugin({ reload: false })`。

當 Markdown 或 MDX 檔案 (包括其引入的檔案) 變更時，來源就會重新載入。若要串接自訂的監看器，請監看 `docs.dir` 以及傳入 `docs.onDependency()` 的檔案，並在檔案變更時呼叫 `docs.invalidateFile(path)`。

### Nitro [#nitro]

使用 Nitro 部署時，請在 Vite 設定中進行兩項變更：

```ts title="vite.config.ts"
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';

export default defineConfig({
  plugins: [
    // Nitro 無法打包 Shiki 的 Wasm 匯入，其預設進入點會內嵌二進位檔
    nitro({ wasm: false }),
  ],
  resolve: {
    alias: [
      // Nitro 會保留此 shim 中的 `require('react')`，導致在執行階段載入第二份 React
      { find: /^use-sync-external-store\/shim$/, replacement: 'react' },
    ],
  },
});
```

### JavaScript 引擎 [#javascript-engine]

MDX 檔案 (`*.mdx`) 會透過 `new Function` 執行，因此在禁止使用 `eval()` 的環境中 (例如 Cloudflare Workers) 無法運作。

Markdown 檔案 (`*.md`) 會直接從語法樹算繪，不需要 `eval()`。外掛產生的 JSX 則由內建的虛擬 JavaScript 引擎進行求值，您可以透過 `rendererOptions.executor` 替換此引擎。

<Callout type="warn">
請勿將不受信任的 MDX 檔案放入 `dir`，因為這些檔案會以 JavaScript 的形式在您的應用程式中執行。
</Callout>

### 停用重新驗證 [#disable-revalidation]

`staticSource()` 會回傳一次性的快照，不會重新驗證。它需搭配 `loader()` 使用，而非 `dynamicLoader()`：

```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/local-md';

const docs = localMd({
  dir: 'content/docs',
});

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

## 從 Fumadocs MDX 遷移 [#migration-from-fumadocs-mdx]

若要遷移，請將 Fumadocs MDX 的設定替換為 `@fumadocs/local-md` 的設定。

### 遷移前 [#before]

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

const docs = defineDocs({
  dir: 'content/docs',
});

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

### 遷移後 [#after]

```ts title="lib/source.ts"
import { dynamicLoader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/local-md';

const docs = localMd({
  dir: 'content/docs',
});

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

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

接著：

* 從您的 Vite 設定中移除 `fumadocsMdx()`。
* 設定[熱重載](#hot-reload)。

最後，將原本參照 `source` 物件的地方 (例如您的載入器中) 改為使用 `getSource()`：

```ts
import { getSource } from '@/lib/source';

// [!code ++]
const source = await getSource();
const page = source.getPage(slugs);
```

頁面資料 API 也有所變更：

```tsx tab="Before"
const page = source.getPage(['...']);

// 標題與描述
page.data.title;

// 自訂 frontmatter 屬性
page.data.full;

// getText() API
await page.data.getText('processed');

// 編譯後的屬性：
page.data.structuredData;
page.data.toc;
return (
  <div>
    <page.data.body components={{}} />
  </div>
);
```

```tsx tab="After"
const page = docs.getPage(['...']);

// 標題與描述（不變）
page.data.title;

// 自訂 frontmatter 屬性
page.data.frontmatter.full;

// 已無 getText() API，請改用 data.content
console.log(page.data.content);

// 編譯後的屬性：
const structuredData = await page.data.structuredData();
const { toc, body } = await (await page.data.load()).render(mdxComponents);

return <div>{body}</div>;
```