Google's guidance for moving URLs: map every old URL to its new URL, use server-side permanent redirects (301 or 308), keep them for at least a year, submit the new sitemap, and expect temporary ranking fluctuation, with small to medium sites taking a few weeks. The step to add is testing: follow every old URL and flag temporary redirects, chains, 404s and redirects to the homepage.

Key takeaways

  • Map every old URL to its new equivalent. Google says it is important to map the old site's URLs to the new site's, and to use server-side permanent redirects, such as 301 or 308.
  • Keep the redirects for at least a year. Google's guidance is as long as possible, generally at least one year.
  • Permanent and temporary redirects are treated differently. A permanent redirect is a signal that the target should be canonical; a temporary one is not used as that signal.
  • Expect fluctuation. Google says a small to medium site can take a few weeks for most pages to move, and rankings may fluctuate temporarily.
  • The common failures are testable. A 302 left in place, a two-hop chain, a 404, a loop, and a redirect to the homepage all show up in one pass of a script.
  • Do not block the old URLs in robots.txt. A crawler that is not allowed to fetch an old URL cannot see the redirect on it.

What Google documents

From Google's site-move and redirect documentation.
Guidance Status Basis
Map the old site's URLs to the new site's URLsDocumentedGoogle's site-move guide
Use server-side permanent redirects (301, 308)DocumentedSame guide
Keep redirects as long as possible, generally at least 1 yearDocumentedSame guide
Submit the new sitemap in Search ConsoleDocumentedSame guide
The Change of Address tool is only for moving between domains or subdomains, not HTTP to HTTPS or www changesDocumentedSame guide
A small to medium site can take a few weeks; expect temporary fluctuationDocumentedSame guide
A permanent redirect is a canonical signal; a temporary one is not used as that signalDocumentedGoogle's redirect documentation
JavaScript redirects are a last resort, since rendering can failDocumentedSame documentation
Redirecting removed pages to the homepage is a mistake; use the closest equivalent or a 404PracticeCommon advice; Google's redirect page does not address it

The last row is flagged because it is practice, not documentation, and so is the hop count: Google's redirect page does not give a limit on chains, so the sensible rule is simply to avoid them, since every hop is another request that can fail.

Building the map

  1. Inventory the old URLs. Combine the sitemap, the Search Console Performance export (pages with impressions), your analytics landing pages, a backlink report, and the server logs. A URL that only appears in one of those is easy to forget and still carries links or traffic.
  2. Map one to one. Each old URL goes to its closest equivalent on the new site. A page that no longer has an equivalent should return a 404 or 410 instead of redirecting to the homepage.
  3. Implement it on the server. A rules file or a redirect table at the host or CDN, not JavaScript and not a meta refresh.
  4. Update everything that points at the old URLs. Internal links, canonical tags and the sitemap should use the new URLs directly; redirects are for outside links and old bookmarks, not for your own navigation.
  5. Change as little else as possible. A move that also rewrites content and redesigns the site leaves you unable to tell what caused any ranking change.

Testing the map

Put the old URLs in a text file, one per line, optionally followed by a comma and the URL it should end up at. This script follows each one hop by hop and classifies the result:

import sys, urllib.request, urllib.parse, urllib.error

MAX_HOPS = 10

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None

opener = urllib.request.build_opener(NoRedirect)

def hop(url):
    req = urllib.request.Request(url, headers={'User-Agent': 'redirect-check/1.0'})
    try:
        with opener.open(req, timeout=20) as r:
            return r.status, None
    except urllib.error.HTTPError as e:
        return e.code, e.headers.get('Location')
    except Exception:
        return 0, None

def follow(url):
    chain, seen = [], set()
    while len(chain) <= MAX_HOPS:
        status, loc = hop(url)
        chain.append((status, url))
        if status in (301, 302, 303, 307, 308) and loc:
            nxt = urllib.parse.urljoin(url, loc)
            if nxt in seen:
                return chain, 'LOOP'
            seen.add(url)
            url = nxt
        else:
            return chain, None
    return chain, 'TOO MANY HOPS'

def same_page(a, b):
    def n(u):
        p = urllib.parse.urlparse(u)
        path = p.path[:-len('index.html')] if p.path.endswith('index.html') else p.path
        return (p.netloc.lower().removeprefix('www.'), path.rstrip('/'))
    return n(a) == n(b)

