A site owner asked me to get his blog off WordPress. It was a Blackstone and flat-top cooking site, Griddle Sizzle, 88 posts built up over a few years, and it had collected the problems a maturing WordPress site tends to collect: pages that loaded slowly, a stack of plugins doing work that plugins should not be trusted with, and a hosting bill that only went in one direction. He did not want a redesign for its own sake. He wanted it fast, cheap to run, and not one missed security patch away from going down.
So I rebuilt it as a static site on Astro, deployed to Cloudflare. Same content, same URLs, a fraction of the weight. What follows is how it went, and the part I am most careful to get right on a migration like this: moving everything across without losing a page.
Why move off WordPress
WordPress is fine until it is not. The failure modes are predictable. Every plugin is code someone else wrote that runs on every request. Every request hits PHP and a database to assemble a page that, for a blog, barely changes from one week to the next. And the monthly cost of keeping that machine warm and patched is real money for a site that is fundamentally a pile of articles.
A static build inverts all of that. The pages are rendered once, at build time, into plain HTML. There is no server assembling anything on the fly, no database to babysit for page loads, no plugin surface to patch. Astro ships almost no JavaScript by default, self-hosts the fonts so there are no third-party font requests, and runs every image through its build pipeline with real dimensions and lazy loading. I built to a Lighthouse target of 95 or better on the home and post templates, and the finished site clears it.
The payoff is practical. A griddle recipe should load the instant someone taps it from a Google result, and the owner should never get a 2am message that his site went down because a plugin update went sideways.
Everything that had to come across
Before writing any code, I pulled a full inventory of the live site so the new build would account for all of it. The starting plan described three kinds of post: education, recipes, and top-lists, which is what the site’s menu shows. The live data had five. Reviews and news are the two that a quick look at the homepage would miss, and reviews turned out to be the second-largest section on the whole site.
That mattered most for the URLs. Reviews alone held 28 posts, so building only the three obvious categories would have left roughly a third of the content with nowhere to live and broken every /reviews/ and /news/ URL, all of them pages with inbound links and rankings. So the build kept all five categories, each on its exact /category/slug/ permalink.
This is why I start a migration by pulling the real post data rather than working from a description of the site. A live site almost always holds a little more than anyone remembers off the top of their head, and on a migration the forgotten corners tend to be the ones with links pointing at them.
The stack, briefly
Nothing exotic, chosen so the owner is not locked into me or anyone else:
- Astro 5, static output, for the site itself.
- Cloudflare Pages for hosting, with a GitHub Actions workflow that builds and deploys on every push to
main. - Pagefind for search, indexed at build time so it runs entirely in the browser with no search service behind it.
- Sveltia CMS at
/admin, so the owner still edits posts in a normal visual editor instead of writing Markdown by hand. The content lives as Markdown in the repo, but he never has to see that.
The whole thing is portable. It is a Git repository and a Cloudflare project. If he ever wants to move it or hand it to another developer, there is nothing proprietary to unwind.
Forms without a backend
The only parts of the site that are not static are the newsletter signup and the contact form. On WordPress those usually mean another plugin or a paid form service with an API key sitting in your config. I did not want either.
Instead they run as two small Cloudflare Pages Functions that write to a Cloudflare D1 database, which is just SQLite that Cloudflare hosts next to the site. A signup lands in a subscribers table. A contact message lands in a contact_messages table. A shared helper handles rate limiting so the endpoints cannot be hammered. There is no Mailchimp, no form SaaS, and no API keys in the code, because the forms only ever write to the owner’s own database. Fewer moving parts, fewer bills, nothing to leak.
Designing away from the food-blog look
The owner did not want the standard food-blog template, and neither did I. The concept I built the design around is that a flat-top griddle is a slab of seasoned carbon steel, cool at the edges and screaming hot in the middle, so the whole site treats color like heat. The chrome is oiled, blackened steel. Articles sit on a warm light plate with a serif body typeface, which is unusual for the niche and quietly signals a long read rather than a quick recipe card. A heat-zone gradient runs from char to ember to flame, used structurally in a few places: a thin band at the top of every page, the scroll-progress line on an article, a bloom on the post cards when you hover them. It always behaves like temperature and never like a flashy neon accent.
You can see how it came together on the live site. Dark plus orange is a stock look if you let it be one, so the rule was that the gradient had to earn its place every time it appeared.
A migration you can actually check
The riskiest part of any content migration is silently losing something. You move 88 posts, 3 go missing, and nobody notices for a month.
The extraction was written so that could not happen quietly. A script pulls the content straight from the live WordPress site’s own API, converts the HTML to clean Markdown, and downloads the media, roughly 791 files in this case. Then it reconciles its own output against the counts WordPress itself reports, so if it expected 88 posts and wrote 87, it says so instead of shrugging. It is re-runnable, so I could extract, spot-check, fix a conversion bug, and extract again without corrupting anything. When the numbers matched on both sides, I trusted the move.
What it costs to run now
About a dollar a month, give or take, with no plugins to update and no server to keep patched. Every post kept its URL, so the search equity carried over intact. The pages are fast because there is almost nothing to them. And the owner got a site he can hand to anyone, backed by his own data, with the only recurring decision being what to cook next.
Most WordPress blogs do not need WordPress. They need the articles to stay put on the same URLs and load fast. That is most of what this project was.
One month in, the numbers
A migration is supposed to protect what a site already has, so breaking even would have counted as a win. The first month came back better than that. I want to show the data plainly, caveats attached, because a single month proves less than the size of these jumps might suggest.
Search visibility moved first. Google Search Console impressions went from 189 to 573 in the month after launch, close to triple, and total first-time visitors in GA4 a little more than doubled over the same window.
The channel breakdown shows where the growth came from.
Organic search nearly tripled, from 354 to 974. The one I did not expect was the AI-assistant channel, which went from 3 visitors to 101. Bing’s crawler data pointed the same way. Its AI citations of the site roughly doubled, from 8,300 to 15,900, and the count of distinct pages it cited climbed from 24 to 55, so more of the site was getting read and quoted rather than the homepage alone.
I would not read too much into a single month, though. It is a short window, this is a small site, and I ran no controlled test, so I am not going to claim the migration alone did all of this. Some of it is ordinary variance and whatever summer does to a grilling blog. What the rebuild did do is remove the things that hold these numbers down: slow pages, bloated markup, and render-blocking scripts that make a page hard for a crawler or an AI model to read. Fast, clean, fully crawlable pages on their original URLs are what all three of these tools reward.
Common questions
Did any URLs change in the migration?
No. Every post kept its exact WordPress permalink in the form /category/slug/, all five categories included. Preserving URLs is what keeps existing rankings and inbound links intact, so I treated it as non-negotiable from the start.
How can a static site handle a newsletter and a contact form?
Through small serverless functions. On this build the forms run as Cloudflare Pages Functions that write to a Cloudflare D1 database. The pages themselves stay static, and only the form submission touches a function. There is no third-party form service and no API keys in the code.
Can the owner still edit posts without touching code?
Yes. The site uses Sveltia CMS at /admin, a normal visual editor. Posts are stored as Markdown in the repository, but the owner edits them in a browser and never has to work in the files directly.
Why Astro instead of another framework?
For a content site, Astro ships almost no JavaScript by default, self-hosts fonts, and optimizes images at build time, which is most of what you need for strong Core Web Vitals. It also outputs plain static files, so hosting is cheap and there is no server to maintain.
What does a migration like this cost to run afterward?
Hosting is roughly a dollar a month on Cloudflare, sometimes free depending on traffic, with no plugin licenses and no managed-WordPress fees. The build and deploy run automatically on every push, so there is no ongoing platform to pay for beyond the domain.
Working on this same shift?
I write about SEO, GEO, and getting found by AI search.
If this resonated, I'd love to compare notes.