本機 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:
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:
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) 。
在 loader 中將其序列化,再於用戶端算繪:
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>../shared/note.md</include>在 Markdown 檔案中,請先設定好 remark-directive,再使用 ::include[../shared/note.md] 指令:
import remarkDirective from 'remark-directive';
const docs = localMd({
dir: 'content/docs',
mdOptions: { remarkPlugins: [remarkDirective] },
});熱重載
@fumadocs/local-md 在執行階段讀取檔案,因此預設不會監看任何檔案。
開發期間,Vite 外掛會透過 Vite 的檔案監看器讓來源失效,不需要額外的處理程序或連接埠。請將外掛加入您的設定,並連接來源:
import { defineConfig } from 'vite';
import { localContentPlugin } from '@fumadocs/local-md/dev/vite';
export default defineConfig({
plugins: [localContentPlugin()],
});由於內容檔案位於模組圖之外,外掛會將您的內容目錄加入 Vite 的監看器。由於沒有任何模組失效,外掛會改為直接重新載入瀏覽器。若要自行處理重新載入,請傳入 localContentPlugin({ reload: false })。
當 Markdown 或 MDX 檔案 (包括其引入的檔案) 變更時,來源就會重新載入。若要串接自訂的監看器,請監看 docs.dir 以及傳入 docs.onDependency() 的檔案,並在檔案變更時呼叫 docs.invalidateFile(path)。
Nitro
使用 Nitro 部署時,請在 Vite 設定中進行兩項變更:
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 引擎
MDX 檔案 (*.mdx) 會透過 new Function 執行,因此在禁止使用 eval() 的環境中 (例如 Cloudflare Workers) 無法運作。
Markdown 檔案 (*.md) 會直接從語法樹算繪,不需要 eval()。外掛產生的 JSX 則由內建的虛擬 JavaScript 引擎進行求值,您可以透過 rendererOptions.executor 替換此引擎。
請勿將不受信任的 MDX 檔案放入 dir,因為這些檔案會以 JavaScript 的形式在您的應用程式中執行。
停用重新驗證
staticSource() 會回傳一次性的快照,不會重新驗證。它需搭配 loader() 使用,而非 dynamicLoader():
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 { 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(),
});遷移後
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()。 - 設定熱重載。
最後,將原本參照 source 物件的地方 (例如您的載入器中) 改為使用 getSource():
import { getSource } from '@/lib/source';
const source = await getSource();
const page = source.getPage(slugs);頁面資料 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?
最後更新於
