blog

Why I built a blog platform in 2026

It's 2026, I wanted a blog, and instead of reaching for any of the dozens of things that would have given me one in an afternoon, I built my own engine. Not a theme or a plugin — a serverless platform on AWS with its own admin app, its own scheduling, its own analytics pipeline.…

It’s 2026, I wanted a blog, and instead of reaching for any of the dozens of things that would have given me one in an afternoon, I built my own engine. Not a theme or a plugin — a serverless platform on AWS with its own admin app, its own scheduling, its own analytics pipeline. The common wisdom says that’s a stupid thing to do, and for most people it’s right. Owning your own blog typically came in two flavours. You could run something WordPress-shaped — a web editor, an API, scheduling, all the conveniences — and either pay for a server that’s awake whether anyone’s reading or not, or let a cheap host run it for you and pay some other way: ads on your pages, throttled traffic, a plan tier that exists to be upgraded from. Or you could go static: near-free hosting, but publishing means a build step and a git commit, and every convenience you want is one you bolt on yourself. Cheap hosting isn’t new; CloudFront in front of S3 has been the sensible default for a decade. What’s changed is that the first flavour doesn’t have to mean a server any more. That’s not a discovery — it’s not common, but I’m certainly not the first to do it — it’s just that this time I decided it was the right call for me. I get the editor, the API and the scheduling on a flat fifteen US dollars a month with a hard spend ceiling, which means a post going properly viral costs exactly the same as a Tuesday with three readers.

I built it because I want the infrastructure and the reader data under my own control. That one’s a principle, not a feature. Every time I read a privacy policy with the line about sharing your data with partners “to support our business” or whatever, I cringe a bit, and I’m not going to be on the other side of that sentence — even for a blog, even though the most exciting thing I could collect about you is which post you read on a Tuesday. If someone reads something here, what happens to that fact should be my decision, and the decision is “it stays in my account”. The second reason is that I’d got used to the conveniences of WordPress-class tools — a real editor, scheduled publishing, an API to build against — and wanted them back without the server they usually come with. And, if I’m honest, the third is that I just really wanted to build it. Engineers be engineering. There were easier ways to get a blog online, and I’m not going to pretend otherwise.

It’s also not the first time I’ve built a version of this. About four years ago I put one together as a customer demo tool while I was at Amazon — never intended to go live, just a way to show a handful of services fitting together in a real pipeline — and the architecture has been kicking around my head ever since. Deciding I actually had something to write was a separate thing, and [[Why write a blog in 2026]] is that story. This one’s about the engine.

“But Brian, why didn’t you use [insert product here]?”

Ghost, Hugo, Substack. They’re three of dozens, but between them they cover the common patterns — a managed CMS, a static generator, a hosted newsletter — and all three are better choices than mine for most people. They deserve a proper argument before I knock them over.

Ghost is good software. The editor is the best in its class, memberships and newsletters are built in, and you can self-host it if you don’t want to rent. But it’s a Node app that has to be running. Self-host it and you own a server, a patching cadence and a backup story. I’ve done that before, and my setup wasn’t even bad — patching was mostly automated, backups worked. What got me was the drip of small things: a disk filling up, a service that didn’t come back after a reboot, a cert renewal that silently failed. None of it hard, all of it needing me to log in and go find out what was wrong, and it added up to time I’d rather have spent on nearly anything else. Babysitting servers is a shit hobby, and I’m reluctant to take it up again for almost anything. Take the managed tier instead and the cost is somebody else’s problem, but the price is set by the size of your list and only ever moves up. Neither is outrageous. Both are the price of persistent compute, and persistent compute is the thing I was trying to design out.

Hugo — or Eleventy, or Astro on its own — dropped onto S3 or Pages is the closest competitor, and the honest answer is that it would have worked. Static output, hosting costs that round to nothing, git as the CMS, live in a weekend. It’s good enough, and for a long time good enough was fine. What it doesn’t give me is an API surface. I wanted the content pipeline to be able to take writes from more than one client: the admin app I use from a phone on the couch now, and at some point a sync straight out of my notes app, which is a soon-not-now feature I wanted to leave room for rather than a requirement. With a static generator, publishing is a git commit, so every client has to be a git client, and scheduling becomes a cron job committing on your behalf. That’s a workable shape. It’s just less than I wanted.

Substack is fine if the audience is the product. If you’re building a list and eventually charging for it, the distribution machinery is worth the tax. But the lock-in isn’t the email addresses — you can export those. It’s that Substack sits between you and every reader, owns the discovery, and the recommendation network only works while you’re inside it. That’s not a flaw in the design, it’s the business model working as intended. I’m not building a mailing list business. I’m writing things down.

Where all three are right: if you just want to write, every one of them ships you faster than I shipped myself.

Ownership is the reason, serverless is what made it affordable

The motive comes first, because the money only matters once you’ve decided you want the thing.

