Every WordPress site I've migrated to Astro had the same hidden landmine. Not the theme, not the plugins. The URLs.
WordPress leaves a mess of URL shapes behind after a few years. /?p=123, /2019/04/some-post/, /category/news/, /wp-content/uploads/2020/05/menu.pdf, tag pages nobody remembers making. Google has all of them indexed. The client has some of them printed on flyers. If you rebuild the site in Astro and just ship it, a chunk of that turns into 404s overnight and the client calls you three weeks later asking why the phone stopped ringing.
So now I don't touch a single Astro component until I've built the redirect map. Here's how I do it.
1. Pull every URL the old site actually has
I grab three lists and merge them:
- The sitemap (
/sitemap_index.xmlor/wp-sitemap.xml, depends on the SEO plugin) - A crawl of the live site (Screaming Frog free tier is fine under 500 URLs, or let your coding agent write a quick crawler)
- The Search Console "Pages" export, because that shows what Google thinks exists, wich is not always what the sitemap says
The Search Console list is the one that catches the weird stuff. Old landing pages from a 2021 ad campaign, a /specials page someone deleted but still gets hits. Those are the ones that hurt when they break.
2. Decide the new URL for each one
I dump the merged list into a CSV with two columns, old and new, and go through it. Most of it is boring:
-
/2019/04/some-post/goes to/blog/some-post -
/category/*mostly goes to/blog - Attachment pages and
?p=links go to whatever post they belonged to - PDFs in
wp-content/uploadsI copy across and keep at a sane path
This is a great job to hand to Claude Code or Cursor. Give it the CSV and the new Astro src/pages tree and ask it to propose the new column. It gets maybe 90% right. I review the rest by hand because the agent has no idea the client calls the "Services" page "What we do" on their business cards.
3. Write it as a redirects file
I host client sites on novence.ai, so the map just becomes a /_redirects file in public/:
/2019/04/:slug/ /blog/:slug 301
/category/* /blog 301
/specials /offers 301
/wp-content/uploads/2020/05/menu.pdf /files/menu.pdf 301
Splats and placeholders cover most of the date-based post URLs in one line, so a site with 300 old posts usually ends up with a redirects file of maybe 20 to 40 lines. Keep it in the repo, it's part of the site now.
4. Test it before DNS moves
This is the step people skip. Before I point the domain, I deploy to the preview URL (npx novence deploy ./dist) and run every old URL from the CSV against it with a tiny script:
for (const { old, expected } of rows) {
const res = await fetch(previewBase + old, { redirect: "manual" });
const loc = res.headers.get("location");
if (res.status !== 301 || !loc?.endsWith(expected)) console.log("FAIL", old, res.status, loc);
}
Anything that prints FAIL gets fixed before the client ever sees it. I make the coding agent run this as a check before every deploy for the first month, so if someone renames a page later the old link doesn't quietly die.
5. Watch it for a few weeks after launch
After the domain switches I keep an eye on two things: Search Console's 404 report, and the analytics for top landing pages. If a page that used to get traffic drops to zero, there's usually a redirect missing. Add a line, redeploy, done.
Why bother
Honestly the redirect map is the difference between "the new site is nice" and "the new site killed our Google rankings". The design gets all the attention but this is the boring hour of work that actually protects the client's business. And once you've done it on two or three migrations, the CSV plus agent plus test script becomes a repeatable thing you can do on every WordPress site you move.
If you're doing agency migrations, how are you handling the old URLs? Curious if anyone has a better way to catch the stuff that isn't in the sitemap.
Top comments (1)
사이트맵에 없는 옛 주소를 Search Console 목록과 합쳐 보는 부분이 실용적입니다. 테스트에 최종 도착 페이지의 상태와 리디렉션 루프·체인 여부도 넣으면, 301은 나오지만 여러 번 돌아가거나 사라진 페이지로 연결되는 경우까지 확인할 수 있겠네요.