Dev.to AI 🤖 Ai 👁 0 📖 17 min read

Cloudflare Built EmDash. I Built NeighDash.

Hello World, Goodbye PHP The first post on this blog is dated 1999 and says, in its entirety, <?php echo "Hello World" ?>. This month, twenty seven years later, the blog finally stopped running PHP. That is a very long

Cloudflare Built EmDash. I Built NeighDash.

Hello World, Goodbye PHP

The first post on this blog is dated 1999 and says, in its entirety, <?php echo "Hello World" ?>. This month, twenty seven years later, the blog finally stopped running PHP. That is a very long hello (and an even longer goodbye). In 2004 WordPress made my life as a "blogger" a lot easier, and it carried this blog for more than twenty years, so this isn't a breakup post (okay, it's a little bit of a breakup post).

The Stable Wasn't Stable

All my sites lived on one Namecheap shared server called premium52. This blog, The Sunrise, Smart Keys, Fireflow, teatime.london and three smaller ones, all sharing one machine with who knows how many strangers. For years that was fine. This summer TLS handshakes started taking 20 to 30 seconds, and every now and then the whole box went dark for an hour, taking every one of my domains down at the same time. By the time I started, Fireflow was answering HTTP 520 to anyone who knocked.

The satisfying part was proving it wasn't my fault (a rare feeling, I recommend it). From an SSH session on the server itself, fetching a static robots.txt from its own IP still took 20 to 30 seconds to shake hands, while my account's CPU and memory usage sat at roughly zero. The incidents in UptimeRobot lined up to the minute with the traffic collapsing in the access logs.

UptimeRobot's latest incidents list, ten resolved incidents between August 4 and August 13, 2026, all with the root cause

"Keyword has not been found" is UptimeRobot's polite way of saying Cloudflare showed an error page where my site should be.

A static site on Cloudflare has no origin server to fall over. So the fix wasn't tuning the server, it was not having one anymore.

One request to the old server and one to the new one, side by side

On diego.horse this is a little toy you can click: it sends the same request to the old host and to the new one, with the delays we measured.

The timings on both sides are ones we actually measured. The odds of getting a good day, a slow handshake or an error page on the old server are my guess (a generous one, looking at that UptimeRobot list).

WordPress, We Need to Talk

To be fair to premium52, it only gave me the final push. WordPress and I had been drifting apart for a while.

Animated GIF of a chestnut horse galloping across a field with only its front half, the back half simply missing.

Me, halfway out of WordPress, galloping anyway.

My inbox had a steady drip of Wordfence emails about someone, somewhere, trying to break into my WordPress. Getting regular reminders that strangers are rattling the door handle of a personal blog does wonders for nobody's nervous system.

Then there were the plugin updates. I got burned badly by automatic updates in the past, so I turned them off and started updating by hand, reading every changelog first like a terms of service I actually intended to honor. I don't miss a single one of those updates I was too scared to click. That's a lot of babysitting for sites I update once a month, twice on a busy one. None of them really needed a CMS (they needed a place to keep the files and someone to hand them out).

WordPress also brought a lot of friends to every page, scripts and markup I never asked for, and changing the layout usually meant fighting the theme with !important until one of us gave up. Now the pages carry (mostly) only what they need, and when I want something to look different I just change it.

And then there's the drama around WordPress itself, which I got tired of following like a novela I never signed up for. Its co-founder Matt Mullenweg runs Automattic, the company behind WordPress.com, which has been in court with the hosting company WP Engine since 2024. This month Automattic's board tried to put him on leave, and he used his 84% of the vote to undo it in about 33 hours. Then he rebuilt the board, with two of the new seats going to co-founders of IRL, the social app that shut down after it turned out nearly all of its users were bots. I don't have a horse in that race (okay, I have exactly one horse, and it's this blog), but I'd rather my sites not depend on how the next episode goes.

