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:

The parts that were fiddly

Three things needed real care:

  1. 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.
  2. 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.
  3. 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.