Auditing Redirect Hops After a Headless Migration

A replatform to a headless stack rewrites URL structure, and stacked redirect rules quietly turn old URLs into multi-hop chains, loops, or soft 404s — an audit measures exactly where each legacy URL now lands.

When to Use This Approach

Run this audit when your site is in any of these post-migration situations:

  • You have just replatformed to a headless frontend and need to confirm the legacy URL inventory still resolves to live pages.
  • Organic traffic dropped after a migration and you suspect broken or chained redirects are stranding inbound links.
  • You inherited a legacy URL list of unknown health and need a status map before deciding what to redirect, keep, or retire.

Redirect hop audit classification Legacy URLs are followed through their redirects and classified into five buckets: clean single hop, multi-hop chain, loop, hard 404, and soft 404. Legacy URLs follow + record Clean single hop (keep) Multi-hop chain (flatten) Loop (break) Hard 404 (redirect) Soft 404 (fix status) Prioritize by links + traffic

Implementation Steps

Step 1: Assemble the Legacy URL Inventory

Pull every pre-migration URL you can find into one deduplicated list. The more complete the inventory, the fewer stranded URLs slip through untested.

# Combine sources into one deduplicated legacy URL list
# 1) Old sitemap URLs
curl -s https://web.archive.org/web/2id_/https://yourdomain.com/sitemap.xml \
  | grep -oP '(?<=<loc>)[^<]+' > urls-sitemap.txt

# 2) Search Console performance export (pages CSV) and analytics landing pages
cut -d',' -f1 gsc-pages.csv | tail -n +2 > urls-gsc.txt
cut -d',' -f1 analytics-landing-pages.csv | tail -n +2 > urls-analytics.txt

# 3) Access-log paths
awk '{print $7}' access.log | sort -u > urls-logs.txt

# Merge, strip host to paths, dedupe
cat urls-*.txt | sed -E 's#^https?://[^/]+##' | sort -u > legacy-urls.txt
wc -l legacy-urls.txt

Validation: spot-check legacy-urls.txt for a known high-value old URL. If it is missing, add its source before proceeding.


Step 2: Batch-Follow Each URL and Record Hops

Follow every legacy URL, capturing the hop count, the full redirect path, and the final status code into a single CSV you can sort and filter.

# scripts/audit-hops.sh
DOMAIN="https://yourdomain.com"
echo "path,hops,final_code,final_url" > redirect-audit.csv

while IFS= read -r path; do
  # -w prints final code, redirect count, and effective URL after following
  read -r code hops final < <(curl -sIL -o /dev/null \
    -w '%{http_code} %{num_redirects} %{url_effective}' \
    "${DOMAIN}${path}")
  echo "${path},${hops},${code},${final}" >> redirect-audit.csv
done < legacy-urls.txt

column -t -s',' redirect-audit.csv | head

Validation:

# Every row should have a numeric hop count and a final status code
awk -F',' 'NR>1 && ($2 !~ /^[0-9]+$/ || $3 !~ /^[0-9]+$/) {print "BAD ROW:", $0}' redirect-audit.csv
# No output means every legacy URL was measured

Step 3: Flag Chains, Loops, and Soft 404s

Bucket the audit results. Multi-hop chains feed straight into flattening redirect chains in edge middleware; loops and 404s are immediate defects; soft 404s are error pages that deceptively return 200.

# Multi-hop chains: more than one redirect before the final response
awk -F',' 'NR>1 && $2>1 {print $1" ("$2" hops) -> "$4}' redirect-audit.csv > flagged-chains.txt

# Hard 404s / gone
awk -F',' 'NR>1 && ($3==404 || $3==410) {print $1}' redirect-audit.csv > flagged-404.txt

# Loops surface as curl error code 47 (too many redirects); capture separately
while IFS= read -r path; do
  curl -sIL -o /dev/null "https://yourdomain.com${path}" 2>/dev/null \
    || echo "${path}" >> flagged-loops.txt
done < legacy-urls.txt

# Soft 404: returns 200 but the final URL is a generic error/not-found route
awk -F',' 'NR>1 && $3==200 && $4 ~ /\/(404|not-found)(\/|$)/ {print $1}' redirect-audit.csv > flagged-soft404.txt
wc -l flagged-*.txt

Validation: open flagged-chains.txt and confirm each listed URL genuinely takes more than one hop with the curl -sIL command from the FAQ below.


Not every broken URL deserves equal urgency. Join the flagged URLs against inbound-link and historical-traffic data so you fix the highest-value paths first.

# links.csv: path,inbound_links   traffic.csv: path,sessions
# Produce a ranked worklist of flagged URLs weighted by links + traffic
cat flagged-chains.txt flagged-404.txt flagged-soft404.txt \
  | sed -E 's/ .*//' | sort -u > flagged-all.txt

