Now booking enterprise content platform builds for 2026. Contact us

All articles Migrations 9 min read

CMS migration tools: crawl, export, transform and check before cutover

A crawler, an export, a transform script and a check each do one job in a CMS migration. This guide matches tools to those jobs, with their limits and costs, and includes a small example that catches missing records, images, locales and redirects before cutover.


A CMS migration uses several tools, and each covers one part of the move. A crawler lists the public URLs and never sees a draft. An export carries the content, though not always its files, its translations or its URLs. A transform script decides how fields map. Only a check run against the source shows whether anything went missing on the way.

This guide groups the tools by the job they do: discovery, extraction, transformation and import, and verification. For each it records what goes in and comes out, what survives, what does not, and what it costs, checked on 1 October 2026. The last part is a small runnable example of the verification job, with the defects it catches.

The four jobs

JobWhat it settlesTools covered here
DiscoveryWhich public URLs exist and what each returnsScreaming Frog SEO Spider, sitemaps, Search Console exports
ExtractionWhat content exists, with its drafts, translations and filesWP-CLI wp export, the WordPress REST API, sanity dataset export
Transformation and importHow each field, reference and URL maps to the new modelA custom script, the target CMS’s importer or API
VerificationWhether everything arrived and the old URLs still workA check script, a list crawl of the old URLs

Discovery: what the public site exposes

The Screaming Frog SEO Spider crawls a site as a search engine would and reports status codes, redirects, titles, meta data, canonicals and links. The free version crawls up to 500 URLs. A licence, €245 a year on 1 October 2026, removes the limit and adds crawl configuration, saving and reopening crawls, JavaScript rendering, crawl comparison and custom extraction.

For a migration, its redirect audit guide uploads the old URL list in list mode and exports an All Redirects report with each URL’s final destination. The guide’s setup ticks “always follow redirects” in the spider configuration, and the comparison table lists crawl configuration as a licensed feature, so check what the free version allows before planning around it. Screaming Frog was not run for this guide.

A crawl finds only what is public and linked. Drafts, unpublished translations, orphaned pages and the content model are invisible to it. The URL inventory therefore also needs the XML sitemap, Search Console’s page export and the CMS’s own list of published entries; the union of the four is the list the redirects have to cover.

Extraction: content and everything it depends on

WP-CLI wp export writes WordPress’s WXR file. Run through WP-CLI 2.12.0, the current release, on a WordPress 7.1.2 test site, it exported every post, page and attachment record, including a draft, a scheduled post and a post pending review; --post_status=draft narrowed the file to the draft. Attachments arrive as URLs only. The command documentation says WXR files carry neither site options nor the attachment files, so the media has to be fetched from the old server while it is still running. The file splits at 15 MB by default (--max_file_size).

The WordPress REST API returns JSON per collection. Posts come back with status publish unless another status is requested. Each request returns at most 100 records, and the X-WP-Total and X-WP-TotalPages headers report the totals (pagination), which gives an extraction script the count to check itself against. Documented only here.

sanity dataset export writes a gzipped tarball holding every document as NDJSON, plus the image and file binaries. --no-drafts leaves out draft versions, --types limits the document types, and --raw keeps asset references exactly as stored (export documentation). When an asset download fails with 401, 403 or 404, the export prints a warning and continues, so count the files in the tarball against the asset documents. Documented only here; it needs a Sanity project.

Whatever the route, write down what each export includes: statuses, locales, types, whether relationships come as IDs or as embedded copies, and where the files are. Those notes become the expected side of the check.

Transformation and import: where the model changes

A transform maps each source field to a target field, converts rich text, resolves references through an ID map, and rewrites internal links and image URLs. A vendor’s importer can do part of this. EmDash’s WordPress importer, tested on a synthetic site on 1 October, moved posts, pages, terms and media; it created no redirects and left internal links pointing at the old addresses.

Payload’s Local API writes documents from server-side code and skips access control by default. An import script that keeps the default can write anything, so run it only where that is authorised, or pass overrideAccess: false with a user so the access rules apply. Payload’s migrations change the database schema or reshape data already in Payload; content from another CMS needs its own import script.

A custom script earns its place when the model changes shape: several source types merge into one, fields split, rich text needs converting block by block, or the import has to run again and again until cutover. Three properties make those reruns safe:

  • Target IDs derived from source IDs, so a second run updates records instead of copying them.
  • A dry run that reports what would be created, updated and left unchanged.
  • No deletes by default, so a record missing from the source gets investigated before anything is removed.

Verification: proving nothing went missing

A check compares the target with the source. One that reads the target’s own URL map cannot notice that the map lost a row, so take the expected side from the export and from the URL decisions the team made.

CheckWhat it compares
CountsEntries by type, locale and status; media files
RecordsEvery source ID has exactly one target record
RelationshipsEvery reference points at a record that exists
ImagesFeatured images and images inside rich text have their files, with matching hashes
LocalesEach translation keeps its locale and its link to the original
Text and metadataBody text, titles and SEO fields match after the normalizations the team approved
LinksInternal links point at pages the new site serves
RedirectsEvery old URL reaches its content in one hop, as a rule map and over real HTTP requests

Run it after every import rehearsal, and again against production after cutover with the same old URL list. A redirect rule that passes on staging still has to be requested on the server that will answer for the old domain.

A reproducible example

The example is a small synthetic migration: 3 authors and 12 articles in English and German, 10 of them published, 3 images, internal links, and 13 public URLs that all move to new patterns, one of them to a changed slug. A transform writes the import files with a dry run and safe reruns. A check compares them with the source and requests every old URL from a local server that applies the redirect rules. The export is hand-written and invented, and the example runs on Node.js with no dependencies.

npm run transform
npm run transform:write
npm run check
npm test

The first command is a dry run, the second writes the import files to target/, and npm run check writes its report to results/ and exits with code 1 when something blocks cutover.

The tests introduce one defect at a time and expect the check to report it, for the right record: a missing record, a duplicate ID, an author that does not exist, a missing image inside rich text, a translation in the wrong locale, a changed slug with no redirect, a redirect to a missing page, a loop, a two-hop chain, lost body text, lost SEO metadata, and a link left on an old URL. After each one the target is rebuilt from the source, and the same check has to pass again. On 1 October all 15 tests passed on Node.js 22.22.3 and 24.11.0, and switching off the loop detection made the loop test fail.

Five of those defects applied together produced this report:

CheckExpectedFound
English articles, published56
German articles, published54
Image references whose file exists96
Old URLs answering with their page over HTTP1311
Old URLs reaching their page in one hop1310

Ten findings blocked the cutover: two wrong counts, three image references to one missing file (two featured images and one inside a body), one wrong locale, a changed slug with no redirect, and an old URL redirecting to a page that no longer existed. The last two were each found in the rule map and again over HTTP. The redirect broke because the German article, moved into the English locale, took its URL with it. Three findings went to review: a two-hop redirect, seen in both checks, and a body link left on an old URL.

A passing fixture shows the check works. It says nothing about a real site’s readiness, where permissions, drafts inside the CMS, integrations, backups and editorial sign-off need checks of their own.

Download the migration check example with its fixture, scripts, tests and the reports from the recorded run. The README lists every command and what each check covers.

Two illustrative scenarios

Both are illustrative. They describe no client and no measured timings.

A marketing site of about 200 pages in one language, moving from WordPress to a headless CMS. The free Screaming Frog crawl covers the public URLs. wp export provides the content and the attachment URLs, with the media downloaded from the live site. The target’s importer, or a short script, maps posts and pages. The check covers counts, images, internal links and an exact redirect list that a person can read in one sitting.

A multilingual estate of about 12,000 URLs in five locales, where two content types merge into one. A crawl needs a licence or a crawler of the team’s own, fed by sitemaps. Extraction runs per locale through the source API, with its reported totals as the expected counts. The transform keeps an ID map, runs dry first and reruns until cutover. The check runs per locale, redirect patterns are requested over HTTP on staging and production, and each market’s editors accept their own content.

Tools at a glance

Costs were checked on 1 October 2026. “Tested” means this guide ran the tool; “documented” means the row comes from the vendor’s documentation.

ToolWhat it gives, and what it does notCost; status
Screaming Frog SEO Spider (discovery, redirect audit)Status codes, redirects, metadata and links of public URLs. It does not see drafts or the content model.Free up to 500 URLs, €245 a year licensed; documented
Sitemaps, Search Console page export (discovery)Published URLs the site or Google knows. They do not list drafts or the content model.Free; documented
WP-CLI wp export (extraction)WXR with every status and the attachment URLs. It does not include media files or site options.Free, open source; tested
WordPress REST API (extraction)JSON per collection, 100 records per request, totals in headers. Only published posts unless other statuses are requested.Free; documented
sanity dataset export (extraction)NDJSON documents and asset files in one tarball. A failed asset download only prints a warning.Free CLI, needs project access; documented
EmDash WordPress importer (import)Posts, pages, terms and media. No redirects, and internal links keep their old addresses.Free, open source; tested
Payload Local API (import)Writes from server-side code, with access control skipped unless told otherwise.Free, open source; documented
The example’s transform and checkAn ID map, a URL map and a report. Its parser reads only the fixture’s markup.Free, MIT; tested

Where the other WAYF pages fit

The CMS migrations hub collects WAYF’s platform guides and comparisons. The pre-migration checklist covers what a non-technical team prepares before the technical work starts. The CMS migration RFP template sets out requirements for proposals, and the CMS quote-comparison worksheet puts the resulting quotes side by side. For the move itself from one platform, see the guides for WordPress, Drupal and Sanity to Payload.

The check’s expected side comes from the source export and the team’s URL decisions, never from the target. That is what lets it catch a record or a URL the target dropped without saying so.


Author

Paul Utr

Co-founder, Chief Growth Officer

Paul has been launching online platforms since his teens, picking up UX and product design by building them. He led the Mailgun redesign at Netguru and was Principal Designer at Ramp Network through its seed-to-Series-B run. At WAYF he leads design and organisational alignment, and watches how language carries through every product we ship.


Rather have it done for you? WAYF runs CMS replatforming end to end.

We're booking content platform
engagements for 2026.

Twenty-five minutes to walk through the work and decide if we're the right team for it. Scoping and a fixed price come after.