Publish an Obsidian vault as a documentation site with VaultPress.
VaultPress turns an Obsidian vault into a documentation site.
Keep writing in Obsidian as usual. Run pnpm generate to sync notes into content/, then preview or deploy the site. The stack is Next.js + Fumadocs, with Obsidian wikilinks, embeds, callouts, Mermaid, and math.
Copy .env.example to .env and configure:
# Required for pnpm generate and pnpm obsidian
OBSIDIAN_VAULT_PATH="/path/to/your/vault"
# Optional
SITE_LANGUAGE=en
GENERATE_INCLUDE=fleeting,permanent,literature
SITE_PROTECT_PASSWORD=your-password| Variable | Used by | Description |
|---|---|---|
OBSIDIAN_VAULT_PATH |
pnpm generate, pnpm obsidian |
Absolute path to your Obsidian vault (local CLI only) |
SITE_LANGUAGE |
Site UI | en (default) or cn — search, navigation, table of contents |
GENERATE_INCLUDE |
pnpm generate |
Comma-separated top-level folders/files to sync; saved after interactive selection |
SITE_PROTECT_PASSWORD |
Site access | Shared password gate for protected: true pages (not encryption) |
OBSIDIAN_VAULT_PATH is not read when building site links — the server never accesses your local filesystem for page actions.
Obsidian vault → pnpm generate → content/ + public/ → pnpm dev → site
- Edit notes in your Obsidian vault
- Run
pnpm generateto convert Markdown into MDX undercontent/, and sync Canvas files intopublic/ - Run
pnpm devto preview locally at http://localhost:3000
pnpm generate only reads your vault and writes to content/ and public/ — it does not modify notes in Obsidian.
| Command | Description |
|---|---|
pnpm obsidian |
Open the vault configured in OBSIDIAN_VAULT_PATH |
pnpm generate |
Generate site content from the vault |
pnpm generate -- --select |
Re-pick top-level folders and files to include |
pnpm dev |
Start the development server |
pnpm build |
Build for production |
pnpm types:check |
Run MDX generation, Next.js typegen, and TypeScript |
pnpm lint |
Run Oxlint |
Set SITE_LANGUAGE in .env:
SITE_LANGUAGE=en # English (default)
SITE_LANGUAGE=cn # 简体中文Restart the dev server after changing it. This changes the site UI only — your note content is not translated.
Each documentation page includes:
- Tags — From frontmatter
tags(string or list), shown below the description - Copy Markdown — Copy the processed Markdown for the page
- Open menu:
- Open in Obsidian —
obsidian://open?file=…using the page's public relative path (.mdx→.md). Opens the note in local Obsidian when your vault mirrors the generated structure. - Open in GitHub — Link to the page source under
content/(configurelib/shared.ts→gitConfigfor your repo) - View as Markdown — Open the raw Markdown endpoint for the page
- Open in Obsidian —
Protected pages use shared-password access control. They are not encrypted: generated MDX still lives in content/ like any other page. The site only withholds the body and some exports until a visitor proves they know SITE_PROTECT_PASSWORD.
Mark a note with frontmatter:
protected: trueObsidian may export this as a string (protected: 'true') — both are supported.
Set the shared password in .env (never commit this value):
SITE_PROTECT_PASSWORD=your-passwordRestart the dev server after changing it. One password unlocks all protected pages for that browser session.
Before unlocking, protected pages stay in the sidebar but their bodies are gated; they are hidden from search, graph, and Markdown endpoints. If someone guesses the URL, they can open the page shell directly — but still cannot read the body without the password, for example:
/permanent/202606061435
The page title, description, and tags remain visible even before unlock. A password form appears in the body only — Copy Markdown, View as Markdown, and the Open menu stay hidden until unlocked.
After a correct password, the browser stores an HttpOnly cookie for about 30 days. Requires server deployment (pnpm build + pnpm start, or Vercel) with HTTPS in production — not static export.
What this scheme is good for
- Keeping protected note bodies out of casual reading, search, and Markdown export (sidebar links remain visible)
- A simple gate when the site is public but a few pages should need a shared secret
- Pairing with a private repository so
content/is not world-readable on GitHub
What it does not protect against
- Repository or build access —
content/*.mdxcontains the full source; anyone with repo, CI, or server filesystem access can read it without the password - URL guessing — if someone knows your folder structure, they may find the page URL and see its title, description, and tags before unlock; the body and Markdown exports remain blocked
- Metadata leakage — title, description, and tags are shown before unlock
- One password for everything — there are no per-page or per-user passwords; sharing the password shares access to all protected pages
- Cookie scope — one successful unlock grants access to every protected page until the cookie expires
- Brute force —
/api/protected-authhas no built-in rate limiting; use a strong password and HTTPS - True secrecy — this is access gating, not encryption, audit logging, or account-based authorization
Practical guidance
- Use a long, unique
SITE_PROTECT_PASSWORDand keep.envout of version control - Deploy over HTTPS so the HttpOnly cookie is marked
Securein production - For highly sensitive material, do not publish it through
pnpm generate; keep it only in Obsidian, or use a proper auth system instead
.env— Vault path, site language, generate selection, protect passwordcontent/— Generated MDX (fromgenerate), plus hand-writtenindex.mdxandgraph.mdxpublic/— Generated static assets (vault media, Canvas.canvasfiles, and canvas-referenced images/PDFs, etc.)app/— Next.js pages and routeslib/— Locale, Obsidian URIs, tags, protected access, canvas parsing, shared configcomponents/canvas-*.tsx— Canvas viewer (React Flow) and node renderersscripts/generate.ts— Vault → site generation script (read-only on vault)scripts/generate-canvas-pages.ts— Syncs canvas files/assets and generates canvas MDX pagesscripts/open-obsidian.ts— Opens the configured vault in Obsidian
Each pnpm generate run starts by deleting previously generated output so removed vault items do not leave stale site files.
| Location | What is removed | What is preserved |
|---|---|---|
content/ |
Every top-level file and folder | index.mdx, graph.mdx only |
public/ |
Everything inside the directory | Nothing |
Examples of items removed from content/: note folders (fleeting/, permanent/, …), generated canvas pages (canvas/demo.mdx), and any other generated MDX trees.
Examples of items removed from public/: synced .canvas files (canvas/demo.canvas), canvas-referenced assets (vaultpress.png), and other vault media written by generate.
After cleanup, generate repopulates both directories from the current vault selection:
- Vault notes and media →
content/+public/ - Canvas files and their referenced assets →
public/ - Canvas page wrappers →
content/
Do not store hand-maintained static files under public/ — they will be deleted on the next run. Keep long-lived documentation in content/index.mdx, content/graph.mdx, or outside the generated output paths.
The first interactive run shows the vault's top-level tree so you can pick folders and files to include. The choice is saved as GENERATE_INCLUDE in .env. Use pnpm generate -- --select to re-pick. In non-interactive environments, all top-level items are included by default.
Excluded from generation:
.obsidian/— Obsidian configurationtemplates/— Note templates
Canvas handling (during pnpm generate):
.canvasfiles underGENERATE_INCLUDEare copied from the vault topublic/(same relative path)- Media referenced by canvas nodes (images, video, audio, PDF, group backgrounds) are copied into
public/even when the asset is outsideGENERATE_INCLUDE - An MDX wrapper is generated under
content/for each canvas (for examplepublic/canvas/demo.canvas→content/canvas/demo.mdx)
Frontmatter handling:
title— Uses the note'stitlefield, then the first#heading, then the filenamedescription— Uses the note'sdescriptionfield only; omitted if emptytags— Passed through from Obsidian frontmatter; normalized to a string array for displayprotected— Whentrue(or'true'), gates the page body behindSITE_PROTECT_PASSWORD(not encryption)
The Graph View page shows an interactive graph of site pages and wikilink connections. Each node is a page; edges are internal links. Click a node to open that page. Protected pages appear only after unlocking.
VaultPress publishes Obsidian Canvas files as read-only pages.
- Create or edit
.canvasfiles in your vault (for examplecanvas/demo.canvas) - Include the canvas folder in
GENERATE_INCLUDE(or select it duringpnpm generate -- --select) - Run
pnpm generate— canvas files land inpublic/, and site pages are generated undercontent/ - Open the page in the site (for example /canvas/demo)
- Nodes — text, file, link, and group
- Edges — side anchors, arrow ends, colors, labels
- Colors — Obsidian presets
1–6and custom hex values - Group — label above the frame; optional background image (
cover/ratio/repeat) - File — filename label above the frame; image, video, audio, PDF, and other file types
- Markdown files — in-node preview using the same MDX pipeline as documentation pages; click the node background to open the full page; links inside the preview work independently
- Text — lightweight Markdown and wikilinks inside the node
- Interaction — drag to pan, scroll to zoom
Canvas pages use full: true layout (no table of contents) and render inside a fixed-height viewer.
- Read-only — no editing on the site
- Text nodes use a lightweight Markdown renderer, not full MDX
![[note.canvas]]embeds in notes are not supported yet- File
subpath(heading anchors) is appended to links but does not scroll the in-node preview
- Framework: Next.js + Fumadocs + React Flow
- Content: Obsidian Markdown → MDX (fumadocs-obsidian)
- Features: Full-text search, knowledge graph, Obsidian Canvas, page tags, shared-password page gating, Obsidian/GitHub/Markdown actions, Mermaid, math
Canvas and follow-up items:
- Support
![[path/to/canvas.canvas]]embeds inside notes - Render text nodes with the full MDX pipeline (match file-node preview fidelity)
- Scroll file-node preview to
subpathheading anchors - Add automated tests for canvas parsing and asset sync
- Document remote image URLs in canvas file nodes (Next.js
imagesconfig)
See CONTRIBUTING.md.
