跳至內容
Fumadocs

本機 HTML

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

簡介

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

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

與本機 Markdown 不同,HTML 在這裡被視為需要調整的資料,而非用來撰寫內容的文件格式。

限制

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

設定

請安裝此套件:

npm install @fumadocs/local-html

建立 localHtml 實例,並將它串接到 Fumadocs:

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() 回傳的來源是一般的內容載入器實例。

使用方式

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

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

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, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { rendererFromSerialized } from '@fumadocs/local-html/client';
import type { HtmlRendererOptions } from '@fumadocs/local-html';
import { getSource } from '@/lib/source';

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

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 renderer = await page.data.load();
    return {
      title: page.data.title,
      description: page.data.description,
      // 語法樹,會在用戶端算繪
      render: renderer.serialize(),
      pageTree: await source.serializePageTree(source.getPageTree()),
    };
  });

function Page() {
  const { title, description, render, pageTree } = useFumadocsLoader(Route.useLoaderData());
  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 標籤對應到您自訂的元件,例如帶有錨點的標題:

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

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

頁面中繼資料

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

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

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

<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 進行驗證。

內容選項

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

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

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

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

語法突顯

<pre><code> 區塊會使用 Shiki 進行語法突顯,選項與本機 Markdown 相同:

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

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

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

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

熱重載

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

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

import { defineConfig } from 'vite';
import { localContentPlugin } from '@fumadocs/local-html/dev/vite';

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

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

停用重新驗證

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

它可搭配一般的 loader() 使用,不必侷限於 dynamicLoader():

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',
});

How is this guide?

最後更新於