Migrating WordPress to Astro Without Losing a URL: the Same Blog, Only Faster

· 14 min read

The GSMX blog moved from WordPress to static Astro and, for readers, only the speed changed: the same addresses, posts, ads and the forum next door. How I did it and how I checked it.

The GSMX blog moved from WordPress to static Astro and, for readers, only the speed changed: the same addresses, posts, ads and the forum next door. How I did it and how I checked it.

The GSMX blog moved from WordPress to static Astro and, for readers, only the speed changed. The addresses, posts and ads are the same, and the forum next door works as before. I checked 2,299 old addresses and none of them leads to the wrong place. On mobile, PageSpeed Insights went from 33–85 to 99–100 (median of 3 runs), and the server answers in under 60 ms instead of over a second.

In short:

  • Same address. The blog stayed at gsmx.pl/blog/. A Cloudflare Worker bound to that path alone serves it, so the forum never noticed.
  • No address was lost. Posts kept their slugs exactly. Only what was broken changed, and every such change has a 301 redirect.
  • Before and after, measured the same way. The same 5 addresses, the same Lighthouse and the median of 3 runs. Without that the comparison means nothing.
  • The ads stayed. They are now the heaviest part of the page, not the blog.
  • Going back was always one command away. WordPress stayed on the hosting for a 4-week observation period.

The starting point: WordPress next to a forum

gsmx.pl is first of all a forum, which runs at the root of the domain. The GSM news blog lived in the /blog/ directory, on a separate WordPress install, with posts going back to 2006.

The state on 25 October 2026, from production and from a copy of the database:

  • 26 installed plugins, 20 of them active: Jetpack, Rank Math, Quick AdSense Reloaded, WP Hide, Fast Velocity Minify, Sucuri, Disqus Conditional Load, an SEO auto-linker and more. On top of that the Presso theme with a child theme, and three sets of tables in the database left over from old installs.
  • 33–42 scripts and 8–10 stylesheets per page. The HTML went through Cloudflare uncached, and the server answered in 0.8–1.6 s.
  • Posts weighed 14–17 MiB according to PageSpeed Insights. Most of that weight was video ads loaded on every post.
  • 20 years of content: several hundred posts, 705 tags, Flash embeds, images on tinypic and fotosik, which no longer exist, galleries, tables and styles written into the HTML.

Search traffic was negligible: 16 clicks in 16 months. That matters for the risk. The migration could not hurt SEO that was barely there, and I am not promising more traffic here. The reasons were different. I wanted a site with no PHP and no database, so nothing to patch. I wanted to be able to move it to any static host. And I wanted to publish new posts from the repository, without logging into a panel.

Measure “before” first

Before changing anything, I measured production. An “after” measurement only means something if you repeat exactly the same thing, so I fixed the method at the start and kept it to the end:

  • 5 addresses: the blog home page, a category and three posts from 2026, 2010 and 2006. The old posts carry the most old HTML.
  • Lighthouse 12 locally, 3 runs per address and mode, median. A single run lies. In PageSpeed Insights two posts with almost identical HTML scored 85 and 33.
  • PageSpeed Insights and GTmetrix as public, verifiable snapshots.
  • The full list of addresses from the WordPress REST API: posts, pages, categories, tags, authors, media. That list later became the redirect test.
PageSpeed Insights for a 2006 post on WordPress, mobile: performance 33, Total Blocking Time 7,690 ms, Speed Index 21.9 s, no real-user data
A 2006 post on WordPress in PageSpeed Insights, 25 October 2026 (Polish interface)

One thing could not be measured. Google had no real-user data for the blog (CrUX: “No data” for all 5 addresses), because traffic was too low. So the comparison is a lab comparison, and I describe it as one.

What I considered and dropped