Money was never really the point, shared hosting is cheap. But it was another service to keep an eye on, plus a support center I'd have to chase whenever things stopped working, and I'm happy to have one less of both.

Meanwhile, at Cloudflare

In the same month, Cloudflare moved its own blog to a new CMS running on Workers. They call it being Customer Zero, running your own products before you ask anyone else to. They load tested it with k6 up to 7,000 requests per second, rolled it out through a proxy Worker at 1%, then 5%, then 15%, then 100% in a single day, and later absorbed a 28,000 requests per second DDoS during Agents Week without blinking (InfoQ has a nice summary).

The CMS they built is called EmDash, which feels a bit personal since the style guide for this blog completely bans the em dash. As an ESL learner, I never understood the em dash anyway.

My version of Customer Zero is Customer Zero Dollars, the Workers free plan. My traffic peaks at a few requests per minute on a very good day, so my load test was opening the homepage, and my gradual rollout was deleting two DNS records. On smartkeys.so the script backed them up, deleted them and attached both domains to the new Worker in 1.3 seconds, ready to put the old records back if anything failed. There was no 1% cohort and no cookie routing, everyone got the new site at once, including me, who is honestly most of the audience.

I'll be honest, I think my approach is better (for me, and I'm very biased). EmDash is a proper CMS, with an admin, scheduled posts and a database sitting behind two layers of cache, which makes total sense when a big team publishes to a blog read by millions. My sites have one writer who publishes about once a week, so there's nothing to put a cache in front of, because there's nothing behind it. Every page is rendered before anyone asks for it, and most visits never even wake up the small Worker that takes care of old links (more on that Headless Horseman below). I'm calling it NeighDash, a Stable Site Generator. (For my ESL friends, neigh sounds like nay), nay dashes, nay CMS, nay dashboard, nay logins, nay database, nay headaches, and it's stable in a way my server and wordpresses never were.

A 1921 German emergency banknote printed in black, orange and green, showing the silhouette of a headless rider slumped on a standing horse under an arch, framed by two armoured figures holding coats of arms, with the number 25 in both top corners.

Meet the Headless Horseman, NeighDash's only moving part. He rides out when an old link calls, and the rest of the time he just stands there looking dramatic. A 25 Pfennig note from Berga an der Elster, 1921, via Wikimedia Commons (public domain).

It isn't without trade-offs. Fixing a typo now means making a git commit. Still, having multiple people update content is like a dev team working on the same project, you get backups, change logs, and version control for free.

Most of what I expected to miss has a static answer, by the way. Comments can live in GitHub Discussions or a tiny Worker, and scheduled posts are just a daily build that publishes whatever is due (the robot read the docs, both fit in the free plan, neither is built yet).

The real trade is ownership, and it's the part the "just go static" crowd never mentions. WordPress's superpower was never the editor, it's the plugin ecosystem, where someone else builds, maintains and patches every feature for you. Here, every dynamic thing I add becomes code I own, with its own spam and security updates to worry about. The owners of the smaller sites I moved can't edit them on their own anymore either, so in practice I'm their CMS now (a slow one, with opinions). For sites that mostly exist to be read, that's a great deal. The day I want logins or a shop, I'm building an app, and that's a different conversation.

The robot did the heavy lifting

I did the whole thing pair programming with Claude Code, the same way I built QWERTYS, except this took from August 4 to the end of September instead of a week. The agent worked out of a repo called cms-agent with a handoff document called CONTEXT.md, so any fresh session could pick up exactly where the last one stopped. It holds the mission, how to reach every server and API, the decisions that were locked, and a numbered list of hard won learnings that is now 29 items long. Reading it feels like reading the diary of a very organized intern who has been burned a few times.

Every site got its own private repo, plain HTML and CSS, deployed by a push to master. The small ones were rebuilt fresh as clean static pages. teatime.london was already static, so it was a straight lift and shift. One of them wasn't even WordPress, it was a Namecheap Website Builder site whose entire content lived inside a SQLite file on the server, so the agent rebuilt the pages straight from the database (a website stored in a database file, stored on a server that kept disappearing, what could go wrong).

