A content compiler that runs at build time
The rule I set for myself was simple: the app should never do work at runtime
that could be done once, at build time. Wikilinks, backlinks, revision
histories, facet counts: all of it is computed ahead of time by a single Node
script, scripts/compile-content.mjs, and written out as static JSON the app
just reads.
Why a plain Node script
I considered writing this as a Vite plugin with virtual modules and decided
against it. A plain .mjs file with no React and no Vite imports is trivially
unit-testable (you point it at a fixtures directory and assert on the JSON it
produces), and there is a real test suite doing exactly that. In dev, a small
watcher re-runs it whenever anything under content/ changes, so the ergonomics
are the same as a plugin without the coupling.
What it produces
Two kinds of output:
index.json: every published note's metadata, the full connection graph, backlinks, timelines and facet counts. The app loads this once at startup.notes/<slug>.json: one file per note, the same metadata plus the processed body HTML, fetched lazily when you open that note.
The parts that were fiddly
Three things needed real care:
- Wikilinks run once, over the produced HTML of both formats. HTML notes
and Markdown notes are parsed differently, but the
[[slug]]resolution happens in a single shared pass afterwards, so the two formats can never drift apart. An unresolved slug becomes a visible "not yet planted" placeholder and a build warning, never a broken link. - Backlinks are just the reverse index. If note A links to B, then B lists A as a backlink. Cheap to compute once you have every note's outgoing links.
- Revisions come from
git log. Each note's timeline is derived from the commits that touched its file, newest first, and every revision links out to that exact version on GitHub.
The git step also has to fail soft. On a shallow clone there may be no history at all, and the build must still succeed: it falls back to the declared date and an empty revision list rather than erroring out.
This note sits under the same This garden, and how it’s built project, and the whole pipeline only exists because of the spec I wrote first in Spec-driven development with OpenSpec and AI agents.