join -t',' -1 1 -2 1 \
  <(sort -t',' -k1 links.csv) \
  <(sort -t',' -k1 traffic.csv) \
  | awk -F',' '{print $1","($2+0)","($3+0)","($2*5 + $3)}' \
  | grep -F -f flagged-all.txt \
  | sort -t',' -k4 -nr > fix-priority.csv

head fix-priority.csv  # path,links,sessions,score — fix top rows first

Validation: the top rows of fix-priority.csv should be URLs you recognize as important. If a known money page is missing, confirm it appears in flagged-all.txt and in the links/traffic exports.


Step 5: Re-Verify After Applying Fixes

After adding or correcting redirects, re-run the audit against just the fixed URLs and confirm each resolves in one hop to a 200. For a systemic view of why URLs fail to index post-fix, pair this with debugging and diagnostics for headless routing.

# Re-check only the URLs you fixed this round
while IFS= read -r path; do
  read -r code hops < <(curl -sIL -o /dev/null \
    -w '%{http_code} %{num_redirects}' "https://staging.yourdomain.com${path}")
  status="OK"; [ "$code" != "200" ] && status="STILL BROKEN"
  [ "$hops" -gt 1 ] && status="STILL CHAINED"
  echo "${status}: ${path} (code ${code}, hops ${hops})"
done < fixed-this-round.txt | grep -v '^OK'

Validation: the command prints no lines, meaning every re-checked URL now returns 200 in a single hop. Any output is a fix that did not land.

SEO Impact Summary

Signal What improves What breaks if misconfigured
Link equity recovery High-value legacy URLs redirect cleanly to live pages Broken or missing redirects strand inbound links on dead URLs
Crawl efficiency Single-hop resolution cuts wasted crawler fetches Multi-hop chains and loops burn crawl budget per URL
Index accuracy Soft 404s are given a real 404/410 or a proper redirect Soft 404s returning 200 pollute the index with empty pages
Traffic retention Prioritizing by traffic recovers referral sessions fast Ignoring priority leaves the most-linked URLs broken longest

Measurable signals to watch:

  • GSC Pages report: “Not found (404)” and “Redirect error” counts should trend to zero for previously indexed URLs.
  • Referral and organic sessions on migrated landing pages should recover toward pre-migration levels.
  • Audit re-run: zero remaining multi-hop or looping URLs in the legacy inventory.

Edge Cases and Gotchas

Soft 404s that return 200 A migrated CMS often serves a friendly “page not found” template with a 200 status. Search engines may still classify it as a soft 404, but automated status checks see success. Detect these by matching the final URL against your error route, as in Step 3, and give them a real 404/410 or a proper redirect.

Redirect loops from conflicting rules Two rules that point at each other (/a/b and /b/a) create an infinite loop that curl aborts with error 47. These usually arise when a new headless rule collides with a surviving legacy rule. Break the cycle at the source and re-audit rather than patching one side.

Case and trailing-slash variants Migrations frequently change slug casing or trailing-slash policy, so /About and /about/ may resolve differently. Include both variants in the inventory, and align them with your slug normalization strategies so a single canonical form is the redirect target.

Rate limits during a large batch crawl Following tens of thousands of URLs quickly can trip CDN or WAF rate limits, poisoning the audit with false 429/503 results. Throttle the batch, use a recognizable user agent, and, where possible, run against an origin allowlist so the audit reflects real redirect behavior, not throttling.

Frequently Asked Questions

How do I list every redirect hop for a URL? Use curl -sIL to follow the redirects and print each response’s status line and Location header. The sequence of HTTP status lines and Location values is the hop-by-hop chain, ending at the final 200 or a terminal error status. Add -w '%{num_redirects}' to get the total hop count in one number for scripted audits.

What’s an acceptable post-migration 404 rate? There is no universal threshold, but any legacy URL that carried traffic or backlinks should return effectively zero 404s. Pages you deliberately retired can and should return 404 or 410. Track the trend in Search Console rather than an absolute figure, and treat every 404 on a previously indexed, linked, or trafficked URL as a defect to fix.

How do I prioritize redirect fixes? Rank the broken or chained URLs by inbound-link value and historical organic traffic, then fix the URLs that concentrate the most external links and prior sessions first. Those recover the most link equity and referral traffic per fix, which matters when a migration produces a long list of low-value paths alongside a handful of critical ones.

Should retired pages redirect or return 410? If a retired page has a clear, relevant successor, redirect to it with a 301 so its signals transfer. If there is no equivalent and the content is genuinely gone, return 410 Gone (or 404) so search engines drop it cleanly rather than chasing an irrelevant redirect target that can look like a soft 404.


Part of: Redirect Chain Management

Related