/ โ† Migration Audit Fixes / Migration Redirects
In this guide
  1. What a good redirect map looks like
  2. Where the old URLs come from
  3. Choosing the right destination for each row
  4. Legacy redirects and the hidden second hop
  5. Turning the map into redirect rules
  6. The fix, step by step
  7. A test script you can run against the map
  8. Keeping the map healthy after launch
  9. Questions and answers

What a good redirect map looks like

A redirect map is a table. Each row holds one address from the old site and the one address on the new site that replaces it. Everything else in the migration depends on it: the redirect rules are generated from it, the post-launch checks are run against it, and when rankings wobble it is the first document you open to find out why.

A usable map has more than two columns. The extra columns are what let you prioritise, review and test it without guesswork.

ColumnExampleWhy it is there
Old URL/shop/oak-desk-120cm.htmlThe path exactly as it was served, before any clean-up.
New URL/furniture/desks/oak-desk-120/The single final destination, never another redirect.
Match typeexact, pattern, merged, retiredShows how the pairing was decided so reviewers can challenge it.
Sourcecrawl, sitemap, logs, backlinksProves the old URL really existed and was used.
Clicks or sessions412From Search Console or analytics; used to sort the testing order.
Linking domains9Pages with external links deserve careful, specific destinations.
Expected status301 or 410What the test script should see for this row.
Owner and notes"Merged with 140cm model"Who signed off unusual choices, and why.

The two failures the Site Migration Audit most often exposes are rows that were never in the map at all, and rows whose destination is itself redirected, missing or unrelated. Both are problems with the map, not with the server, which is why fixing the spreadsheet comes before touching any configuration.

Where the old URLs come from

No single source lists every address a site has ever used. A crawl only finds pages that are linked; a sitemap only lists what the old CMS chose to include; analytics only shows pages that were visited. Combine several, and keep a note of where each URL came from.

๐Ÿ’ก If the site has already moved and nothing was saved, the Internet Archive's CDX API can list captured URLs for a domain, for example https://web.archive.org/cdx/search/cdx?url=example.co.uk/*&fl=original&collapse=urlkey. Treat the result as a starting list, not a complete one.

Choosing the right destination for each row

Matching is where judgement matters. A redirect passes an old page's signals to the new address only when the new page genuinely replaces it. Google has said that redirecting many unrelated pages to one destination, such as the home page, can be treated like a soft 404, so the value of those old URLs is largely lost.

The four kinds of row

  1. Exact equivalents. The same product, article or service at a new address. These should be the large majority.
  2. Pattern matches. Whole sections that moved with a predictable change, such as /blog/YYYY/MM/slug/ becoming /articles/slug/. One rule can cover thousands of rows, but check a sample by hand.
  3. Merges. Several thin old pages combined into one new page. Redirect each to the merged page only if the merged page covers what each old page covered.
  4. Retirements. Content with no successor. A 410 or a helpful 404 is more honest than a redirect to a loosely related page. If a close parent category genuinely serves the same need, that is acceptable.

Matching at scale

For large sites, match by something more stable than the path. Product SKUs, article IDs, ISBNs or the page title are good join keys. Export the old site with its IDs, export the new site with the same IDs, and join the two in a spreadsheet with XLOOKUP or VLOOKUP. Only the rows that fail to join need human attention.

=XLOOKUP(C2, NewSite!C:C, NewSite!A:A, "NO MATCH")

Rows that come back as NO MATCH are exactly the ones worth a person's time. Sort them by clicks and linking domains so the valuable ones are decided first.

Legacy redirects and the hidden second hop

Most sites have been rebuilt before. The old configuration may already contain redirects from an earlier redesign, such as /products.php?id=88 to /shop/oak-desk-120cm.html. If you add a new rule from /shop/oak-desk-120cm.html to the new address and leave the old rule alone, the oldest links now travel through two redirects.

The fix belongs in the map. Add every legacy source to the map as its own row, and set its destination to the final new URL. Then no rule points at an address that is itself redirected. The redirect checker guides on this site cover diagnosing chains in general; here the aim is simply that the map never creates them.

A quick way to find rows that would create a chain is to look up every destination in the old-URL column. Any destination that also appears as a source is a second hop.

=IF(COUNTIF(A:A, B2) > 0, "CHAIN: destination is also redirected", "")
โš ๏ธ Watch for protocol and host changes as well. If the new rules redirect to http:// or to the non-www host and a separate rule then corrects it, every row gains an extra hop. Write every destination with the final scheme and host.

Turning the map into redirect rules