The big two got more care. The Sunrise has 136 pages and this blog has 251 posts. Both went from WordPress to Markdown files and a small Python build script, with Pagefind doing search in the browser. The uploads folder here was 901 MB, but only 341 files were actually used by a post, and they are now 141 MB of WebP. Every old URL still works, including tags, date archives, ?p= shortlinks, the feed and even the old /wp-content/uploads/ image addresses, because Google Images and itch.io still link to them.

So this post is a text file in a git repo. Publishing it means deleting draft: true and pushing.

I was already writing pretty much everything else in Markdown, so leaving the WordPress editor for .md files felt more like coming home. It turned out even better than I expected, because now I can ask Claude to review a draft or change a few things, and every edit shows up as a diff that I approve or throw away in git (yes, including this post, which is a bit meta).

And the agent made itself mostly unnecessary, which is my favorite kind of employee. It left behind a script that pre-renders the whole site and optimizes every image, and each repo is connected to Cloudflare Workers, so a push to master builds and deploys on its own. In theory I don't need the agent anymore (in practice I'll keep bothering it, old habits).

Here's how the pieces fit together for this blog, the biggest of the eight.

How NeighDash runs this blog

No database and no server of my own, and nothing to update at 2 a.m. The only moving part at request time is the small Worker.

Two months, Diego. To move websites that were already online.

– The responsible adult who lives in my head

In my defense, it took two months because it was never the priority. It happened in the gaps between other things, and I wanted to be present for it, reading the decisions the agent made instead of waking up one morning to eight websites I no longer understood.

Things we got wrong along the way

The agent is very good, and also very capable of doing something dumb at full speed. A few highlights from the learnings list.

Roughly how it felt when the first deploy published the .git folder. From The Virginian, via GRIT.

Roughly how it felt when the first deploy published the .git folder. From *The Virginian, via GRIT.*

  • The first deploy published the .git folder to the internet. Workers static assets serve the whole folder you point them at, repo history included. It was caught on the first site and every template now ignores it.
  • A compiled Python file went live on one site. A __pycache__/check.cpython-312.pyc sat there, publicly downloadable, with the local path of my Mac baked into it. A MIGRATION-REPORT.md was one merge away from the same fate. Now every repo ignores whole classes of files instead of a list of names.
  • It merged to master twice without asking. On these sites a push to master is a deploy, so it's supposed to be a decision, not a side effect. The second time was right after I had explained why not to. It now has a standing rule in its memory, work on dev and ask before anything goes live.
  • The biggest performance drag wasn't WordPress, it was Cloudflare. On Fireflow, Bot Fight Mode was injecting a challenge script that cost 1,309 ms of main thread work, about six times what Google Analytics cost. That was my own little Customer Zero moment. And switching it off in the dashboard leaves the JavaScript part on, so you have to check twice.
  • The cache lies, in both directions. After one deploy, five identical requests came back old, new, new, old, old. A single curl will happily tell you the deploy worked while most visitors still get yesterday's page.
  • The Worker was spending the whole account's free budget on images. Cloudflare's free plan gives an account 100,000 Worker requests a day, shared by every site, while plain files are free and unlimited. My setup sent every image and stylesheet through the Worker, about ten requests per page view, so two small sites used 31,000 on an ordinary day. A post like this one getting shared could have taken them all offline until midnight UTC. Now the Worker only wakes up for the few old links that need it (the Headless Horseman, again).

Every one of those mistakes became a rule or an automatic check, which is the part I'm proudest of.

The numbers

The agent ran Lighthouse three times on each side and kept the median, after one early single run produced numbers that were pure noise.

Site Performance score Requests
diego.horse 55 → 80 323 → 13
thesunrise.org 58 → 72 41 → 23
fireflow.cc 96 → 99 11 → 12

