Migrations fail in two ways: a URL nothing redirects to, or a redirect pointing somewhere unhelpful. This is the sequence we work through to catch both before go-live, with the inventory sources, the redirect map rules, and a script that verifies every row on staging instead of sampling.
Watch
Migrations go wrong in one of two ways: a URL that nothing redirects to, or a redirect that points somewhere unhelpful. Almost everything below exists to catch those two before they are live, because after launch you are diagnosing rather than preventing.
This is the sequence we work through. Each item is the check and the fix together, because a checklist that only lists worries is not usable at 6am on launch day.
Before anything: build a complete URL inventory

A crawl alone is not an inventory. It finds what is linked, which excludes exactly the pages most likely to be quietly dropped: old landing pages, PDFs, pages reachable only from an email campaign. Pull from four sources and merge.
# 1. crawl -> Screaming Frog, export Internal > HTML > Address
# 2. sitemaps -> every URL the site declares
curl -s https://example.com/sitemap.xml | grep -oP '(?<=<loc>)[^<]+' > sitemap.txt
# 3. server logs -> anything requested in the last 90 days, incl. unlinked
awk '{print $7}' access.log | sort -u > requested.txt
# 4. Search Console -> Performance, export all pages with impressions
cat crawl.txt sitemap.txt requested.txt gsc.txt | sort -u > inventory.txt
wc -l inventory.txt
The log export is the one people skip and it is the one that finds the surprises. A URL with no internal links and steady traffic from an old newsletter exists only there.
Record current performance at the same time: rankings for your top queries, impressions and clicks per page, and the crawl stats baseline. Without a before, you cannot tell a migration dip from a seasonal one afterwards.
Map every old URL to exactly one new URL
Two columns, one row per URL from the inventory, no blanks. The rules that matter:
- One hop. Old URL goes straight to its final destination. Chaining through an intermediate is the most common way a clean map becomes a slow one.
- Relevance over convenience. Redirecting a retired page to the homepage is treated as a soft 404. If there is no equivalent page, a 410 is more honest than a redirect to somewhere unrelated.
- Permanent, not temporary. A 302 tells Google to keep the old URL indexed, which is the opposite of what a migration wants. The distinction between the two status codes is one of the few places where picking the wrong number has a lasting cost.
- Preserve query strings where they carry meaning, and strip them where they do not.
On nginx, a map block handles thousands of rules without a regex per line and is much faster than the alternative:
map $request_uri $redirect_target {
default "";
/old-page/ /new-page/;
/old-category/thing/ /products/thing/;
}
server {
if ($redirect_target != "") {
return 301 $redirect_target;
}
}
Test the redirects on staging, before launch

This is the step that separates migrations that go quietly from the ones that need a recovery plan. Point the map at staging and check every row automatically. Do not sample.
#!/usr/bin/env bash
# redirect-map.csv: old_path,expected_new_path
while IFS=, read -r old new; do
read -r code loc < <(curl -s -o /dev/null
-w '%{http_code} %{redirect_url}' "https://staging.example.com${old}")
if [ "$code" != "301" ]; then
echo "FAIL status $old -> $code"
elif [ "${loc#*staging.example.com}" != "$new" ]; then
echo "FAIL target $old -> $loc (expected $new)"
fi
done < redirect-map.csv
Every line it prints is a page you were about to lose. Run it again against production within minutes of go-live, because staging and production rarely have identical server config, and the difference is usually discovered the hard way.
While you are on staging, confirm the two things that most often ship by accident: that robots.txt is not still disallowing everything, and that no page carries a leftover noindex.
curl -s https://staging.example.com/robots.txt
curl -sI https://staging.example.com/some-page/ | grep -i x-robots-tag
curl -s https://staging.example.com/some-page/ | grep -i 'name="robots"'
A staging site blocked by Disallow: / that goes live unchanged is the single most expensive migration mistake there is, and it is entirely preventable by one curl after cutover.
Check parity, not just status codes
A migration can score 100% on redirects and still lose visibility, because the destination pages are not carrying what the old ones carried. Redirects preserve the address. They do not preserve the page.
Crawl old and new, then diff on the fields that actually rank:
- Title and meta description. A CMS migration that rebuilds titles from a template silently rewrites every one of them.
- H1 and body word count. A large drop on a template usually means a content field did not map across.
- Canonical tags. Watch for a new site canonicalising everything to the homepage or to the staging hostname.
- Internal links. Links in body content often still point at old paths, so every one takes a redirect hop. Find and update them at the source rather than relying on the map.
- Structured data. Whatever schema the old templates emitted, confirm the new ones still do.
# internal links still pointing at the old structure, post-launch
grep -oP 'href="https://example.com/old-[^"]*"' crawl-new.html | sort | uniq -c | sort -rn
Google's documentation on site moves with URL changes is worth reading alongside this, particularly its point that you should move in stages where the site is large enough to make that possible. Migrating a section at a time turns one unrecoverable event into several small ones.
Launch day, in order

- Drop DNS TTL to 300 seconds at least 24 hours ahead, so a rollback is minutes rather than hours. Put it back afterwards.
- Cut over, then immediately re-run the redirect test against production.
- Fetch the homepage and three deep pages and check status, canonical and robots directives by hand.
- Confirm analytics and tag manager fire on the new templates. Broken tracking makes every later question unanswerable.
- Submit the new sitemap in Search Console, and leave the old sitemap accessible for a while so the old URLs get recrawled and their redirects discovered.
- File the Change of Address in Search Console if the domain changed. Not needed for a URL restructure on the same host.
Point 5 is counterintuitive and worth stating plainly: keeping the old sitemap live for a few weeks helps, because it feeds Google the exact list of URLs you want recrawled so it can see the 301s.
What to expect afterwards
An honest expectation, since most migration content avoids this: a well-executed migration usually still costs some visibility for a period while Google recrawls and reassigns signals. The goal is a shallow dip that recovers, not no dip at all. Anyone promising zero movement is selling something.
Keep the redirects permanently. There is no point at which removing them becomes safe if the old URLs still have inbound links, and the cost of leaving a map block in place is effectively nothing.
If a drop appears and you are unsure whether the migration caused it, separating one cause from another is its own diagnostic.
Frequently asked questions
How to migrate a website without losing SEO?
Build a complete URL inventory from crawl, sitemaps, logs and Search Console, map every old URL to one new URL with a single-hop 301, and verify the whole map automatically on staging and again on production. Most losses trace back to a URL nobody knew existed rather than to a redirect that was written incorrectly.
What is a migration checklist?
An ordered set of checks run before, during and after a site change that alters URLs, hosting, protocol or platform. A usable one pairs each check with the command or setting that fixes it, and is ordered so that the irreversible steps come after the verifiable ones.
What is SEO migration?
Any change that moves the addresses search engines have already indexed: a new domain, HTTP to HTTPS, a URL restructure, a platform replacement, or a redesign that changes templates and internal links. The common factor is that existing URLs stop resolving as they did, which is why redirect mapping is the core of the work rather than a final step.
If a migration is scheduled and you want the inventory and redirect map checked by someone who is not the person who built it, that is the pre-launch review we run.