How you implement the map depends on the platform, but the principle is the same: keep exact rows in a lookup table and keep only a small number of pattern rules. Hundreds of separate regular expressions are slow to evaluate and hard to review.

nginx: a map file

nginx's map directive reads a two-column list and performs one lookup per request, which suits large redirect maps well. Export the spreadsheet as two columns and include it.

# /etc/nginx/redirects.map  (generated from the spreadsheet)
/shop/oak-desk-120cm.html      /furniture/desks/oak-desk-120/;
/products.php?id=88            /furniture/desks/oak-desk-120/;
/about-us.html                 /about/;

# in the http block
map $request_uri $migrate_to {
    default "";
    include /etc/nginx/redirects.map;
}

# in the server block
if ($migrate_to) {
    return 301 https://www.example.co.uk$migrate_to;
}

Because $request_uri includes the query string, a row with parameters only matches that exact string. Decide how to handle unexpected parameters on otherwise mapped paths, such as tracking tags, before launch.

Apache

For short maps, Redirect 301 lines work. For long ones, a RewriteMap in the server configuration (it cannot be declared in .htaccess) gives the same single-lookup behaviour as nginx.

CMS and hosted platforms

WordPress redirect plugins, Shopify's URL redirects and most headless hosts accept a CSV import. Check the import's limits and whether it matches query strings and trailing slashes, and import a small test file first. Remember that some platforms only apply their redirect table when no live page exists at the old path.

Which status code

Use 301 or 308 for moved pages. Google treats both permanent and temporary redirects as signals for choosing which URL to index, but a permanent code states the intent plainly and is what its site-move guidance recommends. The practical risk with a stray 302 is small; the bigger risks are missing rows and wrong destinations.

The fix, step by step

Step 1

Collect every old URL from several sources

Gather addresses from a crawl of the old site, every old XML sitemap, the Search Console performance and links reports, a year of analytics landing pages, recent server access logs and the existing redirect configuration. Paste them into one sheet with a column recording where each came from. Include PDFs and images that earn links. The combined list is almost always longer than any one source suggests, and that difference is where lost URLs hide.

Step 2

Clean and de-duplicate the list

Strip the domain so each row is a path, then remove exact duplicates. Decide deliberately how to treat variants such as a trailing slash, upper-case letters, index.html and tracking parameters like utm_source: either normalise them with one general rule or keep them as separate rows. Remove addresses that never returned a 200, such as bot probes found in logs. Add columns for clicks and linking domains from Search Console so the list can be sorted by value.

Step 3

Match each old URL to one destination

Join the old list to an export of the new site using a stable key such as SKU, article ID or title, so most rows match automatically. Review every row that fails to join, starting with the ones that had the most clicks and links. Give each a specific replacement, a genuinely equivalent parent page, or mark it retired with a 410. Avoid sending unrelated pages to the home page, and record the reason for every merge or retirement in the notes column.

Step 4

Fold in legacy redirects so every row is final

Add the source address of every redirect already in the old configuration as its own row, pointing at the new final destination. Then look up every destination in the old-URL column with COUNTIF; any hit means the destination is itself redirected and the row must be updated to skip the middle step. Write each destination with the final scheme and host, including https and the chosen www or non-www form, so no row relies on a second rule.

Step 5

Turn the map into rules

Export exact rows as a two-column file and load them as a lookup, such as an nginx map, an Apache RewriteMap or the platform's CSV importer. Keep regular-expression rules for predictable section moves only, and place exact rows ahead of them so specific decisions win. Use 301 or 308 for moved pages and 410 for retired ones. Keep the generated file under version control so every change to the map can be traced.

Step 6

Test the whole map on staging

Run a script that requests every old path on the staging host without following redirects, and compares the status code and Location header with the expected values in the map. Then request each destination and confirm it returns 200 without a further redirect. Fix every mismatch in the spreadsheet, regenerate the rules and run the test again. Launch only when the failure list is empty or every remaining failure has been consciously accepted.

Step 7

Retest after launch and patch the gaps

Run the same script against the live domain on launch day, because CDN rules, caching and host settings often differ from staging. Run the Site Migration Audit to compare the old crawl with the live site and list lost URLs and redirect problems. Check Search Console for new 404s and server logs for old paths still returning errors, add the missing rows to the map, and repeat weekly for the first month.

A test script you can run against the map

Testing a few addresses by hand is not enough; the only reliable test is every row. This short Python script reads the map as a CSV with old, new and status columns and reports each row that does not behave as expected. It uses the widely used requests library.

import csv, sys, requests