def verdict(old, chain, problem, home, expected):
    final_status, final_url = chain[-1]
    redirects = chain[:-1]
    if problem:
        return problem
    if final_status != 200:
        return 'BROKEN (%s)' % final_status
    if any(s in (302, 303, 307) for s, _ in redirects):
        return 'TEMPORARY redirect'
    if same_page(final_url, home) and not same_page(old, home):
        return 'REDIRECTS TO HOME'
    if expected and final_url.rstrip('/') != expected.rstrip('/'):
        return 'WRONG TARGET (expected %s)' % expected
    if len(redirects) >= 2:
        return 'CHAIN (%d hops)' % len(redirects)
    return 'ok' if redirects else 'ok (no redirect)'

home = sys.argv[2]            # e.g. https://example.com/
for line in open(sys.argv[1]):
    line = line.strip()
    if not line or line.startswith('#'):
        continue
    old, _, expected = [p.strip() for p in line.partition(',')]
    chain, problem = follow(old)
    print('%-22s %s' % (verdict(old, chain, problem, home, expected), old))
    print('    ' + ' -> '.join('%s %s' % c for c in chain))

Run it with the command below. Here is its output on a local test server with eight planted cases, one of each kind:

python3 check_redirects.py old_urls.txt https://example.com/
Script verdicts on a local test server with planted cases.
Case What happened Verdict
Correct redirect301, then 200ok
Temporary redirect left in place302, then 200TEMPORARY redirect
Two-hop chain301, 301, then 200CHAIN (2 hops)
Page that no longer exists404BROKEN (404)
Old page sent to the homepage301 to /REDIRECTS TO HOME
Redirect loop301 between two URLsLOOP
Redirect to the wrong page301, but not to the expected URLWRONG TARGET
URL that does not redirect200ok (no redirect)

The same test on this site

Run against this site's live URLs on 30 October 2026, it gave these results. The host strips .html and /index.html with permanent redirects, and sends HTTP to HTTPS:

Live results, 30 October 2026. The host is Cloudflare.
URL requested What it did Verdict
http:// home page301 to httpsok
/seo/seo-audit-checklist.html over https301 to the clean URLok
/hosting/index.html301 to /hosting/ok
/seo/seo-audit-checklist.html over http://301, 301, then 200CHAIN (2 hops)
https://www. home page200, no redirectDuplicate host

Two findings. An old HTTP link to an article takes two hops, because the protocol fix and the .html fix are separate rules. And the www host serves the page itself rather than redirecting to the main domain, which leaves two hosts serving the same pages; the canonical tags are what keep them from competing, and a redirect would be a clearer single signal. The script's "ok (no redirect)" does not mean that is fine, only that it is not a redirect problem. The canonical guide covers what these mismatches do to the signals Google weighs.

What to avoid

  • Blocking the old URLs in robots.txt. A crawler that cannot fetch an old URL never sees the redirect on it, the same trap as in the noindex versus disallow guide.
  • Leaving 302s behind. A temporary redirect is not used as a canonical signal, so the old URL can stay in the picture.
  • Redirecting everything to the homepage. The script flags it; a page with no equivalent should return a 404 or 410.
  • Removing the redirects early. Google's guidance is at least a year.
  • Relying on redirects for your own links. Point internal links, canonicals and the sitemap at the new URLs.

After the move

Submit the new sitemap and, only if you changed domain or subdomain, use the Change of Address tool. Then watch the Page indexing report and the Crawl stats report. If new URLs show up as Discovered, currently not indexed or Crawled, currently not indexed, those guides have the checks. For the infrastructure side of a move, see the zero-downtime VPS migration and switching hosts guides, which cover DNS and overlap windows. Expect a few weeks for a small or medium site and some temporary movement in the meantime.

What this does not cover

The script checks HTTP behaviour, not content. It cannot tell whether the new page is a good equivalent of the old one, which is a judgment you make when mapping. The live table is a snapshot of one host on one day. No ranking outcome is promised: Google says to expect temporary fluctuation, and it publishes no guarantee about the result.
Which URLs belong in the map? To decide which URLs should be redirected in the first place, see delete, merge, redirect or keep. If the URLs stay the same but the design changes, the safer check is a before-and-after diff of the two builds.

Badri Dutta

Software engineer · 8 years technical SEO

Written after running the redirect check on this site's own live URLs and finding a two-hop chain and a duplicate host. Most migration guides stop at 'use 301s' and never say how to check you did.

Full background →