# 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:

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.

**Where old versions live.** My first instinct was to render every historical version of a note inside the site. I dropped that: it makes build time grow with the total number of commits, and GitHub already renders any past version of a file perfectly well. So a revision in the timeline is now just a permalink out to that commit's view of the file: zero build cost, and the history is still one click away.

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](/garden/this-digital-garden) project, and the whole pipeline only exists because of the spec I wrote first in [Spec-driven development with OpenSpec and AI agents](/garden/spec-driven-development-with-openspec).
