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.

BugViso

13 min read

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 load155 of 219 (70.8%)
…serving two hosts without a redirect (duplicate www/apex)22 (14.2%)
Sites where some variant needs 2+ redirect hops105 of 219 (47.9%)
…3 or more hops14 (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/410161 (73.5%)
Unknown URL returns 200 (soft 404)14 (6.4%)
Unknown URL redirects to the homepage6 (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

SituationDo this
Page exists on new site301 (or 308) to its exact new URL; see Google on redirects
Page merged into another301 to the page that absorbed its content
Page retired, has backlinks or traffic301 to the closest relevant page
Page retired, no valueReturn 410 (or 404); don't redirect to the homepage
Old redirects already in placeUpdate them to point straight at the final URL (no chains)
Parameter / tracking URLsCanonicalize; 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 has noindex (check the HTTP X-Robots-Tag header 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

WhenCheck
Day 1–3Server logs and Search Console for 404 spikes; redirect map re-run; crawl of the new site vs the benchmark
Week 1Indexing report: new URLs being indexed, old URLs dropping; Core Web Vitals on new templates
Week 2Clicks and impressions by template vs baseline; fix any template that lost titles or content
Week 4Full 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.

python
#!/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:

text
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
text
✓ 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 OK

The 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 → www redirect 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.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.