Obsidian stores your notes as plain markdown in a plain folder. That is the whole reason this is possible: there is no database to export and no proprietary format to reverse-engineer. A static site generator can read that folder directly.

Quartz is the one built specifically for it. It speaks Obsidian’s dialect natively — [[wikilinks]], transclusions, callouts, nested tags — and gives you back the parts of Obsidian you actually miss on the web: backlinks, a graph view, hover previews, and full-text search.

This post walks the whole path end to end. The example vault it produces is published right here on this site, so every claim below is checkable by clicking.

What you end up with

A static site — plain HTML, CSS and JS, no server-side runtime, no database — that you can host anywhere, including for free. Rebuilt with one command whenever you write something new.

Why Quartz and not something else

OptionCostWikilinksBacklinks + graphYour own HTML/CSS
Obsidian Publish$10/moyesyeslimited
Quartzfreeyesyesfull
Hugo / Jekyllfreeneeds pluginsnofull
Docusaurus / MkDocsfreepartialnofull

Obsidian Publish is the zero-effort answer and there is nothing wrong with paying for it. Quartz costs you an afternoon once and nothing after that, and because the output is just files you are never locked in. The generic static site generators are excellent tools that happen to be a bad fit here — they treat [[Some Note]] as literal text unless you go plugin shopping.

Before you start

  • Node.js 22 or newer. Check with node --version. Quartz 4.5 declares node >= 22 and npm >= 10.9.2; older versions fail in confusing ways.
  • git. Used both to install Quartz and to deploy it.
  • An Obsidian vault, or the willingness to make one. The vault does not need to be tidy — you will see below how to publish part of it.

Step 1 — Install Quartz

Quartz is used by cloning it, not by installing it as a dependency. Your site lives inside the clone.

git clone https://github.com/jackyzha0/quartz.git my-site
cd my-site && npm install

That leaves you with a few directories that matter:

my-site/
├── content/           ← your vault goes here
├── quartz/            ← the generator itself; don't edit
├── public/            ← build output; this is the website
├── quartz.config.ts   ← what the site is
└── quartz.layout.ts   ← where things sit on the page

Why clone instead of install?

Quartz’s configuration is TypeScript, and its components are real source files you are meant to edit. Cloning gives you that access. The cost is that upgrading means pulling from upstream and resolving conflicts in your config — npx quartz update does this for you.

Step 2 — Get your vault into content/

Everything under content/ becomes the site. There are three ways to put it there, and the right one depends on how you work.

Copy it. Simplest, and fine if you publish in deliberate batches.

cp -r "/path/to/Your Vault" content/

Symlink it. Your vault stays where Obsidian expects it, and Quartz reads it live. On Windows this needs an elevated shell:

New-Item -ItemType SymbolicLink -Path "content\vault" -Target "C:\Users\you\Documents\Obsidian Vaults\Your Vault"

Make the vault a git submodule. The right answer if your vault is already its own repository. Your notes keep their own history, separate from the site’s.

git submodule add https://github.com/you/your-vault.git content/vault

Whichever you pick, Obsidian can open content/ directly as a vault. That is worth doing — you get Obsidian’s editor, graph and search over exactly the files that will be published, with no sync step to forget.

The example vault on this site is a real one: content/example-vault contains its own .obsidian folder and opens in Obsidian unmodified.

Step 3 — Configure the site

Open quartz.config.ts. Most of it can wait; these four fields cannot.

configuration: {
  pageTitle: "eecs.blog",
  baseUrl: "eecs.blog",
  ignorePatterns: ["private", "templates", ".obsidian"],
  analytics: null,
}

baseUrl is the one people get wrong

No protocol, no trailing slash. eecs.blog, not https://eecs.blog/. It can include a path if your site lives in a subdirectory — example.com/notes.

Get it wrong and the site will still build and still look fine locally. What breaks is everything that needs an absolute URL: RSS, the sitemap, OpenGraph preview cards, and the canonical tags search engines read. These fail silently, which is why this is worth double-checking before you deploy.

Two more worth knowing about:

  • analytics defaults to { provider: "plausible" }. Unless you actually run Plausible, set it to null so your visitors aren’t pinging a third party for nothing.
  • ignorePatterns is your privacy boundary. More on that next.

Step 4 — Decide what stays private

A vault usually contains things you never intended to show anyone. Quartz gives you three independent mechanisms, and it is worth using more than one.

Per-note, by frontmatter. Add draft: true and the RemoveDrafts filter drops that note at build time:

---
title: Half-finished thoughts
draft: true
---

Per-folder, by config. Anything matching ignorePatterns never gets read:

ignorePatterns: ["private", "templates", ".obsidian", "Daily Notes"]

By what you copy. If a folder never enters content/, it cannot leak.

Verify before you deploy, not after

public/ is the entire site. Grep it for something that should never be public and confirm you get nothing back:

grep -ril "confidential" public/ | head

Deleting a note later does not un-publish it from caches, archives, or whatever crawled it in the meantime.

This site’s example vault demonstrates the first mechanism: Analog/Noise in Amplifiers.md exists in the vault and in git, has draft: true, and has no page here. The vault index links to the others and deliberately does not link to that one.

Step 5 — What survives the trip

Most of Obsidian carries over untouched:

FeatureWorks?Notes
[[Wikilinks]]yesresolved by shortest unique path
[[Note|display text]]yes
![[Embedded image]]yesany attachment folder layout
![[Note]] transclusionyesadd a #Section suffix for one heading
Calloutsyesall types, including collapsible
Nested tagsyes#circuit/analog gets its own tag page
aliases: frontmatteryesemitted as real redirects
LaTeXyesKaTeX by default, MathJax available
Mermaid diagramsyes
Code blocksyessyntax highlighting via Shiki
Footnotes, tables, task listsyesGFM
Dataview queriesnorender as raw code blocks
Community pluginsnoExcalidraw, Kanban, etc.
Canvas filesno.canvas is not rendered

The two that hurt are Dataview and Excalidraw, because people build their whole vault around them. Workarounds: replace Dataview index pages with hand-written maps of content, and export Excalidraw drawings to SVG (Export as image → SVG) and embed the SVG instead. Both are one-time costs, and the SVG version is better for the web anyway.

Attachments

Wherever your images live — Resources/, attachments/, alongside the notes — Quartz finds them, as long as they are inside content/. If an image is outside the vault, Obsidian will show it and Quartz will not.

Step 6 — Preview it

npx quartz build --serve

Open http://localhost:8080. This is a hot-reloading dev server: edit a note in Obsidian, save, and the browser updates. It is a preview server only — do not put it on the internet.

Useful flags: -d to point at a different content folder, -o for a different output folder, --port if 8080 is taken, -v when something is wrong and you need to see why.

Step 7 — Build

npx quartz build

This writes the finished site to public/. That folder is self-contained — HTML, CSS, JS, images, an RSS feed and a sitemap. Any static host can serve it.

Step 8 — Deploy

Pick one. All of these assume your Quartz clone is pushed to a git repository of your own (npx quartz sync --no-pull does the first push).

Cloudflare Pages

Connect the repo, then set:

OptionValue
Build commandnpx quartz build
Build output directorypublic
Framework presetNone

Cloudflare shallow-clones by default, which breaks git-derived timestamps. If you rely on those, use git fetch --unshallow && npx quartz build as the build command instead.

GitHub Pages

Add .github/workflows/deploy.yml:

name: Deploy Quartz site to GitHub Pages
 
on:
  push:
    branches: [v4]
 
permissions:
  contents: read
  pages: write
  id-token: write
 
concurrency:
  group: "pages"
  cancel-in-progress: false
 
jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # needed for git-based timestamps
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx quartz build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: public
 
  deploy:
    needs: build
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/deploy-pages@v4
        id: deployment

Then set Settings → Pages → Source to GitHub Actions. For a custom domain, point an apex domain at GitHub’s four A records (185.199.108-111.153), or a subdomain via CNAME to <username>.github.io.

Netlify or Vercel

Build command npx quartz build, publish directory public. Vercel additionally needs a vercel.json at the repo root, or every URL will demand a .html extension:

{
  "cleanUrls": true
}

Your own server

public/ is just files, so rsync is a complete deployment strategy:

npx quartz build && rsync -avz --delete public/ user@host:/var/www/eecs.blog/

The one thing to get right is the URL rewriting. Quartz emits about.html, not about/index.html, so a default config will 404 on /about. For nginx:

server {
    root /var/www/eecs.blog;
    error_page 404 /404.html;
 
    location / {
        try_files $uri $uri.html $uri/ =404;
    }
}

Caddy handles it in one line:

eecs.blog {
    root * /var/www/eecs.blog
    try_files {path} {path}.html {path}/
    file_server
}

Step 9 — The update loop

Once it is live, publishing is:

  1. Write in Obsidian, as normal.
  2. npx quartz sync — commits and pushes your content, pulling in changes from your other machines first.
  3. Your host rebuilds automatically.

If you self-host, replace step 3 with the rsync line above. Either way it is one command, which is the point — a publishing pipeline you have to think about is one you stop using.

When something goes wrong

Worth the afternoon?

The setup is a couple of hours the first time. After that the marginal cost of publishing a note is zero — you write in the same editor you already use, and the site follows. Nothing about the arrangement is proprietary: it is a folder of markdown, a build command, and a pile of static files.

Start with the example vault if you want to see the shape of it before committing your own notes.