# Fumadocs (Framework Mode, React Router): 本機 HTML
URL: /zh-TW/docs/react-router/integrations/content/local-html
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/content/local-html.zh-TW.mdx

適用於本機 HTML 檔案的內容來源。

## 簡介 [#introduction]

`@fumadocs/local-html` 是用於本機 HTML 檔案的內容來源。它在設計上就不需要打包，因此能完全在執行階段運作。

它專為以 HTML 形式取得、而非由您自行撰寫的頁面而設計，例如匯出的簡報、報告，以及其他工具產生的輸出。每個檔案都會依照您的文件主題進行調整，而不是照原樣算繪。

與[本機 Markdown](/docs/react-router/integrations/content/local-md) 不同，HTML 在這裡被視為需要調整的資料，而非用來撰寫內容的文件格式。

### 限制 [#limitations]

* 指令碼會被移除，原始頁面的互動行為不會保留。
* 內容中無法使用 MDX 元件，但您可以在算繪階段將 HTML 標籤對應到元件。
* 處理過程只是轉換，而非淨化 (sanitization) 。所有可執行的內容都會被移除，但其餘標記會原封不動地算繪，而其中引用的資源 (尤其是圖片) 會在每位使用者開啟頁面時被載入。請確保內容目錄中只放您信任的檔案，例如您自己的 Markdown 內容。

## 設定 [#setup]

請安裝此套件：

```package-install
@fumadocs/local-html
```

建立 `localHtml` 實例，並將它串接到 Fumadocs：

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

const pages = localHtml({
  dir: 'content/pages',
  // 選項
});

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

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

`pagesLoader.get()` 回傳的來源是一般的[內容載入器](/docs/headless/source-api)實例。

## 使用方式 [#usage]

`page.data.load()` 會回傳已處理內容的算繪器。

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

```tsx title="app/routes/docs.tsx"
import type { Route } from './+types/docs';
import { useMemo } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { rendererFromSerialized } from '@fumadocs/local-html/client';
import { getSource } from '@/lib/source';

export async function loader({ params }: Route.LoaderArgs) {
  const source = await getSource();
  const page = source.getPage(params['*'].split('/').filter((v) => v.length > 0));
  if (!page) throw new Response('Not found', { status: 404 });

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

export default function Page({ loaderData }: Route.ComponentProps) {
  const { title, description, render, pageTree } = useFumadocsLoader(loaderData);
  const renderer = useMemo(() => rendererFromSerialized(render), [render]);
  const { body, toc } = renderer.renderSync();

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

將元件傳入 `renderSync()`，即可將 HTML 標籤對應到您自訂的元件，例如帶有錨點的標題：

```tsx
import { getMDXComponents } from '@/components/mdx';

const { body, toc } = renderer.renderSync(getMDXComponents());
```

### 頁面中繼資料 [#page-metadata]

每個頁面的標題與描述會從該頁面的 `<head>` 讀取：

* `<title>` 會作為頁面標題。
* `<meta name="description">` 會作為頁面描述。

您可以使用 `fumadocs:` meta 標籤覆寫這些值 (也可以設定圖示) ，而無須更動頁面上顯示的內容：

```html
<meta name="fumadocs:title" content="My Page" />
<meta name="fumadocs:description" content="A short description." />
<meta name="fumadocs:icon" content="SquareBookmark" />
```

文件中每一組 `<meta name>`/`content` 也都可以透過 `page.data.metadata` 取得。

與其他本機來源一樣，`meta.json` 檔案可用來自訂頁面樹，並透過 `metaSchema` 進行驗證。

### 內容選項 [#content-options]

頁面只會保留其內容元素：優先使用唯一的 `<main>`，其次是唯一的 `<article>`，若都沒有則退回使用 `<body>`，並移除 `<header>` 與 `<nav>` 等頁面外框元素。該元素的 `class` 與 `style` 屬性會被移除，改由您主題的 prose 樣式接手；其中的標題則會加上 id，供目錄與搜尋功能使用。

以上每個部分都可以設定：

```ts
import { localHtml } from '@fumadocs/local-html';

const pages = localHtml({
  dir: 'content/pages',
  // 保留原始的 class/style 屬性，不配合您的主題調整樣式
  adaptStyles: false,
  // 從內容中額外移除指定的標籤
  exclude: ['aside'],
  // 自訂挑選頁面內容所在元素的邏輯
  selectContent: (root) => {
    // 回傳 hast 元素，或回傳 undefined 以使用預設行為
  },
});
```

### 語法突顯 [#syntax-highlighting]

`<pre><code>` 區塊會使用 [Shiki](https://shiki.style) 進行語法突顯，選項與[本機 Markdown](/docs/react-router/integrations/content/local-md) 相同：

```ts
const pages = localHtml({
  dir: 'content/pages',
  rehypeCodeOptions: {
    themes: {
      light: 'github-light',
      dark: 'github-dark',
    },
  },
});
```

語言會從程式碼區塊的 `language-*` class 讀取，該 class 可位於 `<code>` 或其外層的 `<pre>` 上：

```html
<pre><code class="language-ts">const answer: number = 42;</code></pre>
```

未指定語言的區塊會以純文字呈現。若要停用語法突顯，請傳入 `rehypeCodeOptions: false`。

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

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

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

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

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

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

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

若想改用您自己的監看器，請在檔案變更時呼叫 `pages.invalidateFile(path)`。

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

若您只需要一次性的快照，不需要重新驗證，可以使用 `staticSource()`。

它可搭配一般的 `loader()` 使用，不必侷限於 `dynamicLoader()`：

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

const pages = localHtml({
  dir: 'content/pages',
});

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