Skip to main content
ASoc
Guide

Migrating a Marketing Site to Next.js Without Losing Rankings

Change the technology or the URLs, never both. The redirect map, the internal-link diff that put 38 of our own pages in “Discovered — not indexed”, and the 90 days after.

The ASoc Team13 min read

Rankings survive a framework migration when the URLs do. Keep every path identical and you can replace Webflow, WordPress or Gatsby with Next.js and see almost nothing in Search Console. Rankings drop when the migration quietly changes three things at once — URLs, internal linking, and what the page says — and nobody can tell afterwards which one did it.

We publish two comparisons that end with "so migrate" and neither explains how. This is that half: the redirect map, the things that are not the redirect map, and what to watch for ninety days afterwards.

The one rule

Change the technology or change the URLs. Never both in the same deploy.

Almost every horror story is a violation of this. A team rebuilds in a new framework, takes the opportunity to tidy /blog/2019/03/my-post into /blog/my-post, restructures the nav, rewrites the copy "while we're in there," ships it all on a Tuesday, and watches traffic fall 40%. Six weeks later they still cannot say why, because they changed four variables simultaneously.

If the URLs must change, that is a second, separate deploy — after the framework migration has been stable for a few weeks and you have a clean baseline to compare against.

Baseline first, because you cannot prove recovery without it

Before you write a line of the new site, export what you have. This takes an hour and it is the difference between "traffic looks fine" and knowing.

ExportFromWhy you need it
Every indexed URLSearch Console → Pages, plus a crawlThe source list for the redirect map
Top 200 pages by clicks and impressionsSearch Console, 12 monthsThe pages whose ranking you are protecting
Top queries per pageSearch ConsoleDetects a content regression, not just a 404
Current titles and meta descriptionsCrawlThe most commonly lost thing in a rebuild
Backlinked URLsAny backlink toolThese must never 404, even if orphaned
Core Web Vitals, field dataSearch Console / CrUXSo you can tell a real improvement from noise

Use a 12-month window, not 3 months — seasonal pages that rank in November will not appear in an August export, and they are exactly the ones that get dropped.

The backlink export matters more than the traffic export. A page with no traffic and four editorial links is passing authority to your whole site; delete it and you lose that permanently. Traffic is recoverable, links generally are not.

The redirect map

If URLs are staying identical — the good case — you still need this document, because it is how you prove they stayed identical.

old_url, new_url, status, notes
/pricing, /pricing, 200, unchanged
/blog/2019/03/deploy-faster, /blog/deploy-faster, 301, date removed
/features/analytics, /features#analytics, 301, merged into features page
/old-webinar, /resources, 301, no equivalent — nearest parent

Four rules that decide whether it works:

  • 301, not 302. A 302 says "this is temporary, keep the old URL indexed." Google does eventually treat a long-lived 302 as permanent, but "eventually" is months of ambiguity you do not need.
  • One hop. /a → /b → /c is a chain, and each hop is a place to lose signal and a crawl budget cost. Redirect /a → /c directly, even if that means regenerating the map after a later change.
  • Redirect to the closest equivalent, never blanket to the homepage. A mass redirect to / is treated as a soft 404 and passes nothing. If there is genuinely no equivalent, redirect to the nearest category page — and if there is not one of those either, let it 404 honestly. A clean 404 is better than a lie.
  • Every row gets tested. Not spot-checked. This is a script, and it is ten lines.
// scripts/verify-redirects.ts — run against production after deploy
import { readFileSync } from "node:fs";

const rows = readFileSync("redirects.csv", "utf8")
  .trim()
  .split("\n")
  .slice(1)
  .map((line) => line.split(",").map((cell) => cell.trim()));

let failures = 0;

for (const [oldPath, newPath, expected] of rows) {
  const response = await fetch(new URL(oldPath, "https://example.com"), {
    redirect: "manual",
  });
  const location = response.headers.get("location");
  const actual = location ? new URL(location, "https://example.com").pathname : oldPath;

  if (String(response.status) !== expected || actual !== newPath) {
    console.error(`FAIL ${oldPath} → ${actual} (${response.status}), want ${newPath} (${expected})`);
    failures++;
  }
}

console.log(failures === 0 ? "All redirects OK" : `${failures} failures`);
process.exit(failures === 0 ? 0 : 1);

Run it against production the hour you deploy, and again a week later. The second run is not paranoia — redirects get clobbered by later config edits, and nothing tells you.

In Next.js the redirects themselves live in next.config.ts:

// next.config.ts
const nextConfig = {
  async redirects() {
    return [
      {
        source: "/blog/:year(\\d{4})/:month(\\d{2})/:slug",
        destination: "/blog/:slug",
        permanent: true, // 301
      },
    ];
  },
};

For more than a few hundred rules, generate them from the CSV at build time rather than hand-writing them, and prefer patterns over enumerations where the transformation is regular. A config with 4,000 literal redirect objects is slow to evaluate and impossible to review.

The things that are not the redirect map

Redirects get all the attention because they are the part with a checklist. In practice, sites that lose rankings after a clean migration usually lost one of these instead.

Internal linking

This is the one we got wrong on our own site, and it cost us real indexing.

Our storefront has 111 product pages. Each one closes with a rail of six sibling products — and the mechanism behind that rail is a wrapping sliding window over catalog order, so every product receives exactly six inbound links from its siblings. That sounds like an over-engineered detail. It is not: the obvious implementations ("newest six", "six random", "six most popular") all produce the same failure, where a long tail of products ends up linked from nothing. Ours did, and Search Console reported 38 product pages sitting in "Discovered — currently not indexed" — known to Google, never crawled, because nothing pointed at them hard enough to be worth the trip.

The same failure mode hides in a migration. The old site's nav, footer, related-posts block and breadcrumbs formed a link graph, and the rebuild reproduces the pages while quietly reproducing only 60% of the links. Crawl the old site and the new one, and diff the internal link counts per URL. Any page that lost most of its inbound links is a page that will drift out of the index over the next few months, with no error anywhere to tell you.

We also learned a related lesson about hub pages: our seven category pages were noindex while we finished them, which is the worst of both worlds. Google eventually treats a long-lived noindex page as nofollow too, so the tier built to funnel crawl equity into the products was withholding it instead. If your migration parks a section behind noindex, treat that as a deadline, not a state.

Content parity

"We rebuilt the pages" usually means the visual sections were rebuilt. The FAQ block at the bottom that nobody looked at contained 400 words targeting the long-tail queries that were half your traffic for that page.

Diff the rendered text, not the design. Pull the old page's body text and the new one's, and look at the word count and the headings. A page that went from 1,400 words to 600 has lost rankings for reasons no redirect can fix.

Metadata

Titles and descriptions are regenerated from a template in a rebuild, and templated titles are usually worse than the hand-written ones they replaced. Check the top 200 against your baseline export. Also check the things that are easy to forget entirely: canonical tags (self-referencing, absolute URLs), robots.txt, the XML sitemap, and structured data.

On sitemaps, one rule we enforce in our own build: lastModified must be a real date, never the build clock. Emitting new Date() for every URL on every deploy tells Google that 100+ pages changed simultaneously — a claim it can check and find false, and it costs the crawl scheduling that decides whether a page gets indexed at all. During a migration, when you most need Google to recrawl accurately, this matters more than usual. Our product URLs use their newest changelog date; category hubs use the newest among their products.

Performance, in the direction you did not expect

Migrating to a React framework can make a content site slower on mobile even when the desktop scores go up. We measured our own: desktop Lighthouse is 100 across all eight main pages, mobile is 89–99, and when we blocked every image and prefetch the home page's LCP moved from 3786 ms to 3740 ms. Forty-six milliseconds — the gap is JavaScript execution, with react-dom alone at 221 KiB raw / 70 KiB gzipped.

That will not by itself cost you rankings; Core Web Vitals are a modest signal and ours are fine. But do not assume a modern framework is automatically a performance win for a content site, and do not let "the new site is faster" go unmeasured in the post-migration report. If your migration is motivated by performance, the Astro comparison has the numbers on when that reasoning holds.

Launch sequence

The order matters, and two of these are commonly done too early.

  1. Staging is noindex and behind auth. Both. A noindex staging site that is publicly reachable still gets discovered and linked.
  2. Deploy with redirects live in the same release. Not "redirects to follow" — the window between them is when the 404s get crawled.
  3. Run the redirect verification script against production. Within the hour.
  4. Submit the new sitemap in Search Console. Keep the old sitemap available for a few weeks if URLs changed; it gives Google a list of old URLs to recrawl and discover the 301s.
  5. Do not remove the old site's DNS or hosting until redirects have been serving for at least a month.
  6. Use the Change of Address tool only for a domain change. It does not apply to a framework migration on the same domain, and using it when nothing moved is a way to confuse things that were fine.
  7. Request indexing for your top 20 pages manually. It is rate-limited and worth it.

The ninety days after

Recovery from a well-executed migration usually looks like a small dip for two to four weeks, then a return to baseline. Watch these, in this order:

SignalWhereWhat is normalWhat is a problem
Crawl errorsSearch Console → PagesBrief rise, then decline404s still rising at week 3
Indexed page countSearch Console → PagesFlat, or small dipSteady decline over weeks
"Discovered — not indexed"Search Console → PagesStableGrowing — an internal linking problem
ImpressionsPerformance reportDip 2–4 weeks, then recoveryNo recovery by week 6
Average position, top queriesPerformance reportSmall movementSpecific pages falling and staying down
Core Web Vitals, fieldSearch ConsoleRolls over 28 daysSustained regression

Two calibrations. Field Core Web Vitals are a 28-day rolling window, so they will not reflect the migration for a month — do not panic at week one, and do not declare victory at week two either. And check every signal against a control: if impressions are down 15% across your whole industry that month, your migration is not the cause, and a competitor's ranking chart will tell you faster than your own.

Mistakes and how they show up

MistakeWhat happensFix
Changing URLs and framework togetherTraffic drops, cause unattributableTwo separate deploys, weeks apart
302 instead of 301Old URLs stay indexed, signals splitpermanent: true in Next.js
Redirect chainsSignal decay, wasted crawl budgetCollapse to one hop
Blanket redirect to homepageTreated as soft 404, passes nothingNearest equivalent, or an honest 404
Rebuilding pages, not the link graph"Discovered — not indexed" grows quietlyDiff internal link counts old vs new
Dropping "unimportant" pages with backlinksPermanent authority lossCheck the backlink export first
Templated titles replacing written onesSlow, broad ranking declineDiff metadata against the baseline
Sitemap lastModified = build timeEvery URL claims to change each deployEmit a real content date
Staging indexedDuplicate content competing with younoindex and auth
Killing the old host at cutoverRedirects stop; everything 404sKeep it a month minimum
Declaring success at week twoThe dip is normal; so is the recoveryJudge at 6–12 weeks

Frequently asked questions

How long until rankings recover after a migration? For a same-domain, same-URL framework migration, often no visible dip at all. Where URLs changed, expect a dip within days, partial recovery in two to four weeks, and stability by six to twelve. Anything still down at week twelve is not "settling" — it is a defect, and it is almost always a redirect that was missed, a page that lost its internal links, or content that did not come across.

Do I need to keep the old URL structure forever? No. Keep the 301s for at least a year. After that, Google has consolidated the signals into the new URLs and the redirects mostly serve human visitors and old links. Consolidating them into pattern rules is fine; deleting them entirely is a small permanent loss for anything still linked externally.

Does moving from WordPress to Next.js hurt SEO by itself? No. Google indexes rendered HTML and a statically rendered Next.js site is as crawlable as a PHP-rendered one. What hurts is what tends to travel with the move: a plugin's structured data, sitemap and redirect handling all disappear at once, and each has to be deliberately rebuilt. Inventory what your CMS was doing for SEO before you switch it off — that list is longer than most teams expect.

Should I migrate the blog at the same time as the marketing pages? If they share a domain and a URL structure, yes — a half-migrated site means two stacks, two deploy paths and two sets of metadata conventions, which is how drift starts. If the blog is on a separate subdomain or a hosted platform, treat it as its own migration with its own baseline and its own redirect map.

Is a domain change different from a framework change? Substantially. A domain change needs everything here plus the Search Console Change of Address tool, updated canonicals and internal links pointing at the new domain, and outreach to update your most valuable backlinks. Expect a longer dip. Do not combine it with a framework migration.

Migrating to Next.js? Start from a finished site

The fastest way to keep a migration to one variable is to not redesign while you rebuild — take a template that already matches the shape of your current site and port the content into it.

ASoc Zenith is an AI-led growth-marketing studio site with six services, case studies and partner badges — the multi-service structure most agency sites are migrating out of a builder. ASoc Amplify is a social-media-management SaaS site with a services grid, results stats and a journal section, so the blog tier comes with it rather than being bolted on afterwards. ASoc Seeker markets an AI keyword-research tool across a benefit grid, use cases and two pricing tiers.

All three ship statically rendered with per-page metadata, canonicals and structured data already wired. Browse the Next.js landing page templates or the Tailwind landing page templates. If you have not settled the framework question yet, Webflow vs Next.js and Astro vs Next.js are the two decisions this post picks up from.

Keep reading

Guide11 min read

How to Evaluate a Website Template Before You Buy: 10 Checks

Judge it on what you cannot fix after buying. Run Lighthouse on mobile, audit the inner pages, count the routes that actually exist — and read the licence for seats, not domains.

Read more