The premise
The old site was an Astro and Sanity build with a game-world portfolio â a hero character walking across a map. Handsome, and completely wrong for what was actually needed: a place to put 186 notes about ESP32 temperature sensors, a Grundig camera system, HOMM3 internals, and language study, without opening a CMS dashboard ever again.
The requirement was one sentence long:Â write in Obsidian, have it appear on the internet, do nothing else.
That sentence turned out to contain about a dozen hidden problems.
Act I â Choosing the machine
Quartz v5Â won because it reads an Obsidian vault natively. No export step, no content transformation, no second copy of the truth.
The first decision that mattered was structural, and it wasnât obvious at the time.
1. Where does the vault live?
The instinct is to keep the vault separate and point Quartz at it. But Quartz builds from content/ inside its own repo, and Obsidian Git can only commit downward from the vault root â its âcustom base pathâ setting handles a repo nested inside a vault, never the reverse.
Resolution: open the Quartz repo root as the Obsidian vault, with notes in content/. One .git, one commit, everything versioned together â config, stylesheets and notes in the same snapshot. This is also what the canonical Quartz tutorials do; it looked like a hack and wasnât.
Cost: .obsidian/ moves up to the repo root, and every vault-absolute path in the notes gains a content/ prefix. Attachment folder settings and Dataview FROM clauses need rechecking.
2. Obsidian shows folders but not code files
After the move, .ts, .scss and .yaml files vanished from the file explorer.
Not a bug. Obsidian only renders markdown, canvas, images and a few media types. The files are on disk and â crucially â Obsidian Git commits them regardless of what Obsidian displays. It runs git add against the working tree, not against Obsidianâs view of it.
Resolution: edit code in VS Code, notes in Obsidian, same folder underneath. For explorer clutter, a CSS snippet at .obsidian/snippets/hide-quartz.css hides the scaffolding by data-path. The âExcluded filesâ setting alone doesnât do it â that only affects search and suggestions.
Act II â The publishing loop
3. âCanât Quartz just push on every change?â
It canât. Quartz is a build tool that runs and exits; GitHub Actions only wakes on a push it receives. Neither can notice a file changing on a laptop.
Surveying what Quartz users actually do:
| Approach | Trigger | Mobile |
|---|---|---|
npx quartz sync by hand | you type it | no |
| Obsidian Git, timed | interval | yes |
| Quartz Syncer plugin | button | yes |
| cron / systemd timer | interval | no |
Every automated option is something on a timer running git push. Thatâs the entire design space. The popular video tutorial doesnât automate at all â its publish step is literally ârun npx quartz sync.â
Resolution:Â Obsidian Git, the only option covering Ubuntu, Windows and Android.
4. Commit often, push rarely
The interval isnât one setting, itâs two â and conflating them costs money.
Commits are free and local. Pushes trigger a site rebuild, which consumes hosting quota.
Settings landed on:
| Setting | Value | Why |
|---|---|---|
| Vault backup interval | 10 min | never lose more than 10 minutes |
| Auto push interval | 30 min | one rebuild per half hour, not per paragraph |
| Pull on startup | on | prevents divergence across three devices |
| Push on startup | on | catches whatever was open at shutdown |
Obsidian Git skips the commit entirely when nothing changed, so an idle vault costs nothing.
5. Why pull at all?
Push is not the counterpart of pull. Push publishes; pull prevents conflicts. Writing from three devices without pulling means editing a stale copy and resolving a merge conflict by hand â in Obsidian, on a phone.
6. Git vs. Remotely Save + Dropbox
Both were running, watching the same folder, on overlapping timers.
Git merges line by line, so two devices editing different paragraphs of one note combine cleanly. Dropbox operates on whole files and produces note (conflicted copy).md. But when two devices edit the same lines, git gives you conflict markers to resolve manually â better at avoiding conflicts, equally unpleasant at resolving them.
Resolution:Â git alone, with Android treated as capture-only. Conflicts need two devices touching one file; if the phone only creates new notes in an inbox folder, the failure mode disappears by construction.
7. GitHub authentication
remote: Invalid username or token. Password authentication is not supported.
fatal: could not read Username: No such device or address
Two errors stacked. GitHub dropped password auth for git in 2021; and the https:// remote tried to prompt for credentials via ssh-askpass, which wasnât installed.
Resolution:Â Personal Access Token or SSH keys. Token was generated in GitHub / Settings / Developer settings / Personal access tokens / classic. Then pasting these lines into git to do not have manually write credentials each time pushing or pulling something.
git config --global credential.helper store
git push
Act III â Where to host it
This took three attempts and one wrong turn.
8. GitHub Pages needs a paid plan for private repos
The vault lives in the repo, so the repo is private. GitHub Pages serves private repos only on Pro and above â roughly $4/month.
9. Netlify vs. Cloudflare: the unit matters
The familiar choice was Netlify (existing account). But the two platforms count differently, and for this workload the difference is decisive:
- Netlify: 300 build minutes/month. A ~2 min Quartz build = ~150 builds â 5/day
- Cloudflare:Â 500Â builds/month, duration irrelevant â 16/day
Quartz builds get slower as a vault grows, so Netlifyâs effective ceiling drops over time while Cloudflareâs doesnât. Exactly backwards for a garden that keeps accumulating.
Resolution:Â Cloudflare Pages.
10. The Workers detour
Cloudflareâs dashboard now nudges everyone toward Workers. Following âCreate a Workerâ led to a screen asking for a deploy command â npx wrangler deploy â which expects a Worker script, not a folder of static HTML.
Two failures followed:
Authentication error [code: 10000]
Token had Super Admin on the account but lacked the explicit Cloudflare Pages permission. Account role and token scope are separate in Cloudflareâs model.
â [ERROR] The Pages project "xzcxfe" does not exist.
Workers Builds doesnât provision a Pages project as a side effect. xzcxfe was the random slug Cloudflare assigned the Worker.
Resolution: abandon that flow entirely. The sidebar is now labelled Compute (Workers), and Pages is a separate option on the Create screen â not a separate section. Compute (Workers) â Create â Pages â Connect to Git.
11. The settings that finally worked
| Field | Value |
|---|---|
| Production branch | v5 |
| Framework preset | None |
| Build command | git fetch --unshallow && npx quartz plugin install --from-config && npx quartz build |
| Build output directory | public |
NODE_VERSIONÂ env var | 22 |
Three non-obvious pieces:
git fetch --unshallow â Pages clones shallow; Quartz reads git history for each fileâs last-modified date. Without this, every page shows the same date.--from-config â installs plugins listed inÂquartz.config.yaml. Skipping it reproduces theÂCannot find module '@quartz-themes/default' error on the build machine.NODE_VERSION=22 â Pagesâ default is older than Quartz v5 wants.
Act IV â The disappearing vault
The worst bug of the rebuild, because the error message pointed at the wrong thing.
12. Found 1 input files from 'content'
186 markdown files on disk. Quartz found one.
The build log said Filtered out 0 files, which looked like exoneration for ignorePatterns â but that line refers to the Filters stage (RemoveDrafts et al.), which runs after parsing. ignorePatterns applies during discovery, so excluded files are never counted as found in the first place. The log line that appears to rule out the culprit is actually consistent with it.
Suspicion fell on the pattern list. It was wrong.
Actual cause: the vault sat at content/Obsidian_Vault/Public/... nested. Unquoted spaces break glob matching, so patterns intended to exclude one folder matched far more of the path than intended.
Resolution: flatten. Vault contents directly under content/, Everything appeared immediately.
13. Excluding loose notes from the content root
The want: type a quick note without choosing a folder, donât publish it.
yaml
ignorePatterns:
- "!(index).md"Act V â Making it look like something
14. The CSS that did nothing
A hand-written custom.scss was applied. Exactly one rule took effect â the timestamp box. Everything else was inert.
Three distinct failure modes were in play, and they need different fixes:
| Bucket | Symptom | Fix |
|---|---|---|
| 1. Losing the cascade | selector matches, but theme CSS overrides it | raise specificity |
| 2. Selector matches nothing | v4 class name, renamed in v5 | find the real class name |
| 3. Works, but imperceptible | 3.5% opacity grain; Chromium-only scroll animation | see it before debugging it |
The decisive test â body { background: #ff0000 !important; } as the first line. Screen goes red = file loads, cascade problem. Screen unchanged = file never reaches the browser. That single result halves the problem, and the two halves have nothing in common.
Result: red. So, cascade.
Root cause, found in the generated HTML: the class list contained markdown-preview-view, popover-hint, nav-folder-title â Obsidianâs class names, not Quartzâs. Thatâs the whole mechanism of @quartz-themes: it relabels Quartz markup with Obsidian classes so unmodified Obsidian themes apply.
Which means theme rules look like .markdown-preview-view h2 â specificity 0,1,1. Custom rules looked like .page article h2 â also 0,1,1. An exact tie, broken by source order, and theme CSS loads later.
Every rule lost by exactly one position.
Resolution: wrap everything in .page.page.page. Matches identical elements, scores 0,3,x, clears the theme cleanly â no !important anywhere.
scss
@use "./base.scss";
.page.page.page {
article h2 { font-size: 1.3rem; }
}Sidebar rules go in a parallel .sidebar.sidebar.sidebar block, since the sidebar is a sibling of .page, not a child. Viewport-fixed effects (grain overlay, scroll progress) stay outside both â scoping them to .page would clip them to the article column.
15. Theme plugin: enable or disable?
Both, depending on intent â and this flipped mid-project:
- Hand-written palette? SetÂ
quartz-themes toÂenabled: false, or it injects its own variables after yours and silently overwrites them. - Adopting a theme? Leave it enabled. Thatâs the entire point.
Landed on origami.gruvbox via @quartz-themes, with a slim custom.scss layer on top for structure the theme doesnât provide.
Related: quartz.config.yaml had a typography block (Fraunces) fighting the themeâs own fonts â visible as fonts loading twice in the HTML, with Quartzâs defaults arriving last. Delete the typography block and let the theme own it.
16. Trying a different theme without destroying anything
bash
git commit -m "working state: origami.gruvbox"
git checkout -b theme/cyberglowExperiment freely; git checkout main && git checkout -- . restores everything. Preview first at quartz-themes.github.io/<theme-name> and skip the branch entirely if itâs wrong.
Anything in custom.scss using var(--secondary), var(--lightgray) etc. follows whatever palette is active. Hardcoded hex values (--tannin: #b98a4b) donât.
Carried over from earlier sessions
Two problems predating this rebuild. Both still open â recorded here so they arenât rediscovered from scratch.
Waypoint
Used for folder-based table-of-contents generation. Two failure patterns:
- Subfolder links missing the parent path prefix â generated links donât resolve.
- Self-referential links using vault-absolute paths prefixed withÂ
Public/, which escape Quartzâs content root entirely.
Partial progress: Waypointâs Custom Filename setting accepts index, which resolves part of the Waypoint/Quartz naming conflict.
Also established: Obsidian comment syntax doesnât hide Waypoint blocks in Quartz. Only the marker lines get stripped, leaving the bare link list visible. Hiding them properly needs either a custom transformer plugin (order < 30, running before OFM) or a pre-build perl one-liner in CI.
Note: the flattening fix from §12 changed every path in the vault. Waypoint output needs regenerating and re-checking before either symptom can be diagnosed further â the old broken paths are no longer the current broken paths.
Local graph view
A slug mismatch in quartz-community/graph renders folder index pages as isolated nodes in the local graph. Pages are keyed as studies/khan-academy/index but served at /studies/khan-academy/, so edges donât connect.
Global graph is unaffected. Upstream bug, not a config error.
CrawlLinks
An earlier session found empty links arrays in contentIndex.json, suggesting the CrawlLinks transformer may be absent from the plugin pipeline. Unverified. If confirmed, this would explain both the graph behaviour and the Waypoint link resolution â worth checking before treating them as separate problems.
Still outstanding
-  Rotate the exposed credentials. Two published files carried a camera password and a service identifier.
- Â Explorer disappearing on click â isolation test (comment out theÂ
.explorer block, rebuild) never run -  Mobile footer overlapping Recent Notes
- Â Delete theÂ
typography block fromÂquartz.config.yaml -  Regenerate Waypoint output against the flattened paths
- Â VerifyÂ
CrawlLinks is in the pipeline -  RunÂ
npx quartz upgrade once, on a branch, to find out what breaks before six months of notes depend on it - Custom footer â*Built with Quartz v5 · Written in Obsidian · Synced via Git · Published on Cloudflare Pagesâ