Control of the data cashes out in decisions a hosted platform will never make for you. My analytics are a first-party, cookieless beacon into my own pipeline: no third party, no cookies, no localStorage, no fingerprinting-adjacent identifiers, and therefore no consent banner, because there’s nothing to consent to. Reader browsers make no third-party requests at all. That costs me something, and I want to be straight about what. I don’t get what Google Analytics would give me — demographics, cross-session journeys, a picture of who a reader is rather than what they read. I get page views, referrers, and rough geography from the edge, and that’s the trade: I know what’s being read, I don’t know who by, and I’m fine with that. Images from link posts get fetched, re-encoded and cached into my own media bucket at build time rather than hotlinked, so nobody else’s server gets to watch my readers either. Accountability for all of it stops with me, and that’s the point. When I build things I want to be the one on the hook for them; it’s the only way I trust that the decisions were made properly.

Historically that ownership had a running cost. Your own server means a bill, a patch cycle, and capacity you’re either wasting or about to run out of. No persistent compute changes the shape of the bill entirely: static site on CloudFront, Lambda only where logic has to run, DynamoDB on-demand, and a CloudFront flat-rate plan at fifteen dollars a month with no overage charges at all. On the compute side, the API that fronts the Lambdas sits behind a CloudFront signed-cookie check, so almost nothing anonymous reaches a function — the analytics beacon and the token exchange are the deliberate exceptions, and both do as little as possible — and every function has reserved concurrency as a free hard ceiling on whatever does get through. The site serves HTML with stale-while-revalidate caching — the edge keeps handing out the last good copy while it fetches a fresh one in the background — so there are no invalidation charges either, and readers never wait on a build. Traffic spikes stop being a financial event, and they stop being an outage risk too, because there’s no pair of servers to get worked to death.

The fair counter is that you can put a CDN in front of a server and get a lot of that. You can, and for a blog it’s a genuinely good insurance policy — a blog is read-heavy by nature, so a CDN absorbs nearly every cost blow-out scenario you’d realistically face. What it doesn’t do is remove the origin. The server still exists, still needs patching and sizing, and still takes every hit for anything dynamic: the admin, the API, any page the cache doesn’t have. If you’re keeping a server anyway, front it with a CDN and you’ll be fine. I just didn’t want the server, and once you don’t have one, the CDN isn’t insurance any more — it’s the whole delivery path.

The shape of it

Enough of the architecture to orient you, not a tour.

Astro is the centre of it. It’s the static site generator, it’s TypeScript-native, and it emits zero JavaScript by default, so the public site is plain HTML and CSS unless a page has a reason to be otherwise. That’s worth more than a fast page load: with no inline scripts to allow, I can run a strict Content-Security-Policy that blocks them outright, which takes most of the easy ways of using a site to do something sketchy off the table. More importantly for the shape, an Astro build can pull content from wherever I tell it to, and that freedom is what makes the rest of the design possible.

Because the build isn’t tied to the filesystem as its only source, I can split the content the way the data wants to be split. DynamoDB owns everything queryable — slug, type, status, publish time, tags — and S3 owns the body. The DDB item stores the S3 object’s versionId and the build fetches bodies by that exact version, never latest, so revision history is a pointer, rollback is a pointer swap, and there’s no revision table to keep consistent. That split is also what gives me the API I wanted: queries against DDB for listing, filtering and scheduling, with the body fetched only when it’s needed.

Publishing flows from the same place. There’s one idempotent ingestContent() behind the API, and every client goes through it — write the body to S3, write the metadata to DDB, kick a build. The admin app is client one. The notes-based sync agent will be client two, on the identical path, with its own machine identity. Designing the ingest boundary for a client that didn’t exist yet cost me almost nothing at the time and removes the “bolt on a second write path” project that would otherwise be waiting for me.

The admin app is a small Astro-built SPA behind the same CloudFront distribution, sitting behind an identity provider I already run, with its own short-lived session so the UI and the API expire together. That’s the whole thing. It’s the flexibility of a WordPress-class platform — a proper editor, an API, scheduled publishing, room for a second client — without a server anywhere in it, and without the patching, capacity and blast-radius questions that come with one.

What it costs to own one

So that’s the calculus. For most people, “just use Ghost, or Hugo, or Substack” is still the right answer, and I’d give it to them. But the reason it used to be the only sensible answer was that owning the whole stack meant either giving up the conveniences or paying for a server to keep them. That trade-off is gone. A WordPress-class blog — editor, API, scheduling, analytics — with every byte of reader data in my own account, running for fifteen dollars a month with a ceiling it can’t go over, and nothing to patch. I wanted the ownership, I wanted the conveniences, and I wanted to build it. In 2026, that’s a fifteen-dollar decision rather than a server-shaped one, and there’ll be more posts about the interesting bits when the mood takes me.

#blog#aws#serverless