orgo

Incremental builds

Editing one post rebuilds four pages, whatever the size of the site.

Incremental rebuilding is not an optimisation bolted on to orgo; it is the constraint the data model was built around. Parsing is a pure function of one file's bytes, link resolution reports the edges it used, and rendering is a pure function of a resolved document. Those properties are what make caching sound — and they are also what make the build parallel.

You do not have to configure any of this. It is described here because knowing what invalidates what explains the behaviour you will see.

What you observe

$ orgo build content -o _site
built 182 page(s) (182 rendered, 0 cached) ...

$ orgo build content -o _site
built 182 page(s) (0 rendered, 182 cached) ...

$ vim content/blog/post.org && orgo build content -o _site
built 182 page(s) (4 rendered, 178 cached) ...

The four are the post itself, its section index, its tag page, and the tag index whose counts changed.

The render key

Every page has a key composed from four hashes:

ComponentChanges when
contentThe source file's bytes change.
resolved linksA link's target moves, is renamed, or disappears.
configorgo.toml changes, or the shared chrome does.
templatesThis page's layout changes, or something that layout extends or includes.

If a page's key matches the cached one and its output file still exists, the file on disk is already correct and is left untouched.

Template scope

The template component covers the layout a page actually renders through, plus everything that layout pulls in — followed through {% extends %}, {% include %}, {% import %} and {% from %}. Editing feed.xml on a 196-page site re-renders one page; editing a post.html that only blog posts use re-renders the posts. base.html is extended by almost everything, so editing it still re-renders almost everything — which is correct, and is why the win shows up on the other edits.

A template whose include is computed at render time — {% include chooser %} — cannot be followed, so it is treated as depending on every template. Over-invalidating costs time; under-invalidating publishes a stale page.

The cache lives in <output>/.orgo-cache.json and is tagged with a format version. A version mismatch, a missing file or a corrupt file all fall back to a full rebuild — the cache is an optimisation, never a correctness dependency. There is a test for each of those three fallbacks.

Resolution records which targets each page consumed, which gives the build a dependency graph. That is what makes renaming a heading work:

a.org:  * Target Heading
b.org:  Jump to [[*Target Heading][there]].

Rename the heading in a.org and both pages re-render — b.org because the URL it emits has changed. Without the graph, b.html would keep a link to an anchor that no longer exists. On a rebuild the graph is merged with the previous build's, so a target that was removed still pulls in the pages that linked to it.

Global chrome

The navigation appears on every page, so a change to it must re-render every page. The site-structure hash covers exactly the pages that can appear in the nav — which is why the default nav.mode = "top-level" matters for more than aesthetics:

Turning on [templates] expose_page_list widens that hash to every page, because then any template can read any page's metadata. That is the documented cost of building an index by hand instead of with a collection.

Generated pages

A listing page has no source file, so it is cached on the thing it actually depends on: the entries it lists — their URLs, titles, dates and tags.

A tag page depends on its own posts and not on the other groups. That is why the group list is given to the tag index and not to every tag page: a page that could see every group would depend on every group, and one new post would re-render every tag page.

Byte equivalence

A full build (--no-cache) and an incremental rebuild produce byte-identical output. This is the property everything else rests on, and it is a test rather than an intention: the suite builds a site both ways and compares every emitted file.

Parallelism

Parsing, resolution and rendering run across cores. Measured on a 1,790-page corpus, a full build went from 3.98s to 0.82s on 12 cores; the 179-page reference corpus builds in 0.07s.

Parallelism is not observable in the result. Emitted bytes are unaffected, and the build report — the order of rendered and skipped — is assembled sequentially afterwards, so a build is reproducible run to run. There is a test for that ordering, because a non-deterministic report over a deterministic site would be a confusing thing to debug.

When to reach for –no-cache

Almost never. Config changes, template edits and cache-format upgrades all invalidate correctly on their own. It exists to answer "is the cache lying to me?" — and if it ever is, that is a bug worth reporting, with the two builds' output to compare.

orgo build content -o _site --no-cache