Homepage weight, in KB

Lighthouse mobile, median of three runs on each side. This blog's homepage used to make 323 requests, now it makes 13.

On this blog the first paint went from 15.5 seconds to 1.7 on a simulated mid range phone (the homepage has different posts on it now than in August, so take the exact figure with a pinch of salt, the direction is not in doubt).

diego.horse homepage on a simulated mid range phone, in seconds

Same Lighthouse setup on both sides, captured August 21 and September 28.

The scores didn't land there by accident. After every deploy the agent ran Lighthouse (and PageSpeed Insights, the same engine in Google's clothes) and worked through whatever it complained about. On this blog the Substack newsletter embed was quietly loading Datadog, Sentry, Google Ads remarketing and about 130 other files on every page, and now it waits until you scroll down to it. The 60 pixel avatar in the header turned out to be downloading a 1,600 pixel image. The theme's CSS went from 134 KB to 58 KB, with before and after screenshots of 22 pages to prove nothing moved. On Fireflow, it even found that the brand colors inherited from the WordPress design failed the contrast test, something I had not been paying attention to at all.

PageSpeed Insights desktop report for diego.horse, 100 for Performance, Accessibility, Best Practices and SEO, 2 out of 2 for Agentic Browsing, with First Contentful Paint 0.4 s, Largest Contentful Paint 0.4 s, Total Blocking Time 30 ms, Cumulative Layout Shift 0 and Speed Index 0.5 s.

PageSpeed Insights for this blog's homepage on desktop. On the simulated slow phone it's the 80 from the table above, but let me enjoy the four circles for a minute. There's also an Agentic Browsing category now, and the robots gave it 2 out of 2.

Smart Keys is the fun one. The standard script said the new site got worse, 69 down to 56, with a 10 second Largest Contentful Paint. Instead of shrugging, the agent chased it and found a one second delay before first paint that only happened when Lighthouse launched Chrome itself, and that nothing on the page caused. Lighthouse's slow phone simulation then stretched that one second into ten. Measured with the same setup on both sides, the store page went from 54 to 89 and server response from 537 ms to 117 ms. Lesson learned, believe the observed timings over the simulated ones when they disagree.

And the most important caveat is in the robot's own words. The "before" numbers are best case, because they were captured when the old server happened to be answering. The honest summary is not that the old sites were slow, it's that they were "unpredictable, and sometimes down." No Lighthouse score has a column for "there was no page."

The carbon rabbit hole

The reports also cover energy and carbon, and this is where I got more than I bargained for. The standard model (the one behind websitecarbon.com) works from bytes transferred, so for this blog it says 0.71 g of CO₂e per page view before and 0.05 g after, a 93% cut.

Estimated CO₂e per page view, in grams

Sustainable Web Design Model v4, which only looks at bytes transferred. An estimate, and the robot was the first to say so.

Then the agent argued with its own number. That model multiplies bytes by a constant and never looks at how hard the phone actually worked. Measuring main thread time instead, the honest saving on a visitor's phone here is more like 73 to 78%, and on Fireflow it was only 35 to 55%, shrinking as connections get faster. For the old shared server it estimated my slice of a 64 core machine running around the clock, and then immediately pointed out that Namecheap's server doesn't power down because I left, my share just goes to the neighbors.

Then it counted the cost of the migration itself. The biggest term was the AI inference, somewhere between 50 and 1,000 Wh for the first site alone, with no way to measure it from my side. Its conclusion is my favorite sentence of the whole project:

Quantifying the carbon consumed more energy than the site's network saving will recover in a year at low traffic.

It also wrote that this was never a carbon decision, because the site was down, and that presenting the carbon saving as the reason would be backwards. I've read a lot of sustainability pages from companies ten thousand times my size that were less honest than that (and it came from the thing burning the most energy in the room).

Still, I want to believe it pays off in the long run. The migration cost was paid once, and the lighter pages save a little on every single visit for as long as these sites stay up.

