How to test redirects after a migration: every row, one hop, a 200

the map was a plan; the live site is the test. how to check every row in an afternoon, and what to watch in search console for the weeks after.

by Max Lorenz, Goodaim · updated

what the test is looking for
  • /about-us/pass
    301 → /about → 200
  • /produkte/zangechain
    301 → 301 → 200
  • /downloads/price-list.pdfno rule
    404
  • /teamwrong code
    302 → /about → 200

SHORT ANSWER

To test redirects after a migration, request every old url from the redirect map against the live site and check four things per row: the first response is a 301 (or 308), exactly one redirect is followed, the final response is a 200, and the final url is the target you planned. A curl loop or a crawler in list mode checks thousands of urls in minutes. Then watch Search Console for several weeks: “Page with redirect” should grow for the old urls while “Not found (404)” and “Redirect error” stay flat.

What counts as a pass

“It redirects” is not the test. A url can redirect and still be wrong in four different ways, and each outcome below points at a different fix.

resultwhat it meansfix
301 → 200, planned targetpass—
301 → 301 → 200a chain: the target redirects againpoint the rule at the final url
301 → 404the target does not exist on the new sitecorrect the target; check for renamed pages
301 → 200, different urla rule fires, but not the planned one — often a wildcard winning over an exact rowrule order, or a duplicate source
302 or 307 → 200a temporary redirect for a permanent movechange the rule type to 301
404 straight awayno rule matched: never imported, or a spelling variantimport it; check case and trailing slash
200 straight awaythe url exists unchanged — fine for exact matches, wrong otherwiseif a redirect was planned, an old page is still published
too many redirectsa loopfind the rule that points back
410a pass — if the url was planned as goneotherwise, a missing target

Some hosts answer their own redirects with a 308 rather than a 301; Google’s documentation treats the two as equivalent, so count both as a pass. The background on each code is in 301 vs 302 vs 410.

Which urls to test

  • every source in the map, not a sample. The rows that break are rarely the ones you would pick by hand.
  • the urls planned as gone, to confirm they answer 410 (or 404) and not a 200 from an old page nobody unpublished.
  • variants of the top pages: http://, the other host (www or bare domain), with and without a trailing slash. A map that is flat for the canonical spelling can still take three hops for http://example.com/page.
  • urls under each wildcard rule that the crawl never found — a few from the server logs or Search Console. That is what the wildcard was for.

If the map came from Silentfrog, the generic csv is already that list: its first two columns are source_url and target_url, absolute urls, and unmatched rows are included with an empty target — which in the test should come back as the 410s or 404s you decided on (the two downloads). If the new site was crawled on a staging domain, replace that host with the live domain in the target column first. Source urls are written normalised, in lowercase and without a trailing slash, so add the original spellings of important urls separately.

A curl loop over the whole map

Google’s site move guide suggests exactly this: “Test the redirects. You can use the URL Inspection Tool for testing individual URLs, or command line tools or scripts to test large numbers of URLs.” This loop reads a csv whose first two columns are the old url and the planned target, makes two requests per row — one without following redirects for the first status, one following them — and writes a tab-separated result:

tail -n +2 silentfrog-redirects.csv | cut -d, -f1,2 | while IFS=, read -r url planned; do
  first=$(curl -s -o /dev/null --max-time 15 -w '%{http_code}' "$url")
  result=$(curl -s -o /dev/null --max-time 15 -L --max-redirs 10 \
    -w '%{num_redirects}\t%{http_code}\t%{url_effective}' "$url")
  printf '%s\t%s\t%s\t%s\n' "$url" "$planned" "$first" "$result"
done > redirect-check.tsv

The columns are old url, planned target, first status, number of redirects followed, final status and final url. num_redirects, http_code and url_effective are curl’s own write-out variables; --max-redirs 10 matches the number of hops Google’s crawlers follow by default. Then print only the rows that fail:

awk -F'\t' '$3 !~ /^30[18]$/ || $4 != 1 || $5 != 200 || ($2 != "" && $6 != $2)' redirect-check.tsv

A row is printed when the first status is not 301 or 308, when the number of hops is not exactly one, when the final status is not 200, or when it lands somewhere other than the planned target. Empty-target rows always print, so the urls you retired are in the same list to confirm, and so do rows whose old and new path are the same: they answer 200 straight away and pass if the page is the right one.