Option Why not
A blog.gsmx.pl subdomain
It would need a redirect for every address from 20 years. Keeping the address was possible, so there was no reason to change it.
A copy of the Presso theme in Astro
It would pull in the same libraries that slowed WordPress down. The blog got a new, simple layout with the GSMX logo and colours.
A custom CMS with an admin panel
Too heavy for a one-language blog whose authors are technical and do not need a panel. It needs PHP and a database, which is exactly what I wanted to get rid of.
Just caching and a plugin clean-up
It would make the site faster, but PHP, the database and the updates would stay. And there would be no publishing from the repository.

How it works: a Worker on /blog only

The whole gsmx.pl domain goes through Cloudflare. The blog is handled by a Worker with Static Assets, bound to two routes: gsmx.pl/blog and gsmx.pl/blog/*. Everything else reaches the forum server, as before the migration.

What happens to a request to gsmx.pl
    1. gsmx.pl/blog and gsmx.pl/blog/*

      the blog Worker on Cloudflare

    2. the rest of the domain

      the PHP forum, unchanged

  1. The _redirects file

    1,466 301 rules from old addresses, generated by the converter.

  2. A finished file from dist/

    The address /blog/slug/ is the file dist/blog/slug/index.html, with no code run.

  3. Worker code, only where a file is not enough

    • old ?p= addresses
    • 410 for wp-admin, xmlrpc.php and removed tags
    • date archives, RSS, old thumbnails
  4. URL contract test

    2,299 old addresses, 0 failures

    200 for a post, 301 to the right place, 410 for a removed one.

A gsmx.pl/blog* route looks simpler, but it would also catch forum addresses that start with “blog”, such as /blogi/. Hence two exact routes, and the post-switch test checked the forum as well.

Astro builds the blog with base: '/blog' into dist/blog/, and the Worker serves the whole of dist/. The address /blog/slug/ then maps to the file dist/blog/slug/index.html, and Cloudflare returns it without running any code. The Worker code only runs where a file is not enough:

  • old addresses with parameters, such as /blog/?p=1234, are redirected to the right post;
  • 410 Gone for wp-admin, wp-login.php, xmlrpc.php, wp-json and removed tags, so Google knows it is deliberate and not an outage;
  • date archives, RSS and old thumbnail paths, which a static rule cannot describe.

The other redirects, 1,466 rules, live in the _redirects file. The converter generates it; nobody writes it by hand. That file has to sit at the root of dist/, not in dist/blog/, because that is the only place Cloudflare reads it. In dist/blog/ it would just be a downloadable file.

Cloudflare redirects addresses without a trailing slash (/blog/slug) with a 307, not a 301 as WordPress did. I accepted that on purpose: the canonical addresses have the slash, and the versions without it barely appeared in links or in Search Console.

The converter: content, media and 20 years of leftovers

The converter reads post content from the WordPress REST API (content.rendered) and the rest from a local copy of the database. Rendered content already has the shortcodes, galleries and paragraphs expanded, which is what the reader saw. Rebuilding that from the raw content in the database would be a large, fragile piece of code for several hundred posts. From the database I take what REST does not give: Rank Math meta titles and descriptions, relations and the media map.

The old posts stayed as cleaned-up HTML inside Markdown files, so they look the way they did. New posts are written in plain Markdown.

What the converter did along the way, according to the migration report:

  • 13 YouTube embeds from the Flash era became a lightweight player that loads on click. 4 other Flash embeds became a note with a link.
  • It checked every image on a third-party host. 21 still worked, so it downloaded them onto the blog. 152 images in 53 posts pointed at services that are gone (tinypic, fotosik, imageshack and old domains). It removed them from the content and listed them in the report.
  • It prepared the media: 508 files, originals up to 1,600 px and WebP variants with srcset, width and height, so images do not push the text around.
  • It fixed what was broken: 3 slugs with typographic dashes and quotes percent-encoded in the address, subcategories reachable at two addresses, a category called bada-2, 7 pairs of duplicate tags (wi-fi and wifi, app-store and appstore…). Every change got a 301, and the converter rewrote internal links in the content to the new addresses straight away.

Working, indexed addresses stayed, even when they were not pretty. Changing an address has a cost, and a prettier slug brings nothing.

The URL contract test: 2,299 addresses, 0 failures

The hard part of a migration is not the templates, it is the old addresses. So the full list of WordPress addresses became a script that checks each one against the contract: 200 for a post, 301 to the right place for a changed address, 410 for a removed one.

I ran it on staging before the switch-over and on production right after it:

Address sourceAddressesFailed
Posts from the REST API3360
Tags from the REST API7050
Old ?p= addresses7070
Media (WP Hide and wp-content paths)3000
Thumbnails1940
Authors, categories, pages330
Other: RSS, date archives, sitemaps, ads.txt, GSC verification240
Total2,2990

That was the condition for the switch-over: 100% or no deploy.

A switch-over without risk

Before binding the Worker to production, I checked the Cloudflare zone. Redirect Rules and Page Rules run before a Worker, so one old rule on /blog would take the traffic. Rocket Loader or email obfuscation would change the HTML. Staging was in the same zone, so I compared its response with the file in dist/. The only difference was the Web Analytics script.

The switch-over itself was a wrangler deploy with two routes, on 2 November 2026 at 08:02 UTC. WordPress was not shut down. It stayed on the hosting; traffic to /blog simply stopped reaching it. Going back means removing those two routes and takes seconds.

From plan to switch-over
  1. Step 1

    25 Oct: plan and "before"

    The migration spec, Lighthouse, PageSpeed Insights and GTmetrix on 5 addresses, the full address list.

  2. Step 2

    25 Oct: converter and Astro site

    Content from the REST API, media, the redirect map, the new blog layout.

  3. Step 3

    25–27 Oct: Worker on staging

    staging.gsmx.pl, the URL contract test, PageSpeed Insights on staging.

  4. Step 4

    30 Oct: categories and descriptions

    A new category structure, separate fields for the H1, the SEO title and the descriptions.

  5. Step 5

    2 Nov: switch-over

    Worker routes on production, the 2,299-address test, sitemaps in Search Console.

  6. Step 6

    2 Nov: "after"

    The same script, the same addresses, the same location as 8 days earlier.

  7. Step 7

    7 Nov: PageSpeed Insights and GTmetrix

    Public measurements on production, three ad fixes and 3 PageSpeed Insights runs.

Results: before and after

The same method as before the migration: Lighthouse 12 locally, 3 runs, median. The migrated site carries AdSense, just like WordPress did.

PageMobileDesktopLCP (mobile)CLS (mobile)Server responseRequestsScriptsWeight
Home page36 → 9480 → 10011.08 → 2.21 s0.335 → 0.0391,518 → 59 ms83 → 2836 → 52,433 → 709 KB
Category61 → 9391 → 1005.79 → 2.20 s0.063 → 0.0391,407 → 48 ms87 → 2838 → 51,315 → 717 KB
2026 post68 → 8691 → 1004.93 → 1.89 s0.075 → 0.0421,059 → 46 ms74 → 2142 → 51,150 → 505 KB
2010 post60 → 8989 → 1006.66 → 2.21 s0.136 → 0.0511,222 → 46 ms78 → 2042 → 51,730 → 474 KB
2006 post49 → 8789 → 1006.77 → 1.74 s0.155 → 0.0451,609 → 26 ms73 → 2042 → 51,581 → 474 KB

Server response changed the most. WordPress assembled every page in PHP on every request. Now Cloudflare returns a finished file from the nearest server. In this measurement all 5 pages score 100 on desktop.

Server response time before and after the migration
Server response time before and after the migrationLighthouse, mobile, median of 3 runs. On WordPress the server answered in 1,059–1,609 ms, on Astro in 26–59 ms.Home pageWordPress, 25 Oct1,518 msAstro, 2 Nov59 msCategoryWordPress, 25 Oct1,407 msAstro, 2 Nov48 ms2026 postWordPress, 25 Oct1,059 msAstro, 2 Nov46 ms2010 postWordPress, 25 Oct1,222 msAstro, 2 Nov46 ms2006 postWordPress, 25 Oct1,609 msAstro, 2 Nov26 ms

Lower is better.

Server response time before and after the migration
PageVersionResponse time
Home pageWordPress, 25 Oct1,518 ms
Home pageAstro, 2 Nov59 ms
CategoryWordPress, 25 Oct1,407 ms
CategoryAstro, 2 Nov48 ms
2026 postWordPress, 25 Oct1,059 ms
2026 postAstro, 2 Nov46 ms
2010 postWordPress, 25 Oct1,222 ms
2010 postAstro, 2 Nov46 ms
2006 postWordPress, 25 Oct1,609 ms
2006 postAstro, 2 Nov26 ms

PageSpeed Insights and GTmetrix

A Lighthouse run on my own machine is easy to doubt, so I repeated the public measurements too: PageSpeed Insights on the same 5 addresses, mobile and desktop, plus GTmetrix from Seattle. Before the migration I ran PageSpeed Insights once per address. After it I ran three independent runs and report the median, because a single run can be a dozen points off. Every result links to a report.

PageMobileDesktopWeight (mobile)
Home page51 → 9998 → 1002,761 → 536 KiB
Category62 → 10085 → 992,005 → 519 KiB
2026 post52 → 9961 → 9615,759 → 620 KiB
2010 post85 → 10092 → 10014,359 → 618 KiB
2006 post33 → 9962 → 9916,945 → 515 KiB

Before the migration: 25 October 2026, a single run, Lighthouse 13.5. After the migration: 7 November 2026, median of 3 runs, Lighthouse 13.6.

Across all 30 runs after the migration, mobile scores 97–100 and desktop 95–100. On WordPress it was 33–85 and 61–98. The 2006 post had a Total Blocking Time of 7,690 ms and now has 40–160 ms. Posts weigh about 0.5–0.6 MiB instead of 14–17 MiB. Accessibility and SEO score 100, against 79–96 and 91–92 on WordPress.

Mobile PageSpeed Insights before and after the migration
Mobile PageSpeed Insights before and after the migrationMobile performance score. WordPress: 33–85, a single run. Astro: 99–100, median of 3 runs.Home pageWordPress, 25 Oct51Astro, 7 Nov99CategoryWordPress, 25 Oct62Astro, 7 Nov1002026 postWordPress, 25 Oct52Astro, 7 Nov992010 postWordPress, 25 Oct85Astro, 7 Nov1002006 postWordPress, 25 Oct33Astro, 7 Nov99

Higher is better; 100 is the maximum.

Mobile PageSpeed Insights before and after the migration
PageVersionScore
Home pageWordPress, 25 Oct51
Home pageAstro, 7 Nov99
CategoryWordPress, 25 Oct62
CategoryAstro, 7 Nov100
2026 postWordPress, 25 Oct52
2026 postAstro, 7 Nov99
2010 postWordPress, 25 Oct85
2010 postAstro, 7 Nov100
2006 postWordPress, 25 Oct33
2006 postAstro, 7 Nov99
Mobile page weight before and after the migration
Mobile page weight before and after the migrationTotal page weight according to PageSpeed Insights. Posts on WordPress weighed 14–17 MiB, mostly because of video ads. On Astro they weigh about 0.5–0.6 MiB.Home pageWordPress, 25 Oct2,761 KiBAstro, 7 Nov536 KiBCategoryWordPress, 25 Oct2,005 KiBAstro, 7 Nov519 KiB2026 postWordPress, 25 Oct15,759 KiBAstro, 7 Nov620 KiB2010 postWordPress, 25 Oct14,359 KiBAstro, 7 Nov618 KiB2006 postWordPress, 25 Oct16,945 KiBAstro, 7 Nov515 KiB

Lower is better.

Mobile page weight before and after the migration
PageVersionWeight
Home pageWordPress, 25 Oct2,761 KiB
Home pageAstro, 7 Nov536 KiB
CategoryWordPress, 25 Oct2,005 KiB
CategoryAstro, 7 Nov519 KiB
2026 postWordPress, 25 Oct15,759 KiB
2026 postAstro, 7 Nov620 KiB
2010 postWordPress, 25 Oct14,359 KiB
2010 postAstro, 7 Nov618 KiB
2006 postWordPress, 25 Oct16,945 KiB
2006 postAstro, 7 Nov515 KiB

What the first measurement caught

The first PageSpeed Insights run after the migration was less even. The home page scored 90 on desktop, and two old posts had a CLS of 0.084 and 0.113. The local Lighthouse run had not shown it. Every time the culprit was the in-content ad slot that AdSense left unfilled. From Poland the ad usually shows; PageSpeed Insights measures from the US and often gets none.

There were three causes, and I found them one after another:

  1. The empty ad slot disappeared at once and the text under it jumped about 300 px. Now it disappears only while it is below the screen.
  2. AdSense first gives the ad 280 px and, when there is none, takes it out of the page layout. A slot reserved at 250 px fell back to that height and the paragraph under it moved up 30 px. The reservation is now 280 px.
  3. An empty ad gets a width of 0 from AdSense. Centred, it moved its contents 330 px to the right. It is now left-aligned.

After these fixes desktop CLS is at most 0.011, and the home page scores 100 on all three runs. The general lesson: a static page does not move on its own. What moves it is whatever you load on top of it from elsewhere, and that has to be measured from more than one place, not just from your own machine.

GTmetrix (Seattle, desktop, home page): grade C → A, performance 62% → 90%, structure 89% → 99%, LCP 3.3 s → 0.38 s. Reports: before and after. That GTmetrix run predates the ad fixes.

A 2008 post on the new GSMX blog on a phone: the header with the GSMX logo, the Rynek (market) category, the Polish title about LG and Samsung in a patent suit, the date, the cover and the start of the post
A 2008 post after the migration, on a phone

What is left: the ads

On mobile the score stops short of 100 because of the ads, not the blog. On staging, PageSpeed Insights broke down the CPU time on the 2026 post:

  • Google ad scripts: 4.2 s;
  • the ad consent dialog: 1.4 s;
  • the blog’s own JavaScript: 41 ms.

When an ad manages to load during the test, the score drops (that post on staging: 69). When it does not, the score is 99–100. There is no fixing that without removing the ads, and the ads stay, because they pay for the blog.

Two ad-related issues only showed up on production:

  • The first “after” run measured CLS of 0.155–0.215 on short old posts. The in-content ad slot only got its height once the ad loaded, and pushed the paragraph below it down. Now every slot reserves 250 px from the first render, and CLS dropped to 0.04–0.05. What remains comes from Google’s consent dialog, not from the blog’s code.
  • AdSense Auto ads placed themselves wherever they liked. I turned them off for the whole of /blog/, and the blog has 4 ad units of its own in fixed places. The forum keeps Auto ads as before. The video ads, which weighed a dozen-plus MiB per post, went away together with WordPress.

What I do not know

The comparison is a lab comparison. On WordPress, Cloudflare Web Analytics collected only 4 measurements from the blog before the switch-over, so I have no real-user “before”. Nor do I have proof that a faster blog will get more search traffic. At 16 clicks in 16 months any change would be noise. The observation period runs until the end of November; after that WordPress will be removed from the hosting.

After the migration: posts from the repository

A static blog does not mean a blog without new posts. A post is a Markdown file in the repository. On gsmx.pl new topics are collected from RSS feeds, and the same automation drafts the post with sources and a cover and opens a pull request. Before the merge every draft goes through a fact check against its sources, and then the blog is built and shipped to Cloudflare with wrangler deploy. 15 posts went out that way in the first week after the migration. A post dated in the future waits and appears with the first build after that date.

How a new post gets published
  1. Topic

    Automation collects topics from RSS feeds, or an author writes the post themselves.

  2. Pull request

    The post as a Markdown file, with sources and a cover.

  3. Review

    • facts checked against the sources
    • copy and publish date
  4. Merge and deploy

    Astro build and wrangler deploy to Cloudflare.

  5. Published

    The post on gsmx.pl/blog

    No panel, no database, no PHP.

What comes next: the forum

The forum on gsmx.pl still runs on XenForo, which means PHP. I am testing a few approaches for it as proofs of concept and MVPs. Most recently I moved it to Flarum as a test. Flarum’s front end is built on jQuery and a fairly dated approach, though, so Flarum will most likely stay only as the back end (headless), with Astro and Vue taking over the forum’s pages. More on that soon.

When a migration like this makes sense

Static Astro fits when content changes at publishing time rather than on every visit. That is true of a blog, a news site and a company website. The number of authors does not matter. On WordPress the GSMX blog had 17 of them, and all of them were moved over. Now anyone with access to the repository can be an author: they write a post as a Markdown file in an editor such as VS Code or with an AI assistant, and open a pull request. On GSMX every author is technical, so nobody misses the panel.

When the authors are not technical and want to keep a panel, WordPress can stay as the CMS while Astro only renders the content. That is how polo.blue has run since 2023. I describe both options on the WordPress to Astro migration page.

If your WordPress is slow and the plugin list grows with every update, start with a measurement and a list of addresses, as here. Those two things decide whether a migration succeeds. The templates are the easy part.

Next step

Your WordPress can be static too

I move sites from WordPress to Astro: the content, the media and the old addresses with 301 redirects, measured before and after. Posts can stay in the WordPress panel or move into files. Tell me what you have and what is getting in your way.

See how I migrate WordPress Get in touch

Frequently Asked Questions

— 01
Do the old addresses still work after moving from WordPress to Astro?
Yes, if you plan for it from the start. On gsmx.pl every post kept its address exactly, and every address that did change (3 slugs with characters percent-encoded in the URL, categories, duplicate tags, media paths) got a 301 redirect. A script checked 2,299 old addresses before the switch-over and right after it. None failed.
— 02
Can an Astro blog live under /blog/ next to another application on the same domain?
Yes. On gsmx.pl the root of the domain is a PHP forum, and the blog is served by a Cloudflare Worker with Static Assets bound to the routes gsmx.pl/blog and gsmx.pl/blog/*. Everything else goes to the forum server as before. A /blog* route would be a mistake, because it would also catch forum addresses that start with "blog".
— 03
How much faster is a static blog than WordPress?
On the same 5 addresses, with the same tool and the median of 3 runs: mobile Lighthouse went from 36–68 to 86–94, desktop from 80–91 to 100, and server response time fell from 0.8–1.6 s to under 60 ms. In PageSpeed Insights the mobile score went from 33–85 to 97–100 across all runs, and GTmetrix raised the home page grade from C to A. Requests dropped from 73–87 to 20–28, scripts from 36–42 to 5.
— 04
What happened to AdSense after the migration?
The ads stayed, and they are now what limits the mobile score. On one post the ad scripts used 4.2 s of CPU and the consent dialog 1.4 s, while the blog's own JavaScript used 41 ms. Every ad slot reserves its height from the first render, because without that the ad pushed the text down (CLS up to 0.215).
— 05
How long did the migration take?
8 days from plan to switch-over: the plan and the "before" measurement on 25 October 2026, the switch-over on 2 November. WordPress stayed on the hosting for a 4-week observation period, so going back meant removing two Worker routes.
— 06
How do you publish without an admin panel?
A post is a Markdown file in the repository. On gsmx.pl new topics are collected from RSS feeds, the same automation drafts the post and opens a pull request with it, and after review and merge the blog is built and shipped to Cloudflare with wrangler deploy. 15 posts went out that way in the first week after the migration.
Back to blog

Related posts

See all blog posts