跳至內容
Fumadocs

本機 Markdown

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

簡介

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

與 MDX Remote 相比,它專注於本機檔案,並負責處理流程中更多的環節 (frontmatter、meta 檔案、搜尋索引) 。

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

限制

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

設定

安裝套件:

npm install @fumadocs/local-md shiki

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

建立 localMd 實例,並將其連接至 Fumadocs:

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

您可以傳入 frontmatterSchema 與 metaSchema,以自訂驗證用的 schema:

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({
    // ...
  }),
});

使用方式

getSource() 會回傳一般的內容載入器。每個頁面都有 page.data.load(),它會編譯該頁面並回傳一個算繪器 (renderer) 。

在您的頁面中算繪:

import { getSource } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';

export default async function Layout({ children }: LayoutProps<'/docs'>) {
  const source = await getSource();

  return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;
}

引入

<include> 預設為啟用狀態:

content/docs/page.mdx
<include>../shared/note.md</include>

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

lib/source.ts
import remarkDirective from 'remark-directive';

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

熱重載

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

開發期間,請在執行應用程式的同時,一併執行內建的開發伺服器。開發伺服器會在單一處理程序中監看檔案,並透過 websocket 廣播變更,因此即使 Next.js 以多個 worker 執行也能正常運作。

package.json
{
  "scripts": {
    "dev": "local-md dev -- next dev"
  }
}

將來源連接至開發伺服器:

lib/source.ts
import { watchWithDevServer } from '@fumadocs/local-md/dev/ws';

if (process.env.NODE_ENV === 'development') {
  void watchWithDevServer(docs);
}

此指令會透過環境變數公開其 URL,因此不需要額外設定。若您自行執行伺服器,請明確傳入 URL,並使用 -p 變更連接埠:

void watchWithDevServer(docs, { url: 'ws://127.0.0.1:8000/_fumadocs_local_md' });

這只會讓伺服器上的來源失效,Next.js 仍會在瀏覽器中保留已算繪的頁面。請在根版面配置中算繪 <DevClient />,讓頁面也一併重新整理:

app/layout.tsx
import { DevClient } from '@fumadocs/local-md/dev/react-client';

export function RootLayout({ children }) {
  return (
    <RootProvider>
      <DevClient />
      {children}
    </RootProvider>
  );
}

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

JavaScript 引擎

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

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

請勿將不受信任的 MDX 檔案放入 dir,因為這些檔案會以 JavaScript 的形式在您的應用程式中執行。

停用重新驗證

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

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 遷移

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

遷移前

import { defineDocs, defineConfig } from 'fumadocs-mdx/config';

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

export default defineConfig();

遷移後

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();
}

接著:

  • 移除 source.config.ts。
  • 從 next.config.mjs 移除 createMDX()。
  • 將 collections/server 的匯入改為使用由 localMd() 建立的載入器。
  • 設定熱重載。

最後,將原本參照 source 物件的地方改為使用 getSource():

app/docs/layout.tsx
import { getSource } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';

export default async function Layout({ children }: LayoutProps<'/docs'>) {
  return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>;

  const docs = await getSource();
  return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>;
}

頁面資料 API 也有所變更:

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>
);

How is this guide?

最後更新於