Skip to content
Development

The same button choice cost me a bigger bug than expected

By Victor Da Luz
railsrubydev-logblog-manager

The next piece of the editor cluster: embed the existing hero image search/select/remove/commit flow inside the post editor, so assigning a hero doesn’t require leaving the page. It ended up teaching me more about verifying assumptions against real data than about the feature itself.

The button question, again

Same shape of decision as the commit flow: should the hero commit go through the same main “Commit” button as the rest of the draft, or stay its own dedicated action? I settled it deliberately before building, the same way the sync-vs-async question got settled: a dedicated hero button, keeping it separate from the draft’s metadata/body commit path.

What that choice didn’t dodge

I initially framed the dedicated-button option as the smaller, safer one. It wasn’t, in the one place that actually mattered. The existing hero committer only ever inserted a fresh heroImage key into a file that had none, hard-failing if one already existed. Making replace actually work - the whole point of closing the staged-vs-live hero gap - meant locating and swapping that key wherever it sits in the file, not just appending.

Before writing any view code, I checked what real production frontmatter actually looks like. All 166 live vdaluz.com posts already carry a heroImage key, set out-of-band by vdaluz.com’s own hero-setting script, sitting at its natural position in the file. None of them had ever gone through the old insertion-only path successfully, because that path can only run once per post. A positional or regex-based “strip the last thing we appended” approach, which is what I’d started sketching, would have silently produced duplicate keys against every single one of those 166 posts. I built a proper AST-based upsert in the existing frontmatter service instead, and validated it against all 166 real files (not a sample) before touching the UI: exactly one heroImage key survives a replace, every other field untouched, same validation discipline the frontmatter service used when it was first built.

The bug that almost shipped

The multi-angle review before merge caught something the empirical check above didn’t: a new hero_committed_at column tracks whether a staged pick has actually been pushed live, and I added it with a plain add_column, no backfill. Three independent review passes converged on the same line: every post that already had a hero committed through this app before the migration deploys has a staged pick present, a live hero present, and this new column permanently NULL, because the staged marker is never cleared on success (it can’t be, something else reads it directly). Every one of those posts would have shown a stale “Commit to live article” button instead of “Applied”, and clicking it would have fired a real, pointless GitHub commit. Nothing in the test suite caught it, because fresh test fixtures never have pre-existing data to get this wrong against. Fixed with a one-line backfill, the same shape this repo already had precedent for in an earlier migration I hadn’t cross-checked against.

What I cleaned up after

The pending-vs-applied display logic (committing / pending / live / none) ended up duplicated across three views once the editor panel existed alongside the two older hero displays. Review flagged it as more than “three similar lines” territory, since it was a multi-branch conditional plus an explanatory comment, copy-pasted three times. Pulled it into one Post#hero_display_state method instead.

What I didn’t verify

Same gap as the commit flow: no write-capable GitHub token in dev, so the actual live commit/replace only ran against a fake client in tests. Browser verification did confirm the part that mattered most here though: staging a new image over an already-live hero correctly showed the new photographer’s name and a commit button, not stale “Applied” text, for a real post in a real session.

Related reading

Development

The editor commit button is a deploy button

Committing a draft to main auto-deploys the blog. Once that clicked, sync-vs-async stopped being a style question - plus the legacy-affiliate carve-out a new validator almost broke.

Read