DEV Community

Cover image for Replatforming Without Losing SEO: Redirect Map Playbook
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

Replatforming Without Losing SEO: Redirect Map Playbook

Most traffic lost in a replatform is not lost to Google's algorithm. It is lost to a spreadsheet nobody finished: old URLs with no destination, redirects that chain through two hops, a staging noindex that shipped to production. A new stack, whether that is Nuxt, Next.js or a custom Laravel storefront, can be faster and better in every way and still drop organic traffic for months if the URL migration is treated as a launch-week chore.

I run Loop Origin, where my team rebuilds and migrates web applications and stores, mostly onto Laravel, Vue 3 and Nuxt. I wrote this with help from AI tools and checked every config and claim myself. The config and scripts below are simplified from what I use on real migrations, and the checker output is from a run I did today.

How do you replatform a website without losing SEO traffic?

Direct answer: Inventory every URL that has traffic, links or indexation, map each one to its closest equivalent on the new site, and serve single-hop 301 redirects from the edge or web server. Keep titles, canonicals and structured data at parity, test every redirect on staging with a script before launch, and monitor 404s and Search Console daily for six weeks.

Step 1: build the URL inventory from four sources

No single source is complete. Merge and deduplicate these:

Source What it finds What it misses
Full crawl of the old site Every linked page, status codes, canonicals Orphan pages nothing links to
XML sitemaps Pages you intended to be indexed Pages added outside the CMS
Search Console Pages and Performance reports URLs Google indexed and ones with clicks Pages with no impressions
Backlink exports and server logs URLs other sites link to, real bot and user hits Nothing important, but noisy

Server logs are the one teams skip. They are also where old campaign URLs, PDF links in emails and legacy query-string product pages show up. Thirty days of access logs filtered to 200 responses for GET requests is usually enough.

Step 2: map, do not mass-redirect

Each old URL gets one row: old,new. The rule is closest equivalent: a product to the same product, a category to the matching category, a blog post to the same post at its new path. Where nothing equivalent exists, send it to the nearest parent category, or let it return 410 if the content is truly gone.

The tempting shortcut is redirecting everything unmatched to the homepage. Google's documentation says redirects to irrelevant pages can be treated as soft 404s, so you lose the signal anyway and confuse users. A smaller number of honest 410s is better.

old,new
/products.php?id=118,/shop/steel-water-bottle-750ml
/category/kitchen,/shop/kitchen
/blog/2019/05/care-guide.html,/guides/bottle-care
/about-us.html,/about
/old-promo,/shop
Enter fullscreen mode Exit fullscreen mode

Step 3: choose where redirects live

Layer Latency Handles query-string URLs Change process Best for
CDN edge rules (bulk redirect lists) Lowest, before origin Varies by provider and plan Dashboard or API, plan limits apply Large static maps, domain moves
Nginx map at the origin Very low, no app boot Yes, with $request_uri Config deploy and reload Most migrations I run
App middleware (Nuxt server route, Next.js middleware, Laravel) Highest, app must handle it Yes Normal code deploy Rules that need a database lookup
CMS redirect plugin Variable Usually Editors can manage Small sites where marketing owns URLs

I default to Nginx maps for anything static, because the lookup is a hash table and the app never sees the request. The config:

# http {} context
map_hash_max_size    262144;
map_hash_bucket_size 256;

# legacy URLs that depend on the query string
map $request_uri $legacy_query_redirect {
    default "";
    include /etc/nginx/redirects/query.map;  # "/products.php?id=118" /shop/steel-water-bottle-750ml;
}

# path-only URLs ($uri is decoded and has no query string)
map $uri $legacy_path_redirect {
    default "";
    include /etc/nginx/redirects/path.map;   # /category/kitchen /shop/kitchen;
}

server {
    server_name www.example.com;
    if ($legacy_query_redirect) { return 301 $legacy_query_redirect; }
    if ($legacy_path_redirect)  { return 301 $legacy_path_redirect; }
    location / { proxy_pass http://nuxt_upstream; }
}
Enter fullscreen mode Exit fullscreen mode

Two maps matter. $uri is normalized and has no query string, so it cannot match products.php?id=118. $request_uri is the raw request line, so it can, but it will not match if tracking parameters are appended. Generate both map files from the CSV in CI, and run nginx -t before reload.

A railway track switch with steel rails diverging on gravel, representing old URLs being routed to their new destinations
AI-generated image.

Step 4: test every row before launch

A redirect map is code, so test it. This script follows each redirect manually and flags non-301 first hops, chains, wrong destinations and broken targets:

for row in csv.DictReader(open(sys.argv[2])):
    url, hops = urljoin(BASE, row["old"]), []
    status, loc = fetch(url)                       # HEAD, no auto-follow
    while status in (301, 302, 307, 308) and len(hops) < MAX_HOPS:
        hops.append(status)
        url = urljoin(url, loc)
        status, loc = fetch(url)
    final_path = path_and_query(url)
    problems = []
    if not hops:                 problems.append(f"no redirect ({status})")
    elif hops[0] != 301:         problems.append(f"first hop is {hops[0]}, not 301")
    if len(hops) > 1:            problems.append(f"chain of {len(hops)} hops")
    if final_path != row["new"]: problems.append(f"lands on {final_path}")
    if status != 200:            problems.append(f"target returns {status}")
Enter fullscreen mode Exit fullscreen mode

I ran it against a local staging stub where I had planted three common mistakes:

OK   /products.php?id=118 -> /shop/steel-water-bottle-750ml
OK   /category/kitchen -> /shop/kitchen
FAIL /blog/2019/05/care-guide.html -> /guides/bottle-care  [chain of 2 hops]
FAIL /about-us.html -> /about  [first hop is 302, not 301]
FAIL /old-promo -> /shop  [lands on /shop/spring-sale; target returns 404]

3 problem(s)
Enter fullscreen mode Exit fullscreen mode

Each failure is one I have seen in real launches: a blog redirect that pointed to an intermediate URL that itself redirected, a framework default that issued 302, and a promo URL mapped to a page deleted before launch. The script exits non-zero, so it can block the deploy pipeline.

Step 5: keep on-page signals at parity

Redirects move the signal; the page has to deserve it. Before launch, diff old and new versions of your top pages for:

  • Title and meta description, and the main H1
  • rel="canonical" pointing to the production URL, not staging
  • Structured data (Product, Article, BreadcrumbList, FAQPage) still present and valid
  • Internal links updated to the new URLs, so the site does not redirect itself
  • Content rendered in the HTML response, not only after client-side JavaScript

The last item is where framework migrations go wrong. If the old site was server-rendered and the new one renders key content client-side, crawlers and link previews see less. I compared the options in Nuxt 3 rendering for B2B SaaS: SSR, SPA or hybrid.

Launch-day and the six weeks after

If the domain changes too, audit DNS first; my DNS audit before a domain migration covers TTLs and cutover. Use Search Console's Change of Address tool only for domain moves.

Failure mode How it shows up Detection Fix
Staging noindex or robots.txt shipped Pages drop out of the index over days Crawl production on launch day Environment-specific config, checked in CI
Redirect chains from old migrations Slower crawling, diluted signals Checker flags chain of N hops Point every old URL straight to the final URL
Unmapped URLs 404 spike in logs Daily 404 report from access logs Add rows, redeploy map
Mass homepage redirects Soft 404s in Search Console Pages report Map to closest equivalent or return 410
Trailing slash or case mismatch Valid-looking URLs 404 Checker with variants One canonical form, redirect the other

Keep the redirects for at least a year. Google's site-move guidance says generally at least one year, and I keep them indefinitely when the map is cheap to serve, because old links in emails and forums never stop arriving.

Migration checklist

  • [ ] URL inventory merged from crawl, sitemaps, Search Console, backlinks and logs
  • [ ] One-to-one CSV map reviewed by someone who knows the catalog
  • [ ] No homepage catch-all; 410 for content that is gone
  • [ ] Redirects at the edge or web server, single hop, 301
  • [ ] Checker script passing in CI against staging
  • [ ] Canonicals, titles and structured data at parity on top pages
  • [ ] Key content in server-rendered HTML
  • [ ] Staging noindex and robots.txt rules removed in production
  • [ ] New XML sitemap submitted; old sitemap kept live briefly so Google recrawls old URLs
  • [ ] Daily 404 and Search Console review for six weeks

Frequently asked questions

Should I use 301 or 308 redirects for a migration?

Both are permanent, and Google treats them the same way. 308 also preserves the request method, which only matters for POST endpoints. For page URLs, 301 is the conventional choice.

How long does traffic take to recover after a replatform?

It depends on site size, crawl frequency and how clean the migration was. A well-mapped migration often shows little disruption; a broken one can take months. I do not trust any fixed timeline, so measure against your own baseline.

Can I change URL structure and design at the same time?

You can, but every simultaneous change makes problems harder to diagnose. If the schedule allows, keep URLs stable in the first release and change structure later.

Do redirects pass all ranking signals?

Google has said permanent redirects do not lose PageRank. Signals still depend on the destination being a genuine equivalent, which is why closest-equivalent mapping matters more than the redirect type.

What about URLs with query parameters like ?id=118?

Map them explicitly using the full request URI, as in the Nginx config above. Decide how to treat extra tracking parameters, either stripping them before matching or matching on the parameter you care about.

Should old URLs stay in the new sitemap?

No. The new sitemap should list only final 200 URLs. Some teams keep the old sitemap live for a few weeks after launch to prompt Google to recrawl old URLs and discover the redirects sooner.


Follow & Connect

Top comments (0)