WordPress to Cloudflare Workers: Why and How I Moved My Site
After ten years on WordPress I rebuilt mnahli.com as a static Astro site on Cloudflare Workers. What it costs, what got faster, and the exact setup.
By Mohamed Nahli 5 min read
I built my first WordPress site in 2015 and probably a hundred more for clients after that. I know it well, I still recommend it for some businesses, and I still rebuild it for clients who want to leave. So when it was time to launch this site, the honest question was: what would I build if nobody was paying me to be careful?
The answer is what you’re reading. A static Astro site, served by Cloudflare Workers, content in a git repository, a tiny Worker for the forms. This post is the reasoning and the setup, in enough detail to copy.
What was wrong with WordPress for this site
Nothing dramatic. The old mnahli.com was an empty WordPress install on a shared hosting plan I already paid for. It worked. But “works” hides a list of small taxes:
- Speed. Shared hosting plus PHP plus a theme plus a cache plugin gets you a page in one to two seconds if you tune it. The edge serves the same page in under 200 ms without tuning.
- Maintenance. Core updates, plugin updates, PHP versions, the occasional plugin that breaks the site at 2 a.m. For a client, fine — it’s billable. For my own site, it’s friction that stops me writing.
- Security surface. A database, an admin login and a plugin ecosystem are three attack surfaces a blog doesn’t need.
- Ownership. Content in a MySQL database is technically mine. Content in a git repository is mine in a way I can
git clone.
None of these is a reason to leave WordPress if you have a team, a shop, or an editor who needs a visual interface. All of them together are a reason for a one-person site to leave.
Why Workers, not Pages
Cloudflare has two ways to host a static site. Pages was the original; Workers with static assets is the newer one, and it’s what Cloudflare now points new projects at. I checked the SEO question carefully because it’s the one people ask me: they’re identical. Same network, same HTML, same caching. Googlebot cannot tell them apart.
The difference is what happens when you need a server for something. On Pages you’d bolt on Functions or a second project. On Workers, the same deployment that serves your HTML can answer POST /api/subscribe. This site needs exactly that — a contact form, a self-hosted newsletter, and later, gated content — so Workers was the obvious choice. If you only need HTML, either is fine, and Workers is still the safer bet for the future.
The stack, and why each piece
| Piece | Choice | Why |
|---|---|---|
| Site framework | Astro 7 | Zero JavaScript by default, content collections with schema validation, images optimised at build |
| Hosting | Cloudflare Workers static assets | Free, global, no cold starts for static files |
| Forms & newsletter | A small Worker, scoped to /api/* |
Only runs when someone submits something |
| Database | Cloudflare D1 (SQLite) | Leads and subscribers in my own database, free tier |
| Spam | Cloudflare Turnstile | Invisible for humans, no reCAPTCHA puzzles |
| CI/CD | GitHub Actions | Tests and SEO checks on every pull request, deploy on merge |
Everything in that table is on the stack page with honest notes, including what each costs.
The setup, step by step
1. Scaffold the Astro site
Static output, no server adapter. Two settings matter for SEO and I set them on day one:
// astro.config.mjs
export default defineConfig({
site: "https://mnahli.com",
output: "static",
trailingSlash: "never",
build: { format: "file" },
});
trailingSlash: "never" plus format: "file" means every page has exactly one URL: /blog/my-post, served from dist/blog/my-post.html. This matters more than it looks — keep reading.
2. Point Wrangler at the build
// wrangler.jsonc
{
"name": "mnahli",
"main": "worker/index.ts",
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"html_handling": "drop-trailing-slash",
"not_found_handling": "404-page",
"run_worker_first": ["/api/*"]
}
}
Two lines here are the ones I got wrong on earlier sites:
html_handling must agree with Astro’s trailing-slash setting. On a previous site I had Astro building /tours/index.html and Cloudflare set to serve both /tours and /tours/. Search Console eventually showed 44 pages indexed at both spellings — every page competing with a copy of itself. drop-trailing-slash here and never in Astro fixes it: one URL, and the other spelling is a permanent redirect.
run_worker_first must be scoped, never /*. The Worker only needs to see /api/*. If you route everything through it, every image and stylesheet request becomes a billable Worker invocation. Scoped, the static files never touch the Worker at all.
3. Write the Worker
The Worker is small. It answers the form endpoints and hands everything else to the asset layer:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/api/subscribe") return handleSubscribe(request, env);
if (url.pathname === "/api/contact") return handleContact(request, env);
return env.ASSETS.fetch(request);
},
};
Both handlers verify a Turnstile token, write to D1, and send me an email. No third-party form service, no monthly submission cap, no one else holding my leads.
4. Deploy, then move the domain
wrangler deploy gives you a *.workers.dev URL to check everything on. Once it looked right, I deleted the old A record pointing at the shared host, added the custom domains in wrangler.jsonc, deployed again, and added one redirect rule for www → apex. The old WordPress install got deleted the same afternoon.
5. Automate it
A GitHub Actions workflow runs on every pull request: type check, unit tests, build, and an SEO lint that fails the build if a page has a title over 60 characters, a missing meta description, an image without alt text, or a post with no link to its topic hub. Merge to main deploys. I never run wrangler deploy by hand anymore.
What changed, measured
- Time to first byte: from around 600 ms on shared hosting to under 50 ms from the nearest Cloudflare location.
- Hosting cost: the shared plan is still there for other sites; this one adds nothing to it.
- Plugins to update: zero.
- Backups:
git clone. - Deploys: every merge, with tests.
What you give up
Be honest with yourself about these before you copy me.
- No visual editor. Posts are Markdown with a small header. I like it; some people hate it. You can add one, but it’s extra work.
- No plugin for that. Comments, related posts, newsletter forms — you build or wire them yourself. That’s a weekend for a developer and a wall for a non-developer.
- A build step. Changing a typo means a commit and a thirty-second build, not a Save button.
If those three are fine with you, there is no reason for a personal site or a small business site to be on WordPress in 2026. If they’re not — or if you have a team, an editor, or a WooCommerce store — WordPress is still right, and a good host with a real cache is your upgrade instead.
Want this done for your site?
This migration is one of the three things I do for clients: a WordPress to Cloudflare rebuild with the SEO carried over, every URL redirected, nothing lost from the index. If your site is slow or fragile and you’d rather not learn Wrangler yourself, send me a brief. And if you’d rather do it yourself, the Build SaaS & Tools topic is where I’ll keep documenting the stack.
Key takeaways
- For a blog and portfolio, a static site on Cloudflare Workers is cheaper, faster and safer than WordPress on shared hosting — and it's what Cloudflare now recommends over Pages for new projects.
- The stack: Astro for the site, Workers static assets for hosting, a small Worker for forms, D1 for the database, GitHub Actions to deploy.
- The two mistakes that cost me on earlier sites: trailing-slash duplicates in the index, and routing every request through the Worker. Both are avoidable with two config lines.
- Content lives in git. That's the real upgrade: version history, pull-request previews, and no database to back up.
Frequently asked questions
Is Cloudflare Workers better than Cloudflare Pages for SEO?
They are identical for SEO — same edge network, same HTML, same speed. Cloudflare recommends Workers with static assets for new projects because it adds an API layer (forms, newsletters, gated content) that Pages cannot offer without a second project.
Does a static site hurt SEO compared to WordPress?
No. Search engines read HTML, and a static site ships cleaner, faster HTML than most WordPress themes. Everything WordPress SEO plugins do — titles, canonicals, schema, sitemaps — is done here at build time, and it's tested on every deploy.
How much does hosting a site on Cloudflare Workers cost?
For a site like this one, effectively nothing. Static assets are free and unlimited; the Worker only runs for form submissions, which fit inside the free tier's 100,000 daily requests. The custom domain and DNS are free too.
Can I still write posts without touching code?
Posts are Markdown files with a small header. You can write them in any text editor, in the GitHub web UI, or — as I'm doing — dictate them and let a tool open a pull request. A visual editor can be added on top later.
Sources
Letters
Get the next one in your inbox
What I'm building, what worked, what didn't, and the numbers. One email when there's something worth sending.