HOST = sys.argv[1]            # e.g. https://staging.example.co.uk
fails = 0
with open("redirect-map.csv", newline="") as f:
    for row in csv.DictReader(f):
        r = requests.get(HOST + row["old"], allow_redirects=False, timeout=15)
        want = int(row["status"])
        got_to = r.headers.get("Location", "")
        if r.status_code != want:
            print("STATUS", row["old"], r.status_code, "expected", want); fails += 1
            continue
        if want in (301, 308) and not got_to.endswith(row["new"]):
            print("TARGET", row["old"], got_to, "expected", row["new"]); fails += 1
            continue
        if want in (301, 308):
            d = requests.get(HOST + row["new"], allow_redirects=False, timeout=15)
            if d.status_code != 200:
                print("DEST", row["new"], d.status_code); fails += 1
print("failures:", fails)

Three kinds of failure come out of it. STATUS means the old path returned the wrong code, often a 200 because a page still exists at that path, or a 404 because the row was never loaded. TARGET means the redirect went somewhere other than the mapped page. DEST means the destination itself is missing or redirects again. Each points to a different fix, so keep them separate in your notes.

๐Ÿ’ก Throttle the script with a short pause between requests on a production server, and run it from a machine the firewall or bot protection will not block, or you will be testing the firewall rather than the map.

Keeping the map healthy after launch

For broader checks on status codes, chains and platform-specific rule syntax, the Redirect Checker Guide goes into more depth. For choosing what to do with URLs that have no obvious replacement, see Fixing Lost URLs After a Migration.

Questions and answers

1. How many old URLs should a migration redirect map contain?

Every address that ever returned a page and still has value: pages with search clicks, pages with links from other sites, pages in old sitemaps, and anything that appears in server logs as receiving real visits. On most sites that is noticeably more than a crawl alone finds. You do not need rows for bot probes or addresses that never existed. If in doubt, include the row, because an unnecessary redirect costs almost nothing while a missing one can lose a page that people still link to.

2. Can I use one pattern rule instead of mapping every URL?

Yes, where the change is genuinely predictable, such as a folder rename where every slug stays the same. One rule can then cover thousands of rows. The risk is that the new site does not have a page for every slug the rule produces, so the redirect lands on a 404. Generate the full list of addresses the rule will create, test them all, and add exact rows for the exceptions ahead of the pattern rule.

3. Should I redirect retired pages to the home page?

Generally no. Google can treat a redirect to an unrelated page, such as the home page, like a missing page, so the old URL passes little or nothing on, and visitors land somewhere that does not answer their question. Point retired pages to a close equivalent if one exists, such as the category that held a discontinued product. If there is truly no equivalent, return a 410 or a helpful 404 page and make sure internal links no longer point to it.

4. What is the difference between 301 and 308 for migration redirects?

Both are permanent redirects. The difference is that a 308 tells the browser to repeat the request with the same method and body, so a form POST stays a POST, while a 301 may be changed to a GET by browsers. For ordinary page moves this makes no practical difference, and search engines treat both as permanent. Use whichever your server or platform supports most easily, and be consistent across the map.

5. How should I handle query strings in the redirect map?

Decide what each parameter means. Parameters that identify content, such as a product ID on an old dynamic site, need their own rows because each value is a different page. Parameters that only track visits, such as utm tags, should not change the destination; either strip them with a general rule before the lookup or pass them through to the new URL. Test both cases, because many platforms and plugins match the path only and silently ignore the query string.

6. Do I still need redirects if Google has already indexed the new URLs?

Yes. Indexing the new addresses is only part of the move. Links from other websites, bookmarks, printed material, old emails and social posts still point at the old URLs, and they will for years. Without redirects, those visits land on errors and the signals from those links stop reaching the new pages. Google advises keeping site-move redirects for at least a year, and most sites keep them permanently because they cost very little to maintain.

7. Why does my redirect work in the browser but fail the test script?

Browsers cache permanent redirects aggressively, so you may be seeing a redirect remembered from earlier testing rather than what the server sends now. Test in a private window or with a script that does not follow redirects. Other common causes are a CDN or firewall treating the script differently from a browser, a rule that only matches with or without a trailing slash, or a staging host with different settings from the live one. Compare the raw headers from both.

Check your redirect map against the live site

The Site Migration Audit compares your pre-migration crawl with the live site, finds lost URLs and validates redirects, so you can see which rows of the map are missing or pointing at the wrong page.

Open Migration Audit Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Migration Audit Fixes  ·  Migration Audit Guide  ·  Fixing Lost URLs After a Migration  ·  Redirect Checker Guide  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.