Website Migration Without Losing Search Traffic: A Developer's Redirect Checklist
Most traffic drops after a website migration have nothing to do with the new design. They come from old URLs that stopped working, and from redirects nobody tested.
The fix isn't complicated, but it has to happen in the right order. Here's the process, including a small script that checks your whole redirect map before and after launch.
1. Build the URL inventory first
Before anyone touches the new site, list every URL that matters on the old one. A crawl alone isn't enough, because it only finds pages that are still linked. Combine these sources:
- A crawl of the live site (Screaming Frog or similar)
- Your XML sitemaps
- Search Console performance and page indexing exports
- Analytics, for landing pages that get traffic
- Server logs, for URLs that bots and old integrations still request
- A backlink export, for pages other sites link to
Remove duplicates and save the result as a CSV. This file is the source of truth for everything that follows.
2. Map old URLs to new URLs, one to one
Each old URL should point to the page that best replaces it:
old,new /services/web-design,/web-services/design /blog/2021/seo-basics,/blog/seo-basics /pricing.html,/pricing Don't send everything to the homepage. Google can treat a redirect to an irrelevant page as a soft 404, so you lose the value of the old URL anyway. If a page really has no replacement, a 410 or 404 is the honest answer.
3. Use the right status code
| Situation | Code |
|---|---|
| Page moved for good | 301 (or 308) |
| Temporary move, such as a short campaign | 302 (or 307) |
| Page removed with no replacement | 410 or 404 |
A common mistake is shipping temporary redirects because they're the framework's default. Express's res.redirect(), for example, sends a 302 unless you pass a status. For a permanent move, set the code explicitly.
4. Keep the rules in one place
With more than a few redirects, a map is easier to maintain and review than a long list of individual rules. In nginx:
map $uri $redirect_target { default ""; include /etc/nginx/redirects.map; } server { # ... if ($redirect_target) { return 301 $redirect_target$is_args$args; } } And in redirects.map:
/services/web-design /web-services/design; /blog/2021/seo-basics /blog/seo-basics; /pricing.html /pricing; Using $uri means the match ignores the query string, and $is_args$args carries it over to the new URL. Without that, you can lose UTM tags and other parameters.
5. Test the map with a script
Clicking through a few URLs by hand won't catch much. This script reads the CSV and checks that each old URL gets a permanent redirect, lands on the expected page, returns 200, and doesn't pass through a chain:
import csv import sys from urllib.parse import urljoin import requests base_url, csv_path = sys.argv[1], sys.argv[2] failures = 0 with open(csv_path, newline="") as f: for row in csv.DictReader(f): old = urljoin(base_url, row["old"]) expected = urljoin(base_url, row["new"]) r = requests.get(old, allow_redirects=True, timeout=15) problems = [] if not r.history: problems.append("no redirect") else: if r.history[0].status_code not in (301, 308): problems.append(f"first hop is {r.history[0].status_code}") if len(r.history) > 1: problems.append(f"chain of {len(r.history)} hops") if r.url != expected: problems.append(f"landed on {r.url}") if r.status_code != 200: problems.append(f"final status {r.status_code}") if problems: failures += 1 print(f"FAIL {row['old']}: {', '.join(problems)}") print(f"{failures} problem(s) found") sys.exit(1 if failures else 0) Run it against staging before launch and against production right after:
python check_redirects.py https://staging.example.com redirects.csv Because it exits with a non-zero code on failure, you can also add it to your CI pipeline.
6. Check what's easy to forget
These pass unnoticed on launch day and cause trouble weeks later:
- Internal links: update them to the new URLs instead of relying on redirects.
- Canonical tags: they should point to the new URLs, not the old ones or staging.
-
robots.txt: make sure the stagingDisallow: /didn't ship to production. -
noindextags: the same goes for any left over from staging. - XML sitemap: it should list only final URLs that return
200. - Old redirects: update them so they go straight to the new URL. This is how
A → B → Cchains begin. - Images and PDFs: these URLs change too, and they often have backlinks.
- Domain moves: use the Change of Address tool in Search Console.
7. Watch it after launch
Migration work doesn't end at deployment. For the next few weeks:
- Check your server logs daily for
404s on old URLs, and add any mapping you missed. - Watch the Search Console page indexing report.
- Run the script again after later deployments, since a config change can quietly break the rules.
- Keep the redirects live for at least a year, as Google recommends for site moves. Keep them longer if the old URLs still get traffic or have backlinks.
Some ranking movement is normal after a large migration while search engines process the changes. A steep drop that doesn't recover usually points to something specific, so start with the checks above.
Quick checklist
- [ ] URL inventory built from several sources
- [ ] One-to-one map, with no bulk redirects to the homepage
- [ ] Permanent status codes on permanent moves
- [ ] Query strings preserved
- [ ] Script passes on staging
- [ ] No staging
noindexorDisallowin production - [ ] Script passes on production
- [ ] 404 logs reviewed after launch
Prism Infoways is a web development and IT services company that offers custom web app development and related services.
This post was drafted with AI assistance.