---
title: Obsidian
description: 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:

```package-install
fumadocs-obsidian shiki
```

### Generate Styles

The components from `fumadocs-obsidian/ui` are styled with Tailwind CSS. Add the following line so their classes are generated:

```css title="Tailwind CSS"
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
/* [!code ++] */
@import 'fumadocs-obsidian/css/preset.css';
```

### Configure Source

Create an Obsidian source and pass it to `dynamicLoader()`:

```ts title="lib/source.ts"
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:

```tsx title="app/docs/[[...slug]]/page.tsx"
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:

```json title="package.json"
{
  "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:

```ts title="vite.config.ts"
import { localContentPlugin } from 'fumadocs-obsidian/dev/vite';

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

```ts title="lib/source.ts"
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:

```ts title="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

Use Standard Schema-compatible validators to customize page frontmatter and `meta.json` files:

```ts title="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,
});
```

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:

- [Mermaid](/docs/markdown/mermaid).
- [Math](/docs/markdown/math).

```ts title="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],
});
```
