跳至內容
Fumadocs

Obsidian

使用 Fumadocs 直接算繪 Obsidian vault。

fumadocs-obsidian 提供執行階段的內容來源。它會直接讀取並編譯 vault,因此不會產生中介的 MDX 檔案,也不需要 Fumadocs MDX 的 remark 外掛。

支援的 Obsidian 語法包括:

  • Wikilink 與嵌入。
  • Callout。
  • 區塊 ID。
  • 註解。

名稱有歧義的 wikilink 會比照 Obsidian 的方式解析:筆記優先於附件,其次以路徑最短者為準。未知語言 (例如 dataview) 的程式碼區塊會以純文字算繪。

設定

安裝套件:

npm install fumadocs-obsidian shiki

產生樣式

fumadocs-obsidian/ui 中的元件使用 Tailwind CSS 撰寫樣式。請加入以下這行,以產生這些元件所需的 class:

Tailwind CSS
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-obsidian/css/preset.css';

設定來源

建立 Obsidian 來源,並將它傳入 dynamicLoader():

lib/source.ts
import { dynamicLoader } from 'fumadocs-core/source';
import { obsidian } from 'fumadocs-obsidian';

const vault = obsidian({
  dir: 'public/vault',
  // 將 vault 附件對應到其公開 URL
  url: (path) => `/vault/${path}`,
});

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

export function getSource() {
  return vaultLoader.get();
}

此範例將 vault 放在 public/vault,讓您的框架能透過 url 回傳的 URL 提供附件。若您的應用程式會另行提供或上傳附件,也可以將 vault 放在其他位置。

接著載入並算繪頁面。page.data.load() 會回傳已編譯頁面的算繪器。

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

app/routes/docs.tsx
import type { Route } from './+types/docs';
import { useMemo } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import * as ObsidianComponents from 'fumadocs-obsidian/ui';
import { rendererFromSerialized } from 'fumadocs-obsidian/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 });

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

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

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

在同一個 vault 快照中,page.data.load() 對每個頁面最多只會編譯一次。Vault 內容會以純 Markdown 編譯:算繪時只會將其對應為 JSX 並解析元件名稱,因此內容中的 JavaScript 一律不會執行。媒體檔案透過 URL 參照,不會讀入記憶體。

熱重載

開發期間,Vite 外掛會透過 Vite 的檔案監看器監看 vault。請將外掛加入設定檔,並連接 vault:

import { defineConfig } from 'vite';
import { localContentPlugin } from 'fumadocs-obsidian/dev/vite';

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

由於 Wikilink 與別名可能會在檔案之間產生相依關係,因此任何一處變更都會讓整個來源快照失效。

靜態來源

若 vault 只需讀取一次,請使用 staticSource():

lib/source.ts
import { loader } from 'fumadocs-core/source';
import { obsidian } from 'fumadocs-obsidian';

const vault = obsidian({
  dir: 'public/vault',
  url: (path) => `/vault/${path}`,
});

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

Schema

您可以使用相容於 Standard Schema 的驗證器,自訂頁面的 frontmatter 與 meta.json 檔案:

lib/source.ts
import { frontmatterSchema, obsidian } from 'fumadocs-obsidian';
import { metaSchema } from 'fumadocs-core/source/schema';
import { z } from 'zod';

const vault = obsidian({
  dir: 'public/vault',
  frontmatterSchema: frontmatterSchema.extend({
    tags: z.array(z.string()).optional(),
  }),
  metaSchema,
});

預設的 frontmatterSchema 能容許結構較鬆散的 vault:像 title: 2024 這類純量標題會自動轉換為字串,而 aliases 可接受單一名稱,也可接受清單。未通過驗證的筆記會被回報並略過,不會影響 vault 中的其他內容。

額外功能

如需啟用個別語法功能,請另外傳入對應的 remark/rehype 外掛:

lib/source.ts
import { obsidian } from 'fumadocs-obsidian';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';

const vault = obsidian({
  dir: 'public/vault',
  remarkPlugins: [remarkMath],
  rehypePlugins: [rehypeKatex],
});

How is this guide?

最後更新於