Changelog

Kolofon · 1 September 2026

0.11.13 — spoken versions of entries

18:31 — an engine sync would have deleted the day-old player

0.11.13 — spoken versions of entries

Text: Kolofon

An entry can now have a spoken version. A player sits under the title, the byline — author, date, views — gains the length of the recording, and the same figure appears next to the title in the series listing and in the stream. Anyone who came to listen finds the button immediately, without scrolling past text they have chosen not to read.

The player is made of the same parts as the rest of the page: a hairline rule, a triangle, a clock. No border, no rounded corners, no shadow. It does not touch the file until someone clicks — the duration comes from the entry data, not from the recording's metadata. Otherwise every visit would pull a megabyte for a reader who came to read.

Files are served by their own route, not the one for images. An image is fetched whole or not at all, so the image route answers with an `immutable` header and knows nothing about range requests. Audio behaves the other way round: the browser first asks for two bytes to learn the length, then for further pieces as it plays, and for a completely different stretch when the listener seeks. Safari on a phone will not start playing at all if the server answers with the whole file instead of the requested slice. Forcing audio through the image route would mean either breaking images or having audio without seeking.

The file's address carries a fingerprint of its contents. This looks like a detail and is in fact a condition of the thing working: if the route declares that nothing at this address will change for a year, then replacing the file under a fixed name never reaches anyone who has already fetched it. The symptom is misleading — the page shows the new duration, because the data arrived fresh from the server, while the player plays the old one, because the file came from the cache. The address has to change with the contents, not with the intention.

The boundary is a separate matter. The feature was built inside the leading instance, because that is where it could be seen fastest. A day later it turned out to block the engine sync: the `src/lib/` and `src/components/` directories synchronise with deletion, so rolling out any new release would have removed the freshly written player. The choice was simple — either do every subsequent release by hand in four places, or move the mechanism where it belongs.

The boundary runs where it always does: the engine knows the mechanism, the instance knows its own recordings. The route, the player, the address construction, the fields in the entry type and two interface strings go to the engine. The frame number, the files and the tools that make them stay with the instance. None of the moved files knows anything about a particular site — the storage binding, the frame number and the strings all come from configuration.

A lesson for next time: a feature written the engine's way but sitting inside an instance looks harmless right up to the moment you need to release anything else. The cost of moving it grows with every day of delay, because the number of places where the same work has to be repeated by hand grows too.