Appearance
Spike: static-site generator evaluation for the docs site
Status: Complete — recommendation below. Implemented in #689: the docs-site tooling is now engine-pluggable — VitePress (the recommendation) is the default and Astro Starlight (the runner-up) ships as a selectable alternative for a live A/B. Quartz and
fix-links.jswere removed. References to Quartz andfix-links.jsbelow describe the pre-migration state this spike evaluated. Question (#487): Quartz 4.4.0 emits internal links that are off-by-one../under Cloudflare Pages' flat (no-trailing-slash) serving, forcing a post-build workaround (scripts/docs-site/fix-links.js). Would a different SSG — or a newer Quartz — render BoB's Diátaxis docs correctly without post-build hacks, while staying markdown-native? Origin: Surfaced by the #463/#464 documentation overhaul (flatdocs/→ category subdirs), which made the link bug pervasive. Date: 2026-06-27.
TL;DR
- Quartz v5 does not fix the problem. The current pin (
v4.4.0) and the newest line (v5.0.0) both emit depth-relative links computed against trailing-slash directory URLs (note/index.html). Quartz has notrailingSlash/cleanUrls/flat output option, so the off-by-one persists in v5 — an upgrade alone keepsfix-links.js. - Recommendation: migrate to VitePress. It is the only candidate that resolves cross-directory links correctly under flat Cloudflare-Pages serving natively, with zero config and no post-build rewriting — because it emits flat
.htmlfiles by default and the relative links are sized for that flat layout. It is Node-native (matches the existing toolchain), fast, actively maintained, and has built-in local search. - Runner-up: Astro Starlight. Solves the problem with two config lines + root-absolute authored links, and additionally gives folder-autogenerated Diátaxis nav, Pagefind search, and the richest theming/plugin ecosystem (including a wikilink plugin).
- The one real regression in any migration: Quartz's signature graph view + automatic backlinks. No mainstream alternative replicates it. If that feature is considered load-bearing for "Big ol' Brain", the alternative is to keep Quartz and keep
fix-links.jsas a deliberate, documented stopgap — but that does not meet the "no post-build hacks" goal.
AC-01 — The cost of the status quo (current Quartz pain)
What breaks
Quartz computes every internal <a class="internal"> link as a relative path (./, ../) and emits each page folder-style as note/index.html, i.e. at a trailing-slash directory URL (/note/). The relative links are sized against that directory assumption.
Cloudflare Pages, however, serves the build's flat <slug>.html files at no-trailing-slash URLs (/note). At a no-slash URL the browser's base directory is one level shallower than Quartz assumed, so every cross-directory ../ overshoots by exactly one segment. Concretely (from the fix-links.js header):
the handbook's
../reference/glossary.mdrenders as../.././reference/glossary, resolving to/reference/...(404) instead of/docs/reference/....
The #463 restructure (flat docs/ → Diátaxis category subdirs: tutorials/, how-to/, reference/, explanation/) turned this from an edge case into a site-wide breakage, because nearly every link is now cross-directory.
What fix-links.js papers over
scripts/docs-site/fix-links.js (wired into scripts/docs-site/build.sh, run after the Quartz build) is a post-build HTML rewriter. For every emitted .html file it:
- Walks all
<a class="internal">links (skips external/absolute/anchor/mailto). - Resolves each
hrefagainst both a folder-base and a flat-base for the page, because Quartz is internally inconsistent (some links folder-relative, some flat). - Picks whichever candidate points to a file that actually exists in
public/. - Rewrites it to a root-absolute
/docs/...URL that resolves regardless of trailing-slash. Unverifiable links are left untouched ("never made worse").
This is a ~80-line guess-and-check normalizer that must run on every build and re-derive the correct URL for every link by probing the filesystem. It works, but it is exactly the kind of post-build hack #487 wants to eliminate.
Version state
| Item | Value |
|---|---|
Quartz pinned tag (init.sh) | v4.4.0 |
| Newest Quartz line (June 2026) | v5.0.0 (default branch); last v4 tag v4.5.2 |
| Workaround | scripts/docs-site/fix-links.js + wiring in build.sh |
| URL-shaping config in Quartz | baseUrl only (absolute artifacts), no flat/trailingSlash option |
Note: Quartz's GitHub Releases page stops at v4.0.8; later versions ship as git tags only, so the Releases API "latest" is misleading — the tag list / default branch are authoritative. v5 still has no output-URL-format setting.
AC-02 — Candidate survey
Evaluated as of June 2026 against the criteria in AC-03. The decisive criterion — correct cross-directory link resolution under flat no-trailing-slash Cloudflare-Pages serving without post-build rewriting — is called out per candidate.
Latest Quartz (v5.0.0)
- Flat-link resolution: NOT-SOLVED. Same model as 4.4.0 — relative links against folder/trailing-slash URLs, no flat/
cleanUrls/trailingSlashoption incfg.ts. (markdownLinkResolution: shortest|absolute|relativecontrols wikilink-to-file matching, not output URL format — it does not help.) Still needs the rewrite hack. - Markdown-native: strongest — Obsidian-flavored MD, native
[[wikilinks]], transclusions, callouts, frontmatter. - Diátaxis nav: auto "Explorer" tree from folders. Search: built-in (FlexSearch). Graph view + backlinks: yes (signature). Build: fast (esbuild), incremental. Theming: SCSS + Preact overrides; opinionated digital-garden look. Maintained: yes.
- Migration: N/A (in-place upgrade; review v5 breaking changes).
Astro Starlight (0.41.1)
- Flat-link resolution: SOLVED-WITH-CONFIG. Sidebar/nav links are root-absolute and
base-prefixed (immune to../depth). Fix the trailing-slash default withbuild: { format: 'file' }+trailingSlash: 'never'and author internal links as root-absolute (/reference/glossary). No post-processing. (Astro does not reliably rewrite relative.md→.mdlinks — issue #5680 — so root-absolute authoring is the supported pattern.) The Cloudflare adapter dropping Pages support is SSR-only and irrelevant to a static upload. - Markdown-native: MD/MDX/Markdoc, schema'd frontmatter (
titlerequired). Wikilinks via plugin (remark-wiki-link/starlight-obsidian). - Diátaxis nav:
sidebar: autogenerate: { directory }from folders. Search: built-in Pagefind. Graph/backlinks: no. Build: Astro/Vite (fast). Theming: richest — CSS vars, Tailwind, component overrides, large plugin ecosystem. Maintained: very active. - Migration: medium-high (content into
src/content/docs/, frontmatter schema, wikilink conversion, links → root-absolute; lose graph/backlinks).
VitePress (1.6.4 stable)
- Flat-link resolution: SOLVED-NATIVELY. Emits flat
.htmlfiles by default (getting-started.html, not.../index.html); inbound links are relative and browser-resolved against that flat layout, so there is no../off-by-one under flat serving — out of the box, no config, no post-processing.cleanUrls: trueoptionally drops the.htmlextension; Cloudflare Pages maps/foo→/foo.htmlwithout a redirect, which the VitePress deploy docs call out as correct. - Markdown-native: markdown-it + Vue-in-Markdown, frontmatter, containers. Wikilinks via markdown-it plugin.
- Diátaxis nav: sidebar is manual config by default; folder autogen via community
vitepress-sidebar. Search: built-in local (MiniSearch). Graph/backlinks: no. Build: Vite (very fast HMR/builds). Theming: Vue default theme, extend/replace, CSS vars, slots — polished + customizable. Maintained: active (v2 alpha in progress). - Migration: medium (frontmatter largely compatible; wikilink conversion; author or autogen sidebar; lose graph/backlinks).
Docusaurus (3.10.1)
- Flat-link resolution: SOLVED-WITH-CONFIG. Rewrites
.md/.mdxlinks to the target's root-absolute, baseUrl-prefixed URL at build time (React Router<Link>), independent of page depth → survives flat serving. SettrailingSlash: falsefor flatmyDoc.html+ no-slash links. The most robust link model of the set. No post-processing. - Markdown-native: MD + MDX (JSX). Wikilinks not native (custom remark / pre-convert).
- Diátaxis nav: autogenerated sidebars from folders +
_category_.json. Search: not built-in (Algolia DocSearch or local plugin). Graph/backlinks: not native (communitydocusaurus-graph). Build: Node/React/Webpack — heaviest toolchain, slowest builds. Theming: most flexible (swizzling, Infima, React). Maintained: Meta-backed, very active. - Migration: moderate (frontmatter remap; wikilink conversion; lose graph/backlinks).
MkDocs + Material (core 1.6.1 / Material 9.7.6)
- Flat-link resolution: SOLVED-WITH-CONFIG. Default
use_directory_urls: true→page/index.html+ relative links against trailing-slash dirs → same off-by-one class as Quartz under flat serving. Setuse_directory_urls: false→ flatpage.html+ file-to-file relative links whose depth matches the URL at any nesting. CF 308-redirects.html→clean URL (one harmless hop). No post-build rewrite. - Markdown-native: Python-Markdown. Wikilinks via plugin (
mkdocs-roamlinksetc.). - Diátaxis nav: explicit in
mkdocs.ymlby default; folder-driven viamkdocs-awesome-nav/literate-nav. Search: built-in (lunr). Graph/backlinks: no. Build: Python toolchain (not Node); fast incremental. Theming: flexible (Jinja2 overrides, palettes). - Maintenance caveat: Material for MkDocs entered maintenance-only mode in 2026 (last feature release Nov 2025, fixes through Nov 2026; maintainer moved to a new SSG, Zensical). A real longevity risk for a fresh 2026 choice.
- Migration: moderate (reads YAML frontmatter; wikilink plugin; rebuild nav; lose graph/backlinks).
mdBook (0.5.3)
- Flat-link resolution: SOLVED-NATIVELY. Output is flat and mirrors source 1:1 (
src/reference/glossary.md→reference/glossary.html); cross-dir links are relative and resolve at any depth. CF's default.html-dropping yields canonical no-slash URLs. No config flag, no hack. Costs: one 308 hop per cross-page click; avoid README/index leaf files (section indexes get a trailing slash); setsite-url = "/docs/". - Markdown-native: CommonMark (pulldown-cmark). Wikilinks/backlinks not native.
- Diátaxis nav: hand-maintained
SUMMARY.md(or third-party autosummary). Search: built-in (Elasticlunr). Graph/backlinks: absent. Build: Rust binary (not Node); fastest. Theming: Handlebars + CSS vars (least component-driven). Maintained: active (rust-lang). - Migration: moderate-high — no native YAML frontmatter (needs a stripper; metadata effectively lost), wikilink conversion, author
SUMMARY.md; frontmatter + graph + backlinks all regress.
AC-03 — Scored comparison matrix & recommendation
Weighted criteria
The flat-serving link fix is the entire motivation for #487, so it dominates. Markdown nativeness and maintenance health are weighted next (a docs tool must read our corpus and must outlive the decision). Graph/backlinks is weighted modestly — it is a genuine Quartz strength but not why we're here.
| Criterion | Weight |
|---|---|
| Correct flat-serving links (no post-build hack) | 30 |
| Markdown-native (incl. wikilink path) | 15 |
| Maintenance / longevity | 15 |
| Diátaxis folder nav | 10 |
| Full-text search | 10 |
| Build speed / toolchain fit (Node) | 10 |
| Theming flexibility | 5 |
| Graph view + backlinks | 5 |
Each cell scored 0–5 (5 = best); weighted total normalized to 100.
| Criterion (weight) | Quartz v5 | VitePress | Starlight | Docusaurus | MkDocs+Material | mdBook |
|---|---|---|---|---|---|---|
| Flat links, no hack (30) | 0 | 5 | 4 | 4 | 3 | 4 |
| Markdown-native (15) | 5 | 4 | 4 | 4 | 3 | 2 |
| Maintenance (15) | 4 | 5 | 5 | 5 | 2 | 4 |
| Diátaxis nav (10) | 5 | 3 | 5 | 5 | 3 | 2 |
| Search (10) | 5 | 5 | 5 | 3 | 5 | 5 |
| Build/toolchain (Node) (10) | 4 | 5 | 5 | 3 | 2 | 3 |
| Theming (5) | 4 | 4 | 5 | 5 | 4 | 2 |
| Graph + backlinks (5) | 5 | 0 | 0 | 1 | 0 | 0 |
| Weighted total /100 | 64 | 87 | 86 | 79 | 57 | 64 |
Each weighted total is Σ(score·weight) / 5 (max cell 5 × total weight 100 = 500, normalized to 100). Worked detail:
- VitePress 87 = (5·30 + 4·15 + 5·15 + 3·10 + 5·10 + 5·10 + 4·5 + 0·5)/5 = (150+60+75+30+50+50+20+0)/5 = 435/5.
- Starlight 86 = (4·30 + 4·15 + 5·15 + 5·10 + 5·10 + 5·10 + 5·5 + 0·5)/5 = (120+60+75+50+50+50+25+0)/5 = 430/5.
- Quartz v5 64 = (0·30 + 5·15 + 4·15 + 5·10 + 5·10 + 4·10 + 4·5 + 5·5)/5 = (0+75+60+50+50+40+20+25)/5 = 320/5. Note: Quartz scores well on everything except the one decisive criterion — it is an excellent tool whose single disqualifier here is the flat-serving link bug it cannot fix natively.
VitePress and Starlight are close (87 vs 86); VitePress edges ahead purely on the decisive criterion — a native solve beats a config solve for a goal whose whole point is "no hacks". Starlight wins on nav autogen + theming. Both are defensible.
Recommendation: migrate to VitePress (Starlight a close second)
Why VitePress. It is the only candidate that satisfies AC-04's bar — correct cross-directory links on flat Cloudflare-Pages serving with no post-build rewriting and no special config — natively, because its default output is flat .html with relative links sized for flat serving. It is Node-native (the existing docs-site toolchain is already Node/npm), builds fast on Vite, ships built-in local search, and is actively maintained.
When to prefer Starlight instead. If folder-autogenerated Diátaxis sidebars and the richest theming/plugin ecosystem (incl. an Obsidian/wikilink plugin) matter more than a zero-config link solve, Starlight is the better pick — it costs two config lines (build.format: 'file' + trailingSlash: 'never') and a switch to root-absolute authored links, and gives a more "docs-product" feel out of the box. VitePress's one notable gap vs Starlight is sidebar autogen (needs the vitepress-sidebar plugin).
What regresses in any migration (and the honest counter-option). Quartz's graph view and automatic backlinks have no equivalent in VitePress, Starlight, Docusaurus, MkDocs, or mdBook. Wikilinks ([[...]]) are non-native everywhere except Quartz and need a plugin or a one-time conversion pass. If the graph/backlinks experience is judged load-bearing for the "Big ol' Brain" identity, the only way to keep it is to keep Quartz and keep fix-links.js as a documented, deliberate stopgap — which fails the "no post-build hacks" goal but preserves the signature feature at zero migration cost. This is a genuine product trade-off, not a technical one, and is the call to confirm before committing to migration.
Migration-effort estimate (Quartz → VitePress)
| Task | Effort | Notes |
|---|---|---|
Scaffold VitePress in .docs-site/, replace Quartz install/build/deploy scripts | M | init.sh/build.sh/dev.sh/deploy.sh rewrite; drop fix-links.js + .quartz-version |
Point config at docs/ as content root, set cleanUrls, base: /docs/ | S | site-config |
Sidebar: add vitepress-sidebar for folder autogen (or hand-author) | S–M | autogen plugin keeps it folder-driven |
Convert [[wikilinks]] → markdown links (or add a markdown-it wikilink plugin) | M | one-time codemod across docs/; size depends on wikilink usage |
| Frontmatter compatibility pass | S | VitePress frontmatter is permissive |
| Regressions to accept | — | graph view, automatic backlinks (no replacement) |
Update /docs-site skill + docs-site.json schema + tests | M | tooling churn mirrors current Quartz wiring |
Overall: medium. The bulk is tooling-script churn + a one-time wikilink conversion; content largely ports as-is. Risk is low because the current site renders fine today — this is a maintainability upgrade, scheduled, not urgent.
Features that would regress (migrating off Quartz)
- Graph view — none of the alternatives have it.
- Automatic backlinks — none have it natively.
- Native
[[wikilinks]]/ transclusions — plugin or conversion required everywhere. - Obsidian-vault authoring ergonomics — Quartz is purpose-built for it.
AC-04 — Proof of correct flat-serving link resolution
The PoC lives at scripts/docs-site/poc/. It does a real VitePress 1.6.4 build of a fixture that mirrors the docs/ Diátaxis taxonomy (category dirs plus a two-level-deep explanation/orchestration/ page, with the same cross-directory ../-links the real docs use), then runs verify-links.js — a neutral checker that resolves every emitted internal link the way Cloudflare serves it (flat, no trailing slash) and confirms it points at a real file. No post-build rewriting is run.quartz-model.js emits the same fixture with Quartz's folder-relative link math as the failing baseline.
Recorded result (2026-06-27, VitePress 1.6.4, Node 25):
VitePress (recommended): 7 pages, 30 links — PASS, every link resolves, no rewriting.
Quartz model (current): 6 pages, 24 links — FAIL, 19 broken under flat serving, e.g.
href="../../../reference/dev-lifecycle" -> /reference/dev-lifecycle (escaped site root)The Quartz failures are precisely the bug fix-links.js papers over (links resolve to /reference/... instead of /docs/reference/...). VitePress's links resolve correctly with zero post-processing, satisfying AC-04. Re-run with npm install && npm run build && npm run verify in the PoC dir.
Sources
- Quartz: https://github.com/jackyzha0/quartz/tags, https://quartz.jzhao.xyz/,
quartz/util/path.ts&quartz/cfg.ts(v5 branch) - VitePress: https://vitepress.dev/guide/routing, https://vitepress.dev/reference/site-config
- Starlight: https://starlight.astro.build/guides/sidebar/, https://starlight.astro.build/guides/authoring-content/; Astro relative-link behavior https://github.com/withastro/astro/issues/5680
- Docusaurus: https://docusaurus.io/docs/api/docusaurus-config, https://docusaurus.io/docs/markdown-features/links, https://github.com/slorber/trailing-slash-guide
- MkDocs/Material: https://www.mkdocs.org/user-guide/configuration/, Material maintenance-mode note https://squidfunk.github.io/mkdocs-material/
- mdBook: https://rust-lang.github.io/mdBook/format/markdown.html
- Cloudflare static-asset routing / html-handling: https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/