Changelog

Kolofon · 11 September 2026

0.11.69 – 0.11.78 — themes, instance settings and a nudge about notifications

after two days in which a single instance became a proving ground for the whole engine

0.11.69 – 0.11.78 — themes, instance settings and a nudge about notifications

Text: Kolofon

Ten releases in two days, nearly all of them from one source: a new instance that was meant to look unlike the others. Every time something was missing, the choice was the same — write an exception into the instance, or turn it into a field in engine configuration. Ten times out of ten the second option won.

A third theme

Themes are no longer a pair. `book` and `dev` are joined by `docs`: light, cool, with a navy accent and a dark bar as its only frame. That is what documentation looks like — white space, a narrow column, plenty of air, colour used strictly as a signal.

One criterion guided it: the new theme has to differ from both of the others at a glance, rather than read as a variant of either. A site for reading prose, a technical calling card and a product's documentation are three different promises.

theme: "docs",
The entire declaration on the instance side

The `dev` theme was finished off along the way. It had kept light paper under the text, so moving from a dark landing screen to a white list of posts looked like two different sites glued together. Everything is dark now, and syntax colours in code blocks go through variables — the same palette cannot serve both paper and graphite.

Five new fields

Each came out of a specific piece of friction, not out of an idea for a feature.

tileImageFit:   "cover" | "contain"
postImageWidth: "bleed" | "column"
headerCompact:  boolean
layoutWidth:    "prose" | "wide"
engineMark:     string
site.config.ts — all optional; absent means unchanged behaviour

**tileImageFit** — a grid tile cropped images to a fixed height. For photographs that is right: the frame is a decision anyway, and an even grid reads better than faithful proportions. For a graphic carrying text, a crop halfway through a sentence takes its meaning away.

**postImageWidth** — the lead image always ran the full width of the page. A photograph gains from that. A graphic with captions, stretched across a frame twice as wide as the text beneath it, looks like a banner from another site, and its lettering ends up larger than the post title.

**headerCompact** — the header sits in two rows by default, because that layout survives a growing number of series. An instance with a handful of entries fits everything into one bar and wins back more than seventy pixels above the fold.

**layoutWidth** — the grid for header, footer and engine credit, computed in one place. Previously the bar sat on one width and the content on another; on a wide screen that produced tens of pixels of misalignment, and it shows immediately.

**engineMark** — the engine's mark beside the credit in the footer. Without the field the credit is plain text, exactly as before.

A nudge about notifications

An installed app asked for nothing. Permission for notifications had to be granted by hand in system settings — a place nobody visits.

The new `PushPrompt` follows the same pattern as the install prompt: a floating bar at the bottom edge, a button, a cross, the decision remembered under an instance key. Deliberately not a new mechanism — it differs only in what it asks about.

We do not raise the permission dialog on launch, and that is a decision rather than a half-measure. Browsers penalise a request made without a user gesture: they quieten it, and sometimes decline on the user's behalf. **A refusal is stored permanently**, so one badly chosen moment closes the door for good. The bar explains what this is for before the system asks.

Motion you can actually see

Three fixes from one family, all found on a device rather than in tests.

Animations started when an intersection observer reported the element — which is not the same as the element being visible. An element waits its turn in the entrance sequence, so counters ran to their target and the console typed out a whole command **at zero opacity**. By the time either appeared, the finished result was sitting there.

The fix: start only once effective opacity — multiplied through the ancestors, because an element's own value is always one — passes the threshold, the element is within the viewport, and the layout has settled. Add zeroing before the first paint so the finished state never flashes, and a fallback that restores the content if nobody ever scrolls that far.

Because that is the governing rule here: **the number and the command are content, and the animation only reveals them**. An element left empty is a bug, not a missing effect.

Propagation

Release 0.11.63 introduced automatic propagation of the engine to instances. The flaw only showed on the second release from the same branch: the workflow asked for a pull request without filtering by state, found the closed one from last time and tried to edit it. The whole run fell over.

It now asks for open ones only. In passing, the strings guard proved it does exactly what it was built for — it stopped two instances missing translations of the new strings and opened drafts for them instead of ready pull requests.

What this leaves

All of these fields share one property: **no declaration means previous behaviour**. Instances that know nothing about them noticed none of the ten releases — and that is the only measure of success for a run of changes like this.

Not that the new variant looks good. That the old ones look exactly as they did yesterday.