Last month I moved a client with about 300 blog posts off WordPress. The design was the easy bit. The content is what eats your week if you dont plan it. Here is the workflow I use now for getting those posts into Astro content collections.
1. Export, then look before you convert
I export the WordPress XML from Tools > Export and also pull the posts through the REST API (/wp-json/wp/v2/posts?per_page=100). The XML has the raw content, the API gives me clean fields for slug, date, categories and featured image id. Both go in a migration/ folder in the repo.
Before converting anything I open ten random posts and check what is actually in them. Page builder shortcodes, [gallery], embedded forms, inline styles. You want to know that before you write a converter, not after.
2. Convert to Markdown, keep the frontmatter boring
A small Node script with turndown does HTML to Markdown. Each post becomes src/content/blog/<slug>.md with frontmatter like this:
title: "Post title" pubDate: 2023-04-12 updated: 2024-01-03 description: "First 150 chars or the Yoast description" category: "news" cover: "./images/post-slug.jpg" oldUrl: "/2023/04/post-slug/" The oldUrl field is the one people skip. It lets me generate the /_redirects file straight from the content, so every old permalink 301s to the new one. No spreadsheet needed for blog posts.
3. Define the collection schema first
In src/content.config.ts I define the collection with Zod and make most fields required. If a post is missing a date or a description the build fails and tells me which file. Way better than finding out from the client after launch.
4. The stuff the converter always gets wrong
- Images with hard coded
wp-content/uploadsURLs. Download them, move them next to the content, rewrite the paths. - Galleries and shortcodes. I flag them in a list for manual review instead of trusting the converter.
- Smart quotes and weird non breaking spaces. One replace pass fixes it.
- Internal links still pointing at the old domain or
?p=123style URLs. Grep for them and map them through the same old to new list. - Comments. Work out real discussion vs spam, agree with the client what to keep, and keep the export instead of silently dropping them.
5. Let the coding agent do the boring bit, but check its work
I give Claude Code the converter script, the schema and one rule: after the conversion, run astro build, then crawl dist/ and list any post where the word count is under 50% of the original. That catches posts where the converter ate half the content. Word count is only a warning sign though, so I still eyeball headings, embeds and a few of the long posts.
6. Preview and ship
Once the build is clean I deploy a preview and click through the archive pages, a handfull of old URLs and the RSS feed. I host these on Novence so it is just npx novence deploy from the project folder, and it sits next to all the other client sites. Then I run the old URL list against the preview with curl and make sure everything returns a 301 to a page that returns 200.
What I would tell myself on the first migration
Spend your time on the audit and the schema. The conversion script is the small part. If the content is clean going in, the rest of the project is quiet, and the client never notices the move except that the site got faster.

