Fixing the Redirect Map for a Site Migration
A redirect map is the list that tells your server where every old address should now send people and search engines.
When a migration goes wrong, the map is usually the reason: rows are missing, several old pages are pointed at the home page, legacy redirects from an earlier redesign were never updated, or the rules were never tested against the full list. This guide shows how to build the map from reliable sources, match each old page to its true replacement, convert the map into server rules, and prove that every row works before and after launch.
- What a good redirect map looks like
- Where the old URLs come from
- Choosing the right destination for each row
- Legacy redirects and the hidden second hop
- Turning the map into redirect rules
- The fix, step by step
- A test script you can run against the map
- Keeping the map healthy after launch
- 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.
| Column | Example | Why it is there |
|---|---|---|
| Old URL | /shop/oak-desk-120cm.html | The 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 type | exact, pattern, merged, retired | Shows how the pairing was decided so reviewers can challenge it. |
| Source | crawl, sitemap, logs, backlinks | Proves the old URL really existed and was used. |
| Clicks or sessions | 412 | From Search Console or analytics; used to sort the testing order. |
| Linking domains | 9 | Pages with external links deserve careful, specific destinations. |
| Expected status | 301 or 410 | What 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.
- A full crawl of the old site. Run it before launch if you still can. Include images and PDFs if they attract links or traffic.
- Old XML sitemaps. Save copies of every sitemap file, including ones for images, video and news.
- Search Console performance report. Export pages with clicks or impressions over the last 16 months, which is the longest range it holds.
- Search Console links report. The top linked pages list shows old URLs that other sites point to, including ones no longer linked internally.
- Analytics landing pages. Pull a year or more of landing-page data to catch seasonal pages that a crawl in June will not see in use.
- Server access logs. Requests that returned 200 over recent months reveal orphan pages, old campaign URLs and files that nothing links to any more.
- Existing redirect rules. Export the current .htaccess, nginx config, plugin list or platform redirect table. Their source addresses are old URLs too.
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
- Exact equivalents. The same product, article or service at a new address. These should be the large majority.
- 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. - 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.
- 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", "")
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.
Keeping the map healthy after launch
- Keep the spreadsheet as the source of truth. Every new redirect goes into the map first, then into the rules. Editing server files directly is how maps and rules drift apart.
- Update rows when pages move again. If a new page is later renamed, change the destination on every row pointing at it rather than adding another hop.
- Keep redirects for the long term. Google advises keeping site-move redirects for at least a year; old links on other sites last far longer, so most sites keep them indefinitely.
- Watch for new 404s from old paths. A monthly check of logs and the Search Console Not found list will surface old URLs that no source captured, such as addresses printed in brochures or used in email campaigns.
- Update internal links. Redirects are a safety net for outside links. Your own navigation, content and sitemaps should point straight at the new addresses.
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.
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.
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 โ