three caveats — the comparison with the planned target is a text comparison, so the map must use the same spelling as the live site (https, host, trailing slash). cut -d, splits on every comma, which breaks for the rare url that contains one. And the loop uses full GET requests, not HEAD, because some servers answer HEAD differently — at a few thousand urls, consider a short sleep between rows to stay polite to your own server.

EXAMPLE

1 140 rows, eleven minutes. 1 097 pass. 31 take two hops — all from the previous relaunch’s redirect file, whose targets moved again. 9 end in a 404: the pricing pages renamed a week before launch. 3 answer 302 — a plugin rule nobody had touched in years. The fix list is 43 lines long, and every line says what to change.

A crawler in list mode

If you prefer a ui, a desktop seo crawler does the same job. In Screaming Frog, its redirect audit tutorial describes the setup: switch to list mode, upload the old urls, enable “Always Follow Redirects” in the advanced configuration so chains are followed to the end, crawl, and export the All Redirects report. It has one row per uploaded url with the number of redirects, whether the chain loops, the final status code and the final address — the same columns as the curl loop, plus every hop in between. The free version stops at 500 urls.

Either way, the check needs the old urls as input. A crawler pointed at the new site’s homepage only sees the new site’s links and never requests the old addresses.

What to watch in Search Console

The live test is done in an afternoon; Google’s side takes weeks. The site move guide puts it at “a few weeks for most pages to move” for a small to medium-sized site, longer for larger ones, and warns that visibility “may fluctuate temporarily during the move.”

reportwhat to look fora warning sign
page indexing — Page with redirectgrowing as old urls are recrawled and dropped from the indexflat after two weeks: old urls not being recrawled — submit the old sitemap
page indexing — Not found (404)flatrising: urls missing from the map; the examples show which folder
page indexing — Redirect erroremptyany entry: a loop, an over-long chain or a bad url in a redirect
page indexing — Soft 404flatrising: redirects to unrelated pages, often the homepage
crawl stats — by responseMoved permanently (301) up after launcha large share of Moved temporarily (302)
sitemapsold sitemap's indexed count falling, new one's risingnew sitemap with errors or few indexed pages
performance — by pagenew urls picking up the clicks the old ones hada page whose clicks vanished without a successor gaining them

The report and reason names are the ones Search Console uses in its page indexing report and its crawl stats report (under settings). Use the URL Inspection tool for single urls you want to look at closely. If the domain changed, the Change of Address tool belongs in the same week — Google says it is only needed for a move from one domain or subdomain to another.

EXAMPLE

three weeks after launch, Not found (404) rises by 60 urls, all under /downloads/. The curl test passed because those urls were never in the map: pdf files that only appeared in the server logs. One wildcard rule fixes them; the lesson goes into the inventory for next time.

Where the map comes from

The test is only as complete as the list it runs on. Silentfrog builds that list before launch: it crawls the old site and reads its sitemaps, accepts your own url list on top, matches every old url to a page of the new site that answered 200 during the crawl, and lets you review the uncertain rows before export. It does not test the live site afterwards — that is what this page is for.

results

8 pages · 2 exact · 3 fuzzy · 1 need review · 1 unmatched · 2 verified

✓Match
/kampagne/sommer-2023Sommeraktion 2023 — 20 % auf alle Zangenunmatched94031%200
/produkte/seitenschneiderSeitenschneider 160 mmreview61272%200
/blog/relaunch-checklisteDie Relaunch-Checklistefuzzy42188%200
/blog/ladezeitLadezeit verbessernfuzzy14391%200
/blog/301-vs-302301 oder 302?fuzzy30592%200
/ueber-unsÜber unsmanual188100%200
/produkte/kombizangeKombizange 180 mmexact274100%200
/kontaktKontaktexact96100%200

the results table: old paths on the left, matched new paths on the right, each row badged exact, fuzzy, needs review, manual or unmatched, sorted worst confidence first.

the results table: every old url with its planned target — the same list the test runs against after launch.

The steps around it are in the website migration checklist; what to do with rows that take two hops is in redirect chains and loops.

In short

  • A pass is 301 (or 308), exactly one hop, a final 200, and the planned target — all four.
  • Test every source in the map, the retired urls, host and protocol variants, and urls under each wildcard.
  • A curl loop with num_redirects, http_code and url_effective checks thousands of rows; a list-mode crawl does the same with a ui.
  • Watch Page with redirect grow while Not found (404), Redirect error and Soft 404 stay flat.
  • Google needs weeks to process a move; keep checking for two to three months and keep the rules for at least a year.

No map yet? Build it in Silentfrog — the first 50 pages of the old site are free, and the csv it exports is the input for every check on this page.