Want to try it?

I packaged everything the robot learned (the playbook, every trap, the templates and the scripts it wrote along the way) as NeighDash, a free and open source Claude Code skill: github.com/diegodotta/neighdash.

Not technical? Open Claude Code (the desktop app is the friendliest way in) and paste this, with your own address:

Help me move my website example.com off WordPress and onto Cloudflare using NeighDash (https://github.com/diegodotta/neighdash). I'm not technical. Read the repo's skills/neighdash/SKILL.md first, explain each step in plain words before doing it, do the typing for me, and ask me before anything that could take my site or my email down.

It will still need you for the parts only a human can do, like creating a free Cloudflare account and clicking "allow" when a login pops up. The rest is the robot's job.

Already using Claude Code? Two lines and your agent knows what mine had to learn the hard way.

/plugin marketplace add diegodotta/neighdash
/plugin install neighdash@neighdash

Using another agent, or curious about what's under the hood? Here's the prompt I'd start with today, knowing what I know now. It's the short version of those learnings (you'll find new ones, that's the fun part). Swap the bits in brackets for your own.

I want to move my WordPress site off shared hosting and onto static HTML served by Cloudflare Workers on the free plan. You're doing the migration with me. Work in small steps and ask me before anything that is hard to undo.

About the site: [domain], hosted at [host], SSH access [yes/no], DNS [already on Cloudflare / elsewhere]. I update it about [once a month] and nobody else edits it.

1. Start a handoff doc, CONTEXT.md, with the goal, how to reach everything, the decisions we lock in and a numbered list of learnings. Keep it current so a fresh session can pick up where the last one stopped.
2. Before changing anything, run Lighthouse three times on the live site and keep the median, so we have an honest "before".
3. List every URL the old site answers: posts, pages, tags, date archives, feeds, ?p= shortlinks, sitemaps and /wp-content/uploads images. Check the access logs for URLs other sites and apps link to. Every one must keep working, as is or as a 301.
4. Convert from the rendered pages, not the raw database content. Small sites become plain HTML and CSS. A blog becomes Markdown plus a small build script, with Pagefind for search.
5. Convert images to WebP at the sizes they are displayed. Keep originals out of git.
6. One private GitHub repo. Only a build folder (dist/) is ever published, never the repo root, so .git, notes, scripts and secrets can't leak.
7. Deploy with Workers static assets, connected to GitHub: work on dev, pushing to master deploys. Never merge to master without asking me. Add a check to the build that fails the deploy when something is wrong.
8. Write a script that requests every old URL and checks the answer. Run it on wrangler dev and on the workers.dev URL before we touch DNS.
9. Cutover: back up the DNS records, delete only the apex A and www CNAME, attach the custom domains, and put the records back automatically if that fails. Never touch MX, SPF, DKIM or DMARC.
10. After: purge the cache and confirm with several requests, not one. Check that Bot Fight Mode and its JavaScript detections are both off. Rerun Lighthouse with the same setup, fix what it flags, and check visual changes with screenshots, not status codes.

You'll need Claude Code (or any agent that can run a terminal), a GitHub account and a free Cloudflare account. The Cloudflare MCP server helps a lot for the DNS and account settings, and the agent can set it up with you.

What's left

The Namecheap plan is cancelled and premium52 is officially someone else's problem. There's still some housekeeping on my list (there always is, a migration is never really finished, it just gets quieter). And yes, this blog still uses WordPress's Kanso theme on purpose, it's kind of nice. Still, the redesign may come next (or never).

Cloudflare ended their post inviting everyone to try EmDash. My invitation is smaller. If your WordPress site gets a few visits a day and mostly exists so people can read things, you probably don't need a CMS at all. You need a folder of text files and someone patient enough to move 251 posts into it. Mine never complained once, it just kept a very long list of everything that went wrong.

Originally published on diego.horse, where the charts and the little race are interactive.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.