This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
I already keep a CHANGELOG.md — six hundred kilobytes of it, one entry per release, written for me. It is the wrong thing to point a visitor at: it is dense, it is in the voice of a diff, and half of it is internal. So /updates is a second, deliberately smaller thing — the newsletter version — and the board next to it is where the next entries come from.
The decision that shaped everything else was not to parse the existing changelog. It would have been less work up front and worse forever: I would have been publishing my own shorthand, and every internal note would have needed redacting by hand. Instead the public entries are a curated data file — a handful of things worth telling someone about, in plain language — and the internal changelog stays exactly what it is. The two never have to agree on tone because they are not the same document.
Search, category and tag filtering, and sorting are plain functions over arrays in lib/updates/query.ts. The two pages hold the controls' state and render whatever the functions return. That split is the whole reason the feed's behaviour has tests without a browser: I can assert that a search over title, summary, tags and body is case-insensitive, and that the newest/oldest sort is stable for entries sharing a date, without rendering a single component.
The board is honest about what it is. There is no backend behind it: a visitor's suggestions and upvotes live in their own browser, layered on top of the seed tickets by lib/updates/ticketStore.ts. The store keeps two keys — the tickets you submitted and the ids you upvoted — and recomputes the merged list on every read, so the seeds are never copied into storage and can change under a returning visitor without a conflict. The page says this in as many words rather than implying a shared board that does not exist.
Shaping it this way was also a bet on the future: the store takes itsStorage as an argument, so swapping the browser for a real API later is a change in one file, not a rewrite.
The point of putting the two next to each other is the link between them. A shipped ticket names the update that closed it, and that update lists the ticket back. Getting those two halves out of sync would be the easy failure, so a data test walks every cross-link in both directions and fails the build if a shipped ticket points at an entry that does not list it — or at no entry at all.