SEO for Site Migrations: A URL Migration Checklist That Keeps Your Rankings
How to inventory every URL, build a one-to-one redirect map in a spreadsheet, implement it in nginx, Apache or Next.js, and monitor Search Console until Google has moved everything across.
A site migration keeps its rankings when every old URL that had traffic or links returns a single 301 to its closest new equivalent, the new site's canonicals, internal links, sitemaps and structured data all point at the new URLs, and the redirects stay in place for at least a year.
Most migration traffic losses come from the same short list: URLs nobody mapped, redirects that all go to the homepage, chains of three or four hops, a staging noindex that went live, and a new site that still links to old URLs. Every one of them is preventable with a spreadsheet and a test run before launch.
This guide is the checklist in order: what to collect before you touch anything, how to build the redirect map, working redirect code for the common servers, what to do on launch day, and what to watch in Search Console for the following months.

Types of migration and what each one risks
| Migration | Example | URLs change? | Main risk |
|---|---|---|---|
| Domain move | oldbrand.com → newbrand.com | Host only | Losing link equity if any path is not redirected |
| URL structure | /blog/2023/05/post → /guides/post | Paths | Unmapped URLs, pattern rules that miss edge cases |
| Platform or CMS | WordPress → Shopify or Next.js | Often, silently | New platform forces its own URL patterns, drops content or metadata |
| HTTPS | http:// → https:// | Protocol | Mixed content, canonicals still on HTTP, redirect loops |
| Merge or consolidation | Two sites or subdomains into one | Host and paths | Many-to-one mapping, duplicate topics competing |
Combine as few as you can. A domain move plus a new URL structure plus a new CMS on the same day makes any drop impossible to diagnose. If you must, at least keep paths identical across the domain change so the redirect is a single pattern rule.
Pre-migration checklist
The goal is a complete list of old URLs that matter, with enough data on each one to prioritise. No single source has them all, so combine:
A full crawl of the current site with Screaming Frog, Sitebulb or similar. Export every HTML URL with status, title, canonical and inlinks.
Search Console Performance, 16 months, by page. The interface exports 1,000 rows; use the API or a connector for more.
GA4 landing pages for the last 12 months, all channels, so you also protect referral, email and direct traffic.
Backlinks: Search Console's Links report (Top linked pages) plus a third-party backlink tool. Pages with external links but no traffic are easy to miss and still carry value.
XML sitemaps and, if you have them, server logs, which show URLs Googlebot still requests that no crawl would find.
Existing redirects from the current server config, CMS redirect plugin or CDN. Old redirects must be re-pointed to the new final URLs, not dropped.
Benchmark before launch so you can measure the result: organic clicks and impressions per page and per section, indexed page count from the Page indexing report, ranking positions for your top 100 to 200 queries, and Core Web Vitals per template. Save the crawl as well; you will diff against it later.
Prepare the new site: keep the staging site blocked with HTTP authentication rather than robots.txt or noindex, so nothing leaks into the index and nothing blocking gets copied to production. Carry over titles, meta descriptions, headings, body copy, image alt text and structured data for every page you are keeping.
URL mapping, step by step
The redirect map is the core deliverable. Build it as a spreadsheet with one row per old URL:
| Column | What goes in it |
|---|---|
old_url | Full old URL, normalised (lowercase host, consistent trailing slash) |
sources | Where it was found: crawl, GSC, GA4, backlinks, sitemap, logs |
clicks_12m / ref_domains | Organic clicks and linking domains, for priority |
new_url | The single destination, as a final 200 URL |
match_type | exact slug, rewritten, consolidated, parent category, retired (410) |
status_tested / hops | Result of the test run: final status and number of redirects |
owner / notes | Who signed off; reasons for non-obvious matches |
Rule 1: one-to-one wherever a matching page exists. Each old URL redirects to the new page that serves the same intent. A product goes to the same product, an article to the same article.
Rule 2: no equivalent means closest relevant parent, or 410. A discontinued product can go to its category if the category meets the same need. If nothing on the new site is relevant, return 404 or 410. Google treats mass redirects to the homepage as soft 404s, so they do not pass value anyway.
Rule 3: every redirect points at a final URL. No chains. If /a already redirected to /b, and /b now moves to /c, update /a to go straight to /c. Googlebot follows a limited number of hops, and every extra hop slows crawling.
How to match old to new quickly. Export the new site's URLs (from the staging crawl or the CMS) to a second tab. Then match in passes:
1. Exact slug match: extract the last path segment from both lists
=REGEXEXTRACT(A2, "([^/]+)/?$")
=XLOOKUP(C2, New!C:C, New!A:A, "")
2. Title match for rows still empty: XLOOKUP the old page title
against new page titles.
3. Pattern rules for whole sections, e.g.
/blog/YYYY/MM/slug → /guides/slug
4. Manual review: sort the remaining rows by clicks_12m and
ref_domains, and map the top few hundred by hand.
Have someone who knows the content sign off on the top pages; automated matches on slugs like /pricing or /about are usually right, but similar product names are often wrong. Pattern rules keep the server config short; the spreadsheet still needs one row per URL so you can test each one.
Normalise variants. Decide one canonical form for the new site (HTTPS, www or not, trailing slash or not, lowercase), and redirect every variant of the old URL in one hop: http://oldsite.com/Page should reach the new final URL directly, not via three normalisation steps.
Redirect code: nginx, Apache, Next.js and Vercel
Use permanent redirects: 301 or 308. Both pass signals the same way for Google; 308 also preserves the request method, which matters only for form posts and APIs.
nginx, using a map for a long list of one-to-one redirects plus a domain rule. The map block goes in the http context; large maps may need a higher map_hash_bucket_size.
map $uri $new_uri {
default "";
/blog/2023/05/site-speed-tips /guides/site-speed;
/products/blue-widget-v1 /products/blue-widget;
/old-pricing /pricing;
}
server {
listen 443 ssl;
server_name www.example.com;
if ($new_uri) {
return 301 $new_uri;
}
# pattern rule for a whole section
location ~ ^/blog/\d{4}/\d{2}/(.+)$ {
return 301 /guides/$1;
}
}
# domain move: every path to the same path on the new host
server {
listen 443 ssl;
server_name oldbrand.com www.oldbrand.com;
return 301 https://www.newbrand.com$request_uri;
}
Apache .htaccess with mod_rewrite. Pick either RewriteRule or Redirect for a site and stick to it; mixing mod_rewrite and mod_alias rules leads to confusing order-of-execution bugs.
RewriteEngine On
# HTTP to HTTPS and non-www to www in one hop
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
# one-to-one redirects
RewriteRule ^old-pricing/?$ /pricing [R=301,L]
RewriteRule ^products/blue-widget-v1/?$ /products/blue-widget [R=301,L]
# pattern rule for a whole section
RewriteRule ^blog/[0-9]{4}/[0-9]{2}/(.+)$ /guides/$1 [R=301,L]
Put the specific rules above broad patterns if they overlap, since [L] stops at the first match. For thousands of one-to-one rows, a RewriteMap in the main server config is faster than a long .htaccess.
Next.js (next.config.js). permanent: true sends a 308.
module.exports = {
async redirects() {
return [
{ source: '/old-pricing', destination: '/pricing', permanent: true },
{ source: '/blog/:year(\\d{4})/:month(\\d{2})/:slug',
destination: '/guides/:slug', permanent: true },
];
},
};
Vercel (vercel.json), for any framework deployed there:
{
"redirects": [
{ "source": "/old-pricing", "destination": "/pricing", "permanent": true },
{ "source": "/blog/:year/:month/:slug", "destination": "/guides/:slug", "permanent": true }
]
}
Vercel caps how many redirects a deployment can define in config; check the current limit in its docs before pasting thousands of rows. For very large maps, load the spreadsheet as a JSON lookup in middleware and return a 301 or 308 from there.
Testing the redirect map before launch
Deploy the redirects to staging and test every row, not a sample. In Screaming Frog, use list mode with the old URLs and enable "Always follow redirects" to report the final URL and the number of hops. Or from a terminal:
while read -r url; do
printf '%s\t' "$url"
curl -s -o /dev/null -L --max-redirs 10 \
-w '%{http_code}\t%{num_redirects}\t%{url_effective}\n' "$url"
done < old-urls.txt > redirect-test.tsv
Paste the output back into the sheet and flag any row where the final status is not 200, hops is more than 1, or the final URL does not equal new_url. Fix and retest until the flagged list is empty.
Launch-day checklist
Remove blocks: staging authentication off, no noindex in meta tags or X-Robots-Tag headers, production robots.txt in place and not blocking the old URLs (Google must crawl them to see the redirects).
Redirects live: rerun the test script against production on the top 500 URLs within the first hour.
Canonicals: every page's rel="canonical" points at its own new URL, not the old one and not staging.
Internal links: navigation, body links, breadcrumbs and footers link to new URLs directly. Linking through redirects works but wastes crawl and sends a mixed message about which URL is canonical.
Structured data: url, @id, item in breadcrumbs, logo and sameAs values point at the new URLs. Schema that still references old, redirecting URLs tells Google two different things about where the page lives.
hreflang annotations use new URLs on every language version.
Sitemaps: new XML sitemap with only new, 200, indexable URLs, referenced in robots.txt and submitted in Search Console.
Verification and analytics: Search Console verified for the new property, GA4 and tag manager firing on the new templates.
The Change of Address tool (domain moves only)
If you moved to a new domain or subdomain, use Search Console's Change of Address tool: open the old property, go to Settings → Change of address, and choose the new property. Both properties must be verified by you, and the old homepage and a sample of pages must 301 to the new site, or the validation step fails.
Do not use it for HTTP to HTTPS moves, www to non-www changes, or path changes on the same domain; those are handled by redirects alone. Bing Webmaster Tools has an equivalent Site Move feature for domain changes.
Post-launch monitoring
Days 1 to 7: check the Search Console Page indexing report for the new property daily. Watch for "Not found (404)" spikes, "Blocked by robots.txt", "Excluded by 'noindex' tag" and "Redirect error". On the old property, "Page with redirect" should grow as Google recrawls; that is expected. Use URL Inspection on your top 20 pages to confirm Google picked the new URL as canonical.
Weeks 2 to 8: compare clicks per section against the benchmark. Crawl the live site weekly for internal links to redirected URLs and for any chains. Check the Sitemaps report: submitted versus indexed counts for the new sitemap should converge. Review server logs or the Crawl stats report for Googlebot hitting 404s on old URLs you missed, and add those to the map.
Months 2 to 12: keep monitoring 404s, because old backlinks and bookmarks keep surfacing URLs.
Google's site move guidance says to keep redirects in place for as long as possible, and at least a year. In practice, keep them indefinitely: external links and old citations do not update themselves, and a redirect rule costs nothing to keep. Never let the old domain registration lapse after a domain move.
Some fluctuation is normal while Google recrawls. Google says a medium-sized site can take a few weeks for most pages to move, and larger sites longer. A drop that is still growing after a month, or is concentrated in one section, points to a mapping problem rather than normal reprocessing.
Why traffic drops after a migration
| Symptom | Likely cause | Fix |
|---|---|---|
| Whole site drops within days | noindex or robots.txt block copied from staging | Remove it, request indexing on key pages, resubmit sitemap |
| 404 spike in Page indexing | Unmapped URLs (often from backlinks or logs) | Add rows to the map; redirect to the closest equivalent |
| Old URLs still ranking weeks later | Redirects are 302, chained, or old URLs blocked in robots.txt | Make them single-hop 301s and allow crawling |
| Google picks the old URL as canonical | Canonicals, internal links, sitemaps or schema still reference old URLs | Point every signal at the new URL |
| One section collapses | Pattern rule wrong for that folder, or content trimmed on the new template | Test that section's rows; restore missing content |
| Long-tail traffic falls, head terms fine | Retired pages redirected to the homepage or categories | Rebuild the useful pages or map them one-to-one |
| Rankings hold but conversions fall | Slower templates, broken tracking | Compare Core Web Vitals and GA4 events per template |
Migrations and AI citations
ChatGPT, Perplexity and Google's AI Overviews cite specific URLs, and those citations can lag behind your migration. Clean single-hop redirects keep old cited links working, and consistent canonicals plus updated structured data help engines settle on the new URL. After launch, run your key pages through the AEO scanner to confirm AI crawlers can fetch the new templates, and check whether AI answers now cite the new URLs or keep citing the old ones with the AI visibility tracker.
Mistakes to avoid
Mapping only what the crawler found. Orphan pages with backlinks and URLs in the logs are missed by crawls. Combine all sources.
Redirecting everything to the homepage. Google treats it as a soft 404, and users land somewhere irrelevant.
Leaving old redirects pointing at old destinations. That creates chains. Update every existing redirect to the new final URL.
Blocking old URLs in robots.txt. Google cannot see a redirect on a URL it is not allowed to crawl.
Removing redirects after a few months. Links and citations keep arriving for years.
Changing everything at once. Domain, URLs, design and content in one release leave you nothing to roll back to. See SEO for web redesigns for the content side, and XML sitemaps for sitemap setup.