Obsidian
Render an Obsidian vault directly with Fumadocs.
fumadocs-obsidian exposes a runtime content source. It reads and compiles the vault directly, so it does not generate intermediate MDX files or need a Fumadocs MDX remark plugin.
Supported Obsidian syntax includes:
- Wikilinks and embeds.
- Callouts.
- Block IDs.
- Comments.
Ambiguous wikilink names resolve like Obsidian: notes take precedence over attachments, then the shortest path wins. Code fences in unknown languages (e.g. dataview) render as plain text.
Setup
Install the package:
npm install fumadocs-obsidian shikiGenerate Styles
The components from fumadocs-obsidian/ui are styled with Tailwind CSS. Add the following line so their classes are generated:
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-obsidian/css/preset.css';Configure Source
Create an Obsidian source and pass it to dynamicLoader():
import { dynamicLoader } from 'fumadocs-core/source';
import { obsidian } from 'fumadocs-obsidian';
const vault = obsidian({
dir: 'public/vault',
// map vault attachments to their public URLs
url: (path) => `/vault/${path}`,
});
const vaultLoader = dynamicLoader(vault.dynamicSource(), {
baseUrl: '/docs',
});
export function getSource() {
return vaultLoader.get();
}The example keeps its vault in public/vault, allowing your framework to serve attachments at the URLs returned by url. You can keep the vault elsewhere when your app serves or uploads its attachments separately.
Then load and render a page. page.data.load() returns a renderer of the compiled page.
Serialize it in the page, and render it in a React island:
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { Docs } from '@/components/docs';
import { getSource } from '@/lib/source';
export const prerender = true;
export const getStaticPaths = (async () => {
const source = await getSource();
return source.getPages().map((page) => ({
params: { slug: decodeURI(page.slugs.join('/')) || undefined },
props: { page },
}));
}) satisfies GetStaticPaths;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { page } = Astro.props;
const source = await getSource();
const pageTree = await source.serializePageTree(source.getPageTree());
const renderer = await page.data.load();
---
<Layout title={page.data.title}>
<Docs
client:load
pathname={Astro.url.pathname}
params={Astro.params}
tree={pageTree}
render={renderer.serialize()}
/>
</Layout>page.data.load() compiles each page at most once per vault snapshot. Vault content is compiled as plain Markdown: rendering only maps it to JSX and resolves component names, so JavaScript in content is never evaluated. Media files are referenced by URL and never read into memory.
Hot Reload
During development, the Vite plugin watches the vault through Vite's file watcher. Add the plugin to your config and connect the vault:
import { defineConfig } from 'astro/config';
import { localContentPlugin } from 'fumadocs-obsidian/dev/vite';
export default defineConfig({
vite: {
plugins: [localContentPlugin()],
},
});Wikilinks and aliases can create dependencies across files, so a change invalidates the whole source snapshot.
Static Source
Use staticSource() when the vault only needs to be read once:
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
Use Standard Schema-compatible validators to customize page frontmatter and meta.json files:
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,
});The default frontmatterSchema is forgiving about loosely structured vaults: scalar titles like title: 2024 are coerced to strings, and aliases accepts both a single name and a list. A note failing validation is reported and skipped without affecting the rest of the vault.
Additions
Pass remark/rehype plugins to enable syntax features separately:
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?
Last updated on
