How to check a site migration redirect map before Google does
A site migration is the moment you are most likely to lose search traffic. The new site can be perfect and still bleed rankings for months if the old URLs do not land on the right new pages. The redirect map (old URL ->
A site migration is the moment you are most likely to lose search traffic. The new site can be perfect and still bleed rankings for months if the old URLs do not land on the right new pages. The redirect map (old URL -> new URL) usually lives in a spreadsheet, and nobody checks every line after launch.
Here is what to check, how to do it with free tools first, and how to run the same checks in bulk.
The five problems to look for
1. Missing redirects. The old URL returns 404. Every backlink pointing to it is wasted.
2. Redirects to the wrong page. The classic mistake is sending everything to the home page. Google tends to treat that as a soft 404, so the old page's value is not passed on.
3. Chains. old -> http://new -> https://new -> https://new/ is three hops. Each hop adds latency, and crawlers stop following after a while. A migration is the right time to flatten every chain to one hop.
4. Temporary redirects. A 302 or 307 says "this move is not permanent". For a migration you want 301 or 308.
5. Downgrades and loops. A hop from HTTPS back to HTTP, or a chain that comes back to a URL it already visited.
Option A: curl for a few URLs
For one URL, curl shows every hop:
curl -sIL -o /dev/null -w "%{http_code} %{num_redirects} %{url_effective}\n" http://example.com/old-page
And curl -sIL http://example.com/old-page | grep -iE "^(HTTP|location)" prints the status and Location of each hop. Note that -I sends a HEAD request, and some servers answer HEAD differently from GET.
Option B: a short Python script for the whole map
For a real map, read the spreadsheet as CSV and follow each chain yourself, one hop at a time, so you see every status code:
import csv
import requests # pip install requests
from urllib.parse import urljoin
def chain(url, max_hops=10):
hops = []
while len(hops) < max_hops:
r = requests.get(url, allow_redirects=False, stream=True, timeout=15)
r.close() # headers only, do not download the body
hops.append((r.status_code, url))
if r.status_code not in (301, 302, 303, 307, 308):
break
url = urljoin(url, r.headers["Location"])
return hops
for old, expected in csv.reader(open("redirect_map.csv", encoding="utf-8")):
hops = chain(old)
status, final = hops[-1]
problems = []
if status >= 400: problems.append(f"final {status}")
if final.rstrip("/") != expected.rstrip("/"): problems.append(f"lands on {final}")
if len(hops) > 2: problems.append(f"{len(hops) - 1} hops")
if any(s in (302, 303, 307) for s, _ in hops): problems.append("temporary redirect")
print("OK " if not problems else "BAD", old, "; ".join(problems))
This covers the five checks for a few hundred lines. You will want to add error handling (DNS failures, timeouts, loops) and parallel requests before running it on thousands of URLs.
Option C: a hosted checker for bulk jobs
Disclosure: this one is ours. We publish Redirect Checker on Apify. You paste URLs, or the two columns of your redirect map (old -> expected), and it returns every hop of every chain. It only reads headers, never page bodies, and runs up to 50 checks in parallel.
from apify_client import ApifyClient # pip install apify-client
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("extas_corp/redirect-checker").call(run_input={"urls": [
"https://old-shop.com/product/42 -> https://new-shop.com/p/blue-shoes",
"https://old-shop.com/about -> /company",
]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if not item["ok"]:
print(item["url"], item["statusChain"], item["finalUrl"], item["warnings"])
Each URL comes back as one row, for example:
{
"url": "http://apify.com",
"finalUrl": "https://apify.com/",
"finalStatusCode": 200,
"statusChain": "301 > 200",
"warnings": [],
"expectedUrl": "https://apify.com/",
"matchesExpected": true,
"ok": true
}
ok is true only when the final status is below 400, there is no loop, and the chain ends on the expected URL. Warnings flag chains longer than one hop, temporary redirects, HTTPS to HTTP downgrades and loops. The comparison ignores the case of the scheme and host, default ports, a trailing slash and the #fragment; the query string must match. DNS, TLS and timeout errors are reported on their row and do not stop the run.
It is free during launch: you only pay Apify's own platform usage, which the free monthly credit covers for normal use. Limits: it follows HTTP redirects only (the Location header), not JavaScript or <meta refresh> redirects, and sites that block bots may answer 403 or 429, which is reported as is.
When to run the checks
- Before launch, on staging, with the full map.
- On launch day, on production, right after the switch.
- A few weeks later, because redirects get lost when someone edits the server config or changes a plugin.
Keep the map file. It is also your test suite for the next migration.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.