6 min read
  • workflows
  • AI

One blog system, four sites

Deep green card reading 'Four blogs, one brain.' in white serif, beneath a row of four coloured blocks joined by arrows

A quick word before the story. Like many developers these days, I work with an AI assistant in the loop, for building and increasingly for writing. This post is the first here on AI-assisted content systems, the plumbing rather than the predictions, and there will be more of them, because the experience keeps producing lessons worth writing down.

Every side project’s blog dies after post two. Not because writing is hard, but because nothing forces the next post to exist. I had four of these graveyards running at once, so I spent an afternoon building one system that writes for all four, and the writing turned out to be the easy part.

Here is the state I started from. This site had a blog that was alive but slow, last post March. ClusterPilot had a blog with literally zero posts in it. Fieldnotes had exactly one. Sojihome, my home-care app, had six posts from a launch burst and then silence. Four sites, four different flavours of the same failure.

The target was simple: one post per site per week. For the three product sites, organic search is the entire marketing plan, so a dead blog is not a cosmetic problem, it is the marketing not happening.

Discipline is not a plan

The obvious fix is to try harder. Block out Friday afternoons, keep a streak, feel guilty when it slips. I have run that experiment enough times to know how it ends. Discipline-based cadences do not survive contact with a PhD. The week the simulation breaks is exactly the week the blog post does not get written, and that is most weeks.

So the honest goal was not to write more. It was to make output a byproduct of work I was already doing, so the cadence would survive a busy week without any willpower being spent on it.

That reframing changed what I built. Not a writing habit, a routing system.

The system only knew about one site

My content setup already existed, but it had one hard assumption baked in everywhere: there was one website. The command that drafts a post was hardcoded to this site. The planning file that tracks what is due had no concept of which site a post belonged to. The fourth site did not appear anywhere at all. I had been treating “my blog” as a singular noun.

The first real piece of work was a site registry: one file that is the single source of truth for each site’s audience, voice, post length, frontmatter schema, category list, SEO checklist and publish mechanics. Every command and skill now reads that registry instead of assuming a site.

The detail I am most pleased with is the voice override. Three of the four sites share my normal writing voice. Sojihome does not: it is calm and gentle and never salesy, a completely different register. Rather than fork the whole pipeline, the registry carries a per-site voice override. The same drafting machinery writes a technical Linux post and a quiet essay about seasonal cleaning, and reads the target voice from one field.

The second piece was a cadence table in the planning file: site, last published, next due. The weekly planning command reads it. The rule I gave it matters more than the table does. A light week has to name which sites slip, out loud, rather than silently dropping them. A visible miss is a decision you can look at. A quiet failure is how you end up back at six posts and silence.

Then I looked at the raw HTML

This is the part I did not see coming. Two of the four sites are React single page apps, and their blog posts lived as JavaScript objects with HTML strings inside them. I opened the rendered page source to check something unrelated and realised what that meant.

No per-post title in the HTML. No meta description. The post URLs were missing from the sitemap entirely: one sitemap listed only the homepage. No RSS. Every post published to those two sites was, as far as a search engine was concerned, close to invisible. The exact channel the weekly cadence was supposed to feed was a channel Google could barely see.

That is a bracing thing to discover halfway through building a system whose whole justification is organic search.

The fix did not need a framework migration or a redesign. I moved the blog and docs content into markdown files, then added a small build step that prerenders the existing React components to static HTML, one file per post URL, with real title and meta tags in the head. Same components, rendered ahead of time instead of only in the browser. The build also emits a proper sitemap and an RSS feed.

The tools involved were unremarkable, which is the point. Vite already drove the build, react-router already knew every post URL so I could walk its routes, marked turned the markdown into HTML, and sharp generated the image cards. Nothing exotic, just wired together to run at build time.

The before-and-after is the satisfying bit. One site went from a homepage-only sitemap to 12 URLs. Both went from zero per-post meta tags to full head data. That is the difference between publishing into a channel and publishing into a void.

The catch the checklist caught

I nearly shipped a subtler bug. After deploying, the SEO checklist said to curl the live pages, not just check the local build. I almost skipped it, because the local build was clearly fine.

The live site 301-redirected the slash-less post URLs to their trailing-slash form. My canonical tags pointed at the slash-less version. So every canonical tag pointed at a redirect rather than at the page itself, which is exactly the sort of thing a crawler treats as a small act of hostility. Fixed the same hour, but I would never have found it from the local build. The lesson stuck: verify against the thing that is actually live, not the thing on your laptop.

Firing it mid-task

The last piece is the one that makes the cadence self-sustaining. There is now a trigger I can fire from inside any of the four repos, mid-task. A background agent picks up a short sanitised brief I write in passing, drafts the post for the correct site reading that site’s registry entry, generates a featured image in that site’s palette, and hands back a draft. The work I was already doing carries on without waiting.

Publishing stays a human decision. Nothing is committed until I read the draft and give an explicit go. The system removes the friction of starting a post, not the judgement of shipping one.

Which brings me to the honest disclosure. This post was drafted by that system. I fired the trigger partway through an unrelated session, kept working, and came back to a draft and a thumbnail waiting for review. If you want to know whether the thing works, that is the demonstration: you are reading its output.

What actually failed, and what to check

If there is one idea to take from this, it is that content systems fail at the seams between tools, not inside them. The writing was never the bottleneck. Routing to the right site, tracking cadence, and the grubby publish mechanics were. I had quietly assumed the hard part was coming up with words, even with AI assistance on hand.

Two concrete things to check on your own projects. If your blog is a JavaScript-rendered single page app, open a post’s raw HTML and look at your sitemap before you write another word. You may be filing beautifully into a drawer nobody opens. And whatever your setup, make the next post a byproduct of work already happening rather than a separate act of will, because the discipline version does not survive a bad week.

I write about this kind of workflow plumbing fairly often. If extracting shared logic into one source of truth is your thing, centralising your plot style into one shared theme file is the same instinct pointed at matplotlib instead of blogs.

← Back to all posts