21 euros a month: why I moved my blog off WordPress

For years this blog ran on a classic WordPress hosting plan. The plan cost me 21 euros a month. Last weekend I moved the site to a static setup on GitHub Pages. The new bill is 4 US dollars a month, for GitHub Pro. That is roughly 200 euros a year saved, for a blog that gets a handful of posts a year.

This is the short version of why, what moved, and what nearly bit me.

From a WordPress stack at 21 euros a month to static files at 4 dollars a month Left: a WordPress site needs a browser, PHP, a database, a theme and plugins, several of them with pending updates, and costs 21 euros per month. Right: the new setup needs only Markdown files in Git published as static pages on a CDN, and costs 4 US dollars per month for GitHub Pro. BEFORE WordPress hosting Browser PHP ! Database Theme ! Plugins ! ! = update waiting 21 € per month AFTER GitHub Pages Markdown files in Git Static pages on a CDN nothing to patch 4 $ per month, GitHub Pro

Left: what I paid for and had to keep updated. Right: what I pay now.

The business case

  Before After
Hosting WordPress plan, 21 EUR/month GitHub Pages, included
Extra cost none GitHub Pro, 4 USD/month
Updates WordPress, theme, plugins: click through the dashboard a few gems, proposed automatically as pull requests
Attack surface PHP, database, login page, plugins static files
Backups host-dependent every post lives in Git

Why pay the 4 dollars at all? GitHub Pages is free for public repositories. I wanted my repository private, because it also holds drafts and notes, and Pages from a private repository needs a paid plan. Four dollars buys me that. And the same subscription has room for five more blogs that I can move over, which brings the cost to under a dollar per site.

What I give up: the admin screen. I now write posts as Markdown files and publish by merging a pull request. For me that is a feature, not a loss.

What moved

  • Content. I exported everything from WordPress as XML and converted it to Markdown with a small script. 21 posts and a few pages, plus two drafts that now sit in a _drafts folder where nobody can see them.
  • URLs. Every old address still works. The scheme /category/post-name/ is pinned per post, and a script checks every old URL against the new build. Result: all of them present, before anything went live.
  • Images. They lived in cloud storage that only WordPress could read. A CDN now sits in front of the storage, and the posts point to the CDN.
  • What I dropped. Comments, the newsletter form and a cookie banner. Without tracking cookies there was nothing left to ask consent for.

The cutover, anonymised

I changed the DNS records at the very end. Before and after, with example.com standing in for my domain and addresses from the ranges reserved for documentation:

BEFORE
example.com.      300  A      192.0.2.10
example.com.      300  AAAA   2001:db8::10
www.example.com.  300  A      192.0.2.10
www.example.com.  300  AAAA   2001:db8::10

AFTER
example.com.      300  A      <the four addresses GitHub documents>
example.com.      300  AAAA   <the four IPv6 addresses GitHub documents>
www.example.com.  300  CNAME  username.github.io.

My mail records (MX, SPF and the DKIM entries) stayed exactly as they were. I kept the TTL at 300 seconds during the move, so a rollback would have been a matter of minutes. I also kept the old server running until the new site had proven itself.

Less to click, less to attack

A WordPress site is a running application: PHP, a database, a theme and a handful of plugins. Each of them needs updates, and each update is a small chore. Worse, a security fix I have not clicked yet is a hole that stays open until I do.

A static site is only files. There is no login page to attack, no database to inject into, and no plugin that quietly stops being maintained. The update treadmill, with its dashboard badges and its “just one more plugin to refresh”, is gone.

That is not the same as zero risk, so here is what is left:

  • The build tools. A few Ruby gems and the GitHub Actions that build the site. A bot proposes their updates as pull requests, a build checks them, and I merge when it is green.
  • The account. Whoever gets into my GitHub account gets my sites. A passkey or two-factor login on that account matters more than any plugin ever did.

So the four dollars are not only a cost question. The same subscription has room for five more blogs, and each one that moves over loses its update chores and most of its attack surface.

What nearly bit me

  1. Where DNS really lives. The name servers listed inside my zone were not the ones the registry delegates to. Asking the registry directly saved me from editing the wrong place.
  2. The forgotten IPv6 record. Leave the old AAAA record in place and some visitors keep landing on the old server. Delete it or replace it.
  3. The base path. Before a custom domain is set, a project site lives under /repository-name/. One more build after setting the domain fixed my broken stylesheet links.
  4. A date in the future. I gave a new page a date a few hours ahead of the build. Jekyll skips such pages without any warning, and the page returned a 404 although the footer linked to it.
  5. Certificates take a while. After the DNS switch, GitHub needs between a few minutes and an hour to issue the certificate. Plan for that window, and pick a quiet time if the old site sent strict HTTPS headers.

The product lesson

Two things made this an easy decision.

It was cheap to reverse. TTL at five minutes, old server still online, every file in Git. When a decision costs almost nothing to undo, I do not need to be sure. I only need to be careful about the first hour.

The unit economics were embarrassing. I paid 21 euros a month to host something that, for the reader, is a handful of static pages. Boring technology that fits the actual load is a better deal than a platform built for much more than I use.

If your site is mostly text that rarely changes, run the numbers once. Mine said: stop paying for a database.