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/dynamic';
import { obsidian } from 'fumadocs-obsidian';
const vault = obsidian({
dir: 'public/vault',
// map vault attachments to their public URLs
url: (path) => `/vault/${path}`,
});
if (process.env.NODE_ENV === 'development') {
void vault.devServer();
}
const vaultLoader = dynamicLoader(vault.dynamicSource(), {
baseUrl: '/docs',
});
export function getSource() {
return vaultLoader.get();
}The example keeps its vault in public/vault, allowing Next.js 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:
import { getSource } from '@/lib/source';
import { notFound } from 'next/navigation';
import defaultMdxComponents, { createRelativeLink } from 'fumadocs-ui/mdx';
import * as ObsidianComponents from 'fumadocs-obsidian/ui';
import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';
export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) {
const source = await getSource();
const page = source.getPage((await params).slug);
if (!page) notFound();
const { body, toc } = await (
await page.data.load()
).render({
...defaultMdxComponents,
...ObsidianComponents,
a: createRelativeLink(source, page),
});
return (
<DocsPage toc={toc}>
<DocsBody>{body}</DocsBody>
</DocsPage>
);
}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, JavaScript in content is never evaluated. Media files are referenced by URL and never read into memory.
Hot Reload
Run the included content watcher alongside your framework:
{
"scripts": {
"dev": "fumadocs-obsidian dev -- next dev"
}
}The vault.devServer() call shown above receives changes from that watcher. Since wikilinks and aliases can create cross-file dependencies, a change invalidates the whole source snapshot — only the changed files are re-read from disk though.
For Vite, use the in-process adapter instead:
import { localContentPlugin } from 'fumadocs-obsidian/dev/vite';
export default defineConfig({
plugins: [localContentPlugin()],
});import { watchWithVite } from 'fumadocs-obsidian/dev/vite';
if (import.meta.env.DEV) watchWithVite(vault);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