Introduction
Publish an Obsidian vault as a documentation site with VaultPress
What is 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.
Setup
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.
Workflow
Obsidian vault → pnpm generate → content/ → pnpm dev → site- Edit notes in your Obsidian vault
- Run
pnpm generateto convert Markdown into MDX undercontent/ - Run
pnpm devto preview locally at http://localhost:3000
pnpm generate only reads your vault and writes to content/ — it does not modify notes in Obsidian.
Commands
| 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 |
Site language
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.
Page features
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
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.
Viewing protected pages
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/202606061435The 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.
Security model
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
Directory layout
.env— Vault path, site language, generate selection, protect passwordcontent/— Generated MDX (fromgenerate), plus hand-written pagesapp/— Next.js pages and routeslib/— Locale, Obsidian URIs, tags, protected access, shared configscripts/generate.ts— Vault → site generation script (read-only on vault)scripts/open-obsidian.ts— Opens the configured vault in Obsidian
Generation rules
scripts/generate.ts clears content/ before each run, except hand-written index.mdx and graph.mdx, so removed or renamed notes do not leave stale files.
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
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)
Graph View
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.
Stack
- Framework: Next.js + Fumadocs
- Content: Obsidian Markdown → MDX (fumadocs-obsidian)
- Features: Full-text search, knowledge graph, page tags, shared-password page gating, Obsidian/GitHub/Markdown actions, Mermaid, math