Five Sites, One Engine

We set out to build a CMS and spent two days deleting code instead. The deletions paid better.

The obelisk in Ripon market square
Photo: Ripon Obelisk by Sandy Gerrard, via Wikimedia Commons, CC BY-SA 2.0 (cropped)

The plan was a content management system. A small, sharp one: no database to babysit, no plugin bazaar, content living in plain files, every edit recorded as a commit so the history of a site reads like the history of anything else worth keeping. I have wanted this for years, and I sat down to start it.

First, out of habit, I looked under the hood of the sites I already run. I maintain five of them, built over several years for myself and for clients. Each one carries an engine I wrote: the machinery that turns a page's metadata into search listings, social cards, sitemaps, feeds, and the structured data that tells Google what a page actually is.

I found the engine written five times. Not copied five times. Written five times, each a little different. The newest site had the most advanced version of some parts. An older site had the most complete version of others. Two sites had almost none of it. Nobody decides to do this. It happens one reasonable copy at a time, the way sediment happens.

The CMS could wait. The real first job was staring back at me.

The requirement behind the requirement

A content management system writes content into something. If the something exists in five divergent versions, the CMS inherits the divergence and multiplies it. Every fix becomes five fixes. Every client site becomes another fork. I would be building the exact mess I was trying to escape, just with my name on it instead of a vendor's.

So the requirement changed. Before any CMS: one engine, extracted from the five copies, taking the best version of each part, living in one place, consumed by every site as a versioned dependency. The unglamorous move. The one with no demo at the end of the first day.

One rule held the whole thing together

Extraction is not improvement.

That sentence did more work than any code I wrote. It means: port the best existing version of each part exactly as it is. Resist every "while I'm in here." If something deserves improving, improve it later, as its own deliberate, versioned change that can be inspected on its own. The moment extraction and improvement mix, you can no longer tell which change broke what, and a consolidation quietly becomes a rewrite that never ships.

The rule sounds easy. It is not. Two days of work is two days of noticing things you could make better and writing them on a list instead of touching them.

Photograph everything

Here is the part I would teach anyone, because it requires no special tools and changes how safe a migration can be.

Before touching a site, I built it and photographed it. Not screenshots: bytes. Every page, the sitemap, the feeds, the robots file, saved exactly as the server produced them. Then I swapped the site's engine for the new shared one, built again, and photographed again. Then I compared, byte for byte.

If the comparison comes back empty, the migration is provably safe. Not "looks right to me" safe. Provably. When it does not come back empty, the diff points at exactly what changed, and you decide on purpose whether that change is acceptable. One site came back identical. Another came back identical except for one page, and the diff showed precisely why, in thirty seconds, with no detective work.

Most of the dread in changing software is not knowing what you broke. You can replace that dread with a file comparison.

What fell out

This is the part I did not plan, and it is the reason this post exists.

The moment the engine became one named thing with its own test suite and its own standards, problems that had been invisible for years surfaced in days. Documentation that lied about its own contracts, caught by a type checker within the first hour of turning it on. An error path that could hang a request forever, found because a verification run froze instead of finishing. A build step that would have stamped the wrong canonical address into every page of a site, caught by the photograph-and-compare harness before a single visitor ever saw it.

None of these problems were new. They were old. They had been sitting in those five copies the whole time, beneath notice, because no single copy was worth instrumenting properly. The value was always there. It was just trapped in the duplication, and consolidation is what let it out.

The sites themselves came out ahead too. The two that had almost no engine gained the whole thing in an afternoon each: structured data, social cards, dynamic sitemaps, breadcrumbs for the deep pages. Hundreds of lines of duplicated machinery left each codebase. And the engine that remained is tested, type-checked, and released with one command.

The bonus nobody ordered

With one engine under every site, a feature I had only planned for someday took two hours: rendering every page once at build time and shipping it as a plain file, with the live renderer standing behind it as a fallback for anything not prerendered.

Static files are fast. Measured on the same machine, same pages, twenty requests each: the static version answered in roughly half the time, before production load makes the gap wider, because a file does not compete for the processor the way a renderer does. Each site chooses this per build, and a page without a static file simply falls back to live rendering. It can degrade. It cannot break.

All three sites are deployed this way now. They feel quicker because they are.

What it cost

Honesty requires this section. The CMS that started all of this still does not exist. Two days went into work that produces no screenshot, no feature, nothing a client would ever applaud in a demo. And the discipline was genuinely the hard part; the list of improvements I wanted to make and did not is longer than the list of changes I shipped.

I would spend the two days again without hesitating. The CMS did not get postponed. It got cheaper. It now has one engine to write into, content that already lives as structured, editable data, and a verification harness that can prove a change is safe before anyone sees it. The mountain did not shrink, but most of it turned out to be already climbed.

If you own a website

You may never extract an engine, and you should not have to. But the properties that made this migration provable are properties you can ask for by name, whoever builds your site: files you own rather than rent. A history of every change. Verification you can see, instead of reassurance. A stack a different developer could take over tomorrow without a ransom note.

If a vendor cannot offer those, the problem is not their talent. It is their foundation.

Before you go

Whether or not any of this is your world, here is a question worth taking with you. Ask it of your own project, or your own vendor, some quiet Monday:

Where do we have two copies of the same thing slowly disagreeing with each other?

The answer is where your next unplanned week is hiding. It is also, in my experience, where the treasure is buried.

One more thing, about why this post exists at all. I built everything described here alongside Anthropic's Fable, the AI I paired with, and when the third site went live it told me our progress deserved a milestone. I reflected on what two days had actually produced and decided a milestone was too small. How about an obelisk? So this post is the obelisk. The work is memorialized where it will last, published by the very engine it describes.

If you want a web presence built this way, files you own, changes you can audit, speed you can measure, that is the way Aeris Labs builds. No rush. The foundation is built to be here a long time.