Website Migration SEO Checklist: Before, During and After
A website migration SEO checklist for domain, HTTPS and platform moves: benchmark crawl, redirect map, launch checks and 30-day monitoring, with a script.
A website migration keeps its rankings when every old URL 301s in a single hop to its exact new equivalent, the new pages carry the same content, titles, canonicals and internal links, and nothing ships with noindex or a robots.txt block. Google's site move guide describes the same principles. The checklist is the same whether you're changing domain, moving to HTTPS or swapping platforms: benchmark crawl → redirect map → staging validation → launch-day checks → 30 days of monitoring.
Most sites already carry migration debt from past moves. On 10 October 2026 we tested the host and URL hygiene of 219 random homepages. 47.9% needed two or more redirect hops on at least one of their four host variants (http/https × www/apex). 14.2% of sites reachable on all four served the same homepage on two different hosts with no redirect. 31.6% answered an inner page at both its slash and no-slash URL. And 11.2% of URLs sampled from site sitemaps redirected. Every one of those is a leftover from a migration that wasn't finished.
This checklist covers each phase, the data behind it, and a script that validates your redirect map against the live site.
The Redirect Debt Most Sites Already Have
| Measurement (10 Oct 2026) | Result |
|---|---|
| Sites where all four host variants load | 155 of 219 (70.8%) |
| …serving two hosts without a redirect (duplicate www/apex) | 22 (14.2%) |
| Sites where some variant needs 2+ redirect hops | 105 of 219 (47.9%) |
| …3 or more hops | 14 (6.4%) |
First hop on http:// variants is temporary (302/307) | 45 of 399 (11.3%) |
Sites still serving a page over plain http:// | 7 (3.2%) |
| Unknown URL returns a real 404/410 | 161 (73.5%) |
| Unknown URL returns 200 (soft 404) | 14 (6.4%) |
| Unknown URL redirects to the homepage | 6 (2.7%) |
| Inner page answers on both slash and no-slash form (duplicate) | 50 of 158 (31.6%) |
| Sitemap URLs that redirect (25 sampled per site, 38 sites) | 11.2% |
Method. The 219 homepages are the random sample (Tranco 94GG2, ranks 1,001–50,000) from our website accessibility statistics. For each, we requested all four host variants and followed redirects hop by hop, requested a random non-existent path, and requested one internal page with its trailing slash added or removed. 64 sites had at least one variant blocked by a bot challenge or not resolving, so the convergence figures use the 155 that loaded on all four. The sitemap figure comes from our earlier structure study, which fetched sitemap URLs exactly as listed.
A two-hop chain on one variant isn't a crisis, but every migration you run on top of existing debt adds another hop. Fix the old chains while you're building the new map. (Our own site was one of the 47.9% when we wrote this: http://www. went to https://www. and then to the apex. The script below caught it.)
Phase 1: Before — Benchmark and Plan (2–6 Weeks Out)
- Crawl the old site completely and export every URL with status, title, meta description, H1, canonical, robots directives, word count and inbound internal links. This crawl is the contract the new site must honour.
- Export from Search Console and analytics: top pages by clicks, by conversions, and by external backlinks. These URLs need hand-checked redirects.
- Pull URLs from every source, not just the crawl: XML sitemaps, analytics landing pages, backlink exports and server logs. Orphan pages with traffic won't appear in a crawl.
- Decide the URL rules for the new site (host, HTTPS, trailing slash, lowercase, parameters) and write them down.
- Build the redirect map: one row per old URL,
old_url,new_url, to the closest equivalent — never everything to the homepage. - Record baselines: indexed pages, clicks by template, Core Web Vitals, and the full audit scores.
Redirect map rules
| Situation | Do this |
|---|---|
| Page exists on new site | 301 (or 308) to its exact new URL; see Google on redirects |
| Page merged into another | 301 to the page that absorbed its content |
| Page retired, has backlinks or traffic | 301 to the closest relevant page |
| Page retired, no value | Return 410 (or 404); don't redirect to the homepage |
| Old redirects already in place | Update them to point straight at the final URL (no chains) |
| Parameter / tracking URLs | Canonicalize; redirect only if they were indexed |
Phase 2: Staging Validation (1–2 Weeks Out)
- Protect staging with HTTP authentication, not robots.txt. Blocking crawlers doesn't stop URLs from being indexed, and robots.txt rules have a habit of shipping to production.
- Crawl staging with the same settings as the benchmark and compare: every benchmark URL should have a mapped equivalent with the same title, H1, canonical (pointing at the production URL) and comparable word count.
- Run the redirect map against staging (the script below takes any host).
- Check templates for content that only renders with JavaScript, missing structured data, changed internal links in navigation and footer, and image URLs that will 404.
- Prepare a production robots.txt and XML sitemap with the new URLs, and diff them against the staging versions.
Phase 3: Launch Day
Do the switch early in the week, with developers available, away from peak season.
- Deploy redirects before or with the new site, never after.
- Confirm production robots.txt doesn't contain
Disallow: /, and no template hasnoindex(check the HTTPX-Robots-Tagheader too). - Run the redirect map script against production: every row should show a single 301/308 to a 200.
- Test the four host variants and the trailing-slash rule.
- Spot-check the top 50 pages by traffic and by backlinks manually.
- Submit the new XML sitemap in Search Console. For a domain change, also verify the new property and use the Change of Address tool, and keep the old domain's redirects in place for at least a year.
- Confirm analytics and tag manager fire on the new site — and that consent still gates them.
Phase 4: After — 30 Days of Monitoring
| When | Check |
|---|---|
| Day 1–3 | Server logs and Search Console for 404 spikes; redirect map re-run; crawl of the new site vs the benchmark |
| Week 1 | Indexing report: new URLs being indexed, old URLs dropping; Core Web Vitals on new templates |
| Week 2 | Clicks and impressions by template vs baseline; fix any template that lost titles or content |
| Week 4 | Full re-audit with the benchmark settings; update internal links that still point at redirected URLs |
| Month 3+ | Keep redirects; remove internal links to old URLs; consider retiring the old sitemap |
Some fluctuation in the first weeks is normal while Google recrawls. A sustained drop on one template almost always traces back to something on the checklist: missing redirects, changed content, lost internal links or an indexing directive.
Validate the Redirect Map: Script
This script checks each row of your redirect map against the live (or staging) site. It follows redirects hop by hop and flags chains, temporary redirects, wrong destinations, redirects to the homepage, and targets that aren't 200, are noindex or canonicalize elsewhere.
#!/usr/bin/env python3
"""redirect_map_check.py: verify a migration redirect map, row by row, against the live site.
Usage:
python3 redirect_map_check.py redirects.csv [--workers 8]
redirects.csv columns: old_url,new_url (header row required; absolute URLs)
Standard library only. For each old URL it follows redirects hop by hop (no auto-follow) and checks:
a single permanent hop (301/308), that it lands exactly on new_url, that new_url answers 200, is not
noindex and canonicalises to itself, and that nothing redirects to the homepage (a soft-404 signal).
Exit code 1 if any row fails.
"""
import argparse, csv, re, ssl, sys, urllib.error, urllib.request
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import urljoin, urlsplit
UA = "Mozilla/5.0 (compatible; redirect-map-check/1.0)"
CTX = ssl.create_default_context()
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *a, **k):
return None
OPENER = urllib.request.build_opener(NoRedirect, urllib.request.HTTPSHandler(context=CTX))
def hop(url):
try:
with OPENER.open(urllib.request.Request(url, headers={"User-Agent": UA}), timeout=20) as r:
return r.status, None, r.read(1_000_000).decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Location"), ""
except Exception as e:
return type(e).__name__, None, ""
def check(row):
old, new = row["old_url"].strip(), row["new_url"].strip()
url, chain, problems = old, [], []
for _ in range(10):
status, loc, html = hop(url)
chain.append(str(status))
if status in (301, 302, 303, 307, 308) and loc:
url = urljoin(url, loc)
continue
break
hops = len(chain) - 1
codes = [int(c) for c in chain[:-1] if c.isdigit()]
if hops == 0:
problems.append("does not redirect" if status == 200 else f"returns {status}, no redirect")
if hops > 1:
problems.append(f"chain of {hops} hops")
if any(c in (302, 303, 307) for c in codes):
problems.append("temporary redirect (use 301/308)")
if hops and url.rstrip("/") != new.rstrip("/"):
problems.append(f"lands on {url}, expected {new}")
if hops and urlsplit(url).path in ("", "/") and urlsplit(new).path not in ("", "/"):
problems.append("redirects to the homepage (treated like a soft 404)")
if chain[-1] != "200":
problems.append(f"final status {chain[-1]}")
else:
robots = re.search(r"<meta[^>]+name=[\"']robots[\"'][^>]*content=[\"']([^\"']+)", html, re.I)
canon = re.search(r"<link[^>]+rel=[\"']canonical[\"'][^>]*href=[\"']([^\"']+)", html, re.I)
if robots and "noindex" in robots.group(1).lower():
problems.append("target is noindex")
if canon and urljoin(url, canon.group(1)).rstrip("/") != url.rstrip("/"):
problems.append(f"target canonicalises to {canon.group(1)}")
return old, " → ".join(chain), problems
def main():
ap = argparse.ArgumentParser()
ap.add_argument("csv")
ap.add_argument("--workers", type=int, default=8)
a = ap.parse_args()
rows = list(csv.DictReader(open(a.csv)))
with ThreadPoolExecutor(a.workers) as ex:
results = list(ex.map(check, rows))
bad = 0
for old, chain, problems in results:
bad += bool(problems)
print(f"{'✗' if problems else '✓'} {old} [{chain}]")
for p in problems:
print(f" - {p}")
print(f"\n{len(rows) - bad} of {len(rows)} redirects OK")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())We ran it on part of our own site's redirect map — three blog posts merged on 8 October 2026, a feature alias, a host variant and one URL that never existed:
old_url,new_url
https://bugviso.com/blog/how-often-should-you-do-an-seo-audit,https://bugviso.com/blog/how-often-should-you-audit-your-website-by-site-size-and-type
https://bugviso.com/blog/what-is-a-website-health-score-and-what-does-it-mean,https://bugviso.com/blog/website-health-score-explained-whats-a-good-number
https://bugviso.com/blog/core-web-vitals-explained-lcp-inp-and-cls-in-plain-english,https://bugviso.com/blog/what-are-core-web-vitals-explained-simply
http://www.bugviso.com/pricing,https://bugviso.com/pricing
https://bugviso.com/features/wcag,https://bugviso.com/features/wcag-accessibility-mobile
https://bugviso.com/old-landing-page-that-never-existed,https://bugviso.com/scan✓ https://bugviso.com/blog/how-often-should-you-do-an-seo-audit [308 → 200]
✓ https://bugviso.com/blog/what-is-a-website-health-score-and-what-does-it-mean [308 → 200]
✓ https://bugviso.com/blog/core-web-vitals-explained-lcp-inp-and-cls-in-plain-english [308 → 200]
✗ http://www.bugviso.com/pricing [308 → 308 → 200]
- chain of 2 hops
✓ https://bugviso.com/features/wcag [308 → 200]
✗ https://bugviso.com/old-landing-page-that-never-existed [404]
- returns 404, no redirect
- final status 404
4 of 6 redirects OKThe merges and the feature alias are single 308 hops to a 200, as they should be. The host variant shows the two-hop chain from the data table, now on our fix list, and the never-existing URL correctly returns 404. If it had been a real old page with backlinks, that row would be a missing redirect. Run the script on the full map before launch, on launch day and again after a week.
How BugViso Supports a Migration
- Benchmark and re-crawl: a multi-page crawl (homepage links first, then the whole sitemap tree) exports a per-page CSV with status code, crawl depth, title and meta description (with lengths), first H1 and H1 count, word count, canonical URL and whether it matches, indexability, LCP/FCP/TTFB, page size, console errors, accessibility violations, internal/external/broken link counts and schema entities. Export the benchmark crawl and the post-launch crawl and diff them in a spreadsheet.
- Host and canonical checks: HTTPS enforcement with the redirect status code, canonicals pointing at the other www/apex host, protocol mismatches, and subpages canonicalizing to the homepage.
- Re-audit with identical settings from your audit records after launch, and scheduled weekly scans for the 30-day monitoring window.
- Protected-site detection: if a WAF or bot challenge blocks the crawler on the new infrastructure, the scan stops and says so instead of scoring a challenge page.
Crawls are capped by plan (75 to 300 pages), so for large sites crawl the top templates and sections, and use the script for the full redirect map. BugViso doesn't produce an automatic before/after diff, so compare the exported CSVs. You can benchmark your site with a BugViso crawl before the move; crawl limits and exports are on the site crawl and white-label reports page. For the launch itself, see the website launch checklist, and for chains, redirect chain audits.
High-Risk Oversights
- Redirecting everything to the homepage. Google treats mass homepage redirects like soft 404s, so the old pages' value is lost.
- Shipping staging's robots.txt or
noindex. It's the fastest way to deindex a site. Check headers as well as HTML. - Forgetting images and PDFs. They rank and get linked too. Include them in the map.
- Chains from old migrations. An old
http→wwwredirect plus a new domain redirect makes three hops. Point every redirect at the final URL. - Removing redirects after a few months. Keep them for at least a year after a domain change, ideally permanently.
- Changing content and URLs at the same time. When rankings move you can't tell which change caused it. If you can, migrate first and redesign later.
FAQ
What is a website migration in SEO?
Any change that alters URLs, hosts or how pages are served: a domain change, an HTTP-to-HTTPS move, a platform or CMS switch, a URL structure change, or a redesign that changes templates and content.
How long does it take to recover rankings after a migration?
For a clean migration with complete one-hop redirects and unchanged content, rankings usually settle within a few weeks as Google recrawls. Losses that persist beyond that point to missing redirects, changed content or indexing directives.
Should I use 301 or 302 redirects for a migration?
Permanent redirects: 301, or 308 to preserve the request method. In our data 11.3% of first hops on http:// variants were temporary 302/307 redirects. Use temporary redirects only for genuinely temporary moves.
Do redirect chains hurt SEO?
Each extra hop adds latency and a point of failure, and long chains may not be followed to the end. Almost half the homepages we tested (47.9%) needed two or more hops on some host variant. Point every redirect directly at the final URL.
Conclusion
Migrations lose rankings through small leaks — chains, homepage redirects, stray noindex, missing old URLs — so benchmark first, map every URL to its exact equivalent, validate the map on staging and production, and re-crawl with identical settings with a BugViso audit for the first 30 days.
See where your site stands
Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.