What are we doing??

I have wanted to self host an obsidian publish site for a bit, Digital Garden, Hugo , and some other options that I’ve messed with in the past just didn’t sit right for me. Then I ran across Quartz and things clicked. it seemed like a lot better option for my workflows so I decided to pull the trigger on it.

Updated November 2026

The first version of this setup put the Quartz tooling inside the Obsidian vault. That worked, but it meant Obsidian was indexing node_modules (about 17,000 files) on every launch and Obsidian Sync was dragging it along too. The layout is now flipped: the vault lives inside the Quartz folder, and only content/ gets published. This article describes the current setup. The gotchas I hit on the way are at the bottom.

The layout

~/Documents/Quartz/           <- git repo, what Cloudflare builds
├── quartz/                   Quartz source
├── quartz.config.ts, quartz.layout.ts, package.json, ...
├── node_modules/, public/    local only, never committed
└── Sync Vault/               <- the Obsidian vault. Open THIS.
    ├── .obsidian/
    ├── content/              <- the ONLY folder that is published
    └── Church/, Archive/, Daily Notes/ ...   private, stay here

Obsidian only ever indexes and syncs the folder you open as a vault, so it never sees the Quartz tooling one level up. Git only tracks the tooling plus Sync Vault/content. Nothing private can reach the repo, and nothing heavy can reach Obsidian.

Setup

We’re doing this on Arch Linux (Omarchy to be exact). Clone Quartz somewhere that is not your vault:

git clone https://github.com/jackyzha0/quartz.git ~/Documents/Quartz
cd ~/Documents/Quartz
npm i

Then move your existing vault folder inside it. Quit Obsidian first.

mv ~/Documents/"Sync Vault" ~/Documents/Quartz/"Sync Vault"

Your published notes go in Sync Vault/content/. Everything else in the vault stays private.

Moving an existing vault without upsetting Obsidian Sync

Don’t re-open the moved folder with “Open folder as vault”. That creates a new vault ID and Sync has to re-merge everything. Instead, with Obsidian closed, edit ~/.config/obsidian/obsidian.json and change the path of the existing vault entry to the new location. Same ID, same files, same relative paths: Sync sees nothing changed.

And whatever you do, don’t let Obsidian open the old, now-empty path under the old ID. Sync would treat every note as locally deleted and push those deletions to your other devices.

Publishing up notes.

For notes that live in the Sync Vault/content folder, we add this front matter to the document.

---
title: Example Title
draft: false
tags:
  - example-tag
---
 

There’s more details on front matter here on The Quartz site.

To build locally and check what will be published, run this from the repo root:

npx quartz build -d "Sync Vault/content" --serve

Git Setup

Create a repo on GitHub, then set up git in ~/Documents/Quartz (the repo root, not the vault).

WARNING

I have a lot in this vault, so the .gitignore ignores everything by default and only lets through the Quartz tooling and Sync Vault/content. Git can’t un-ignore a file inside an ignored folder, so the vault section works in three steps: allow the vault folder, ignore its children, then allow content back.

# Ignore everything by default
*
 
# --- Quartz tooling needed by the Cloudflare Pages build ---
# (dependencies are installed by Pages from package-lock.json; node_modules is NOT committed)
!quartz/
!quartz/**
quartz/.quartz-cache/
!package.json
!package-lock.json
!quartz.config.ts
!quartz.layout.ts
!tsconfig.json
!globals.d.ts
!index.d.ts
!LICENSE.txt
!README.md
!.gitignore
 
# --- The Obsidian vault: publish ONLY Sync Vault/content ---
!Sync Vault/
Sync Vault/*
!Sync Vault/content/
!Sync Vault/content/**
git init
git add .
git commit -m "initial commit"
git branch -M main
git remote add origin $REPO.git
git push -u origin main

Sanity check before the first push: git ls-files | wc -l should be a little over a thousand for a vault this size, not twenty thousand. If it’s huge, node_modules leaked in.

Connecting CloudFlare Pages

https://pages.cloudflare.com/

On the Cloudflare site, select Workers & Pages, then at the top is an ”+ Add” button, click on this and select “Pages”.

Import a Git Repository and enter the following details. Framework preset - “None” Build command - npx quartz build -d "Sync Vault/content" Build output directory - public

Hit the “Save and Deploy” button and we’ll be online shortly! Pages installs the dependencies itself from package-lock.json, which is why node_modules never needs to be committed.

The -d flag is not optional

If Quartz can’t find its content folder it does not fail. It happily builds a site with zero pages, exits successfully, and Cloudflare deploys it. I learned this by taking the site down for ten minutes. Cloudflare only keeps the last good deployment live when a build fails, and this one doesn’t.

As a second line of defence I changed the default in quartz/cli/args.js from "content" to "Sync Vault/content", so a plain npx quartz build also does the right thing in this repo.

Build Watch Path Configuration

Quartz has a structure to its configuration, in this case we only really need to “watch” the content folder as that’s where all the magic is. Under the “Settings” tab for your project, we’ll select “Build Watch paths” and enter in Sync Vault/content/*

This tells Cloudflare to only build when something under that path changes. Get this wrong and nothing breaks, builds just stop triggering, which is confusing in a different way.

Adding a custom domain

Under our project setting in Cloud Flare we’ll go to the “Custom domains” tab, with this option you’ll simply enter the domain you have setup in your CF account and it should automatically configure itself.

Making builds fast

Out of the box Quartz renders a social-preview image for every page (Plugin.CustomOgImages() in quartz.config.ts). With a full Bible in the vault that’s 1,338 images per build and took the emit step from seconds to minutes. Deleting that one line from the emitters list dropped the build from about four minutes to under one, and the output from 120 MB to 95 MB. The images only ever show up as link previews in chat apps, so I didn’t miss them.

Update: Quartz v5 (November 2026)

The site now runs Quartz v5. The big change is that v5 configures everything in a quartz.config.yaml plus a plugin system, so the few behaviours I had patched in code live as small local forks of the community plugins under plugins/ in the repo (footer, Bible folder ordering, search-index size cap, era-aware dates, canvas paths, same-page heading links). Two things that bit me:

  • Watch paths. If Cloudflare’s build watch path only covers content/, a push that changes only plugins or config never builds. Include plugins/*, quartz/* and the config file.
  • The footer plugin has to be named footer. Quartz binds that layout slot to the plugin name, so a renamed fork silently leaves the slot empty.

What v5 bought me: Obsidian canvases and Bases render as real pages, and the plugin forks are a cleaner place for customizations than patching the engine. Builds take longer (~13 minutes) because every build restores and compiles the plugins.

Gotchas I hit

  • quartz/static/icon.png and og-image.png must exist. The Favicon emitter hard-fails the whole build without them. They’re in upstream Quartz; if they go missing, restore them from git or the upstream repo.
  • Don’t git checkout an older branch inside the repo while Obsidian is open. Git will remove any tracked content files that branch doesn’t have, Obsidian sees your notes disappear, and Sync propagates that.
  • .canvas and .base files in content/ are published as raw JSON, not rendered. Quartz v4 has no canvas support. Keep them out of content/ unless you want them downloadable.
  • Custom callouts work. An unknown callout type like [!BIBLE] comes through as a custom callout. Colour and icon come from quartz/styles/custom.scss.