Skip to content
Development

Building locale-aware post scanning for blog-manager

By Victor Da Luz
railsrubyi18ndev-logblog-manager

I run a Rails app called blog-manager that scans my two Astro sites for new posts, imports them, and syndicates them to Medium and Dev.to. It’s been flat this whole time: every post lives directly in src/content/blog/<slug>.md, one file per post, no subdirectories.

That’s about to change. I want Spanish translations of my posts, and the plan that came out of a spike a few days ago settled on a git-side approach: English canonical at src/content/blog/en/<slug>.md, Spanish sibling at es/<slug>.md, written in the same commit so drift shows up in diffs instead of hiding in a database somewhere. blog-manager and syndication stay English-only forever, they just need to know where to look once posts move into en/.

This issue was the prerequisite work: teach the scanner to find posts in the new layout, teach the publishing skills to write there, and build a CI check that catches a missing translation. None of the actual site migration happens here, that’s a separate, larger piece of work. This is just making sure blog-manager doesn’t break when it does.

The scanner change

The core of blog-manager’s scanner is a GitHub Contents API call: list the files in src/content/blog/, diff against what’s in the database, upsert what changed, soft-delete what disappeared. Simple, and it’s worked fine for a year.

My first pass at locale-awareness was: probe src/content/blog/en/ first. If it exists and has markdown files, scan that instead of the flat directory. Otherwise fall back to flat, unchanged. Two failure modes I’d already thought through: a missing en/ directory raises a 404 from GitHub, which I had to catch instead of letting it propagate (my job’s discard_on handler treats an uncaught 404 as a broken blog and marks it failed), and an en/ directory that exists but is still empty needed the same fallback, or a half-finished migration would look like every post got deleted.

I wrote tests for both. Everything passed. I opened the PR.

What code review caught

I run a multi-angle review before merging anything into this repo, eight independent passes over the diff looking for correctness bugs, redundant code, and convention violations. Four of them, working independently, converged on the same finding: my “prefer en/, fall back to flat” logic assumed migration was all-or-nothing. The moment en/ had even one file in it, I stopped looking at the flat directory entirely.

That’s wrong for the actual plan. My archive of 166 posts isn’t going to move into en/ all at once, that’s a lot of files to translate and I’d rather do it gradually, maybe starting with new posts only and backfilling the old ones later if I bother at all. Which means for a while, en/ will have some posts and the flat directory will have others, at the same time, on purpose. My scanner would have quietly soft-deleted every post still sitting in the flat directory the first time I moved even a single post into en/. Not permanent data loss, since posts un-delete themselves if their file reappears, but they’d have vanished from my dashboard and dropped out of syndication until I finished a migration I might take months to do.

The fix was to stop treating a non-empty en/ as fully authoritative and instead union it with the flat listing, en/ wins if a slug shows up in both, but nothing already migrated to flat gets dropped just because I started moving files around. It’s a small code change. Finding it wasn’t small, it took four different review angles independently landing on the same bug before I trusted it enough to rewrite the core logic and the tests around it.

Two of those four review passes also caught something I’d missed for a very different reason: I’d written new code comments with em dashes, which violates a formatting rule I have for literally everything I write, including code. Caught by the same automated pass, in the same review, right next to the actual correctness bug. Different severity, same lesson: a review pass doesn’t know which finding matters more until you look.

A bug I didn’t write

While I was wiring the CI check into my main blog’s pre-commit config, a completely unrelated agent session was live in the same working directory, shipping an RSS feed feature. Neither of us was using a git worktree, that project’s convention is “work directly on main, no parallel branches,” which is safe as long as it’s actually true that only one thing touches the repo at a time.

It wasn’t, this once. I’d added one line to package.json, an npm script pointing at my new translation-check script, and left it uncommitted for a few minutes while I tested things. The other session committed its own changes with a plain git add and swept my one line in with it. Nothing broke, the line is correct and it works, but now git log -- package.json attributes it to a commit about an RSS feed that has nothing to do with translations. I didn’t rewrite the merged commit, that felt like more risk than a mislabeled line justified. I wrote it down instead, so next time I catch it before it happens instead of after.

What’s next

The scanner and the publishing skills are done and merged. The actual migration, moving my archive into en/, standing up Astro’s i18n routing, and shipping real Spanish translations, is separate work, starting with my smaller site as a pilot before I touch the bigger one.

Related reading

Development

Translation mode for blog-manager

A split-view editor where translations share metadata but own their prose, a two-sha staleness model, and a formatting bug I copied from my own earlier code.

Read