actuallyfrank blog
writing in publicno analytics on this pagerss still works
All posts
changelog

The homepage says what shipped

the-homepage-says-what-shipped

The most prominent card on actuallyfrank.io shipped four literal TODO: strings to every visitor:

export const CURRENTLY: [string, string][] = [
  ["Building", "TODO: what you're building"],
  ["Fixing", "TODO: what's broken"],
  ["Reading", "TODO: what you're reading"],
  ["Avoiding", "TODO: what you're avoiding"],
];

Unfinished, in public, for weeks.

The obvious fix is to write four sentences. The problem is that "Currently" implies work in progress, and three of those four rows have no honest automatic source — no issues to read, no reading API since Goodreads closed theirs. A panel that only ever says impressive things is a panel nobody believes, and one that has said the same thing for three weeks is worse.

What this project does produce reliably is finished work. Every change is planned, archived on completion, and archiving is already a commit, which is already a deploy. The update mechanism existed. Nothing read it.

Meanwhile the page below already claimed the blog is the changelog. It wasn't one. So the fix was to make that sentence true first.

How it works

A post can now declare itself a changelog entry:

kind: "changelog"
change: "2026-08-09-add-changelog-and-ship-log"

Both fields are optional and both degrade. A post with no kind is an article, which is why the nine posts that predate this needed no edits at all. An unrecognised value degrades the same way — a typo costs a label, not a post.

pnpm changelog:gen then reads the published changelog posts and writes a committed file the apex imports. The apex and the blog are separate deployments that can't read each other's files, so a committed file is what carries the data across. A build-time fetch would have coupled two independent deploys into an ordering dependency, and the failure mode is silent: blog deploys second, the homepage builds stale, nothing complains.

The part I care about most is that the generator runs the blog's own reader rather than parsing frontmatter a second time. Two implementations of "is this post published" is precisely how a draft ends up announced on the homepage while 404ing on the blog.

The thing that makes it stick

None of the above would survive contact with a busy week without this: a test regenerates the list in memory and fails when the committed one differs.

apps/www/src/changelog.generated.ts is stale. Run: pnpm changelog:gen

It's a test rather than a script because tests run in the commit hook and scripts run when you remember. There is already a check in this repo that verifies generated database types, and it is correct, and nothing runs it. That was the example to avoid, not to follow.

What it doesn't do

Nothing polls anything. There is no live status, no database, no cron. The only thing the homepage learns automatically is what a published post already says — and it shows plain dates rather than relative ages, because "shipped 24 days ago" is an accusation and 09 AUG 2026 is a log.

Writing the post is still manual, and deliberately so. A generated summary of my own work would read like a robot, on the one page where that matters most.

The real cost is that finishing something now also means writing about it. If the prose isn't ready, draft: true makes the post invisible everywhere until it is — so archiving is never blocked on finding the words.

This post is the first entry the card generated from itself.