Chapter 4

How do you migrate a healthcare site without losing its search traffic?

Build an exact-match redirect map from a full inventory, keep everything but the URL the same, and don't launch until every old URL resolves on the new host.

You build an exact-match redirect map from a full inventory of the old site's URLs, you keep everything that isn't the URL the same (titles, canonicals, structured data, internal links, images), and you don't launch until an automated check proves every old address resolves on the new host. Do that and you'll see a dip of roughly 10 to 20 percent in impressions for a couple of weeks and then recovery. Skip any part of it and you'll see a cliff, and cliffs on healthcare sites are expensive: the pages that rank are the condition and service pages that bring in patients.

The mechanics are not complicated. They are tedious, and tedium is where shortcuts get taken.

Why do migrations lose rankings?

Almost always because URLs changed and the old ones were not mapped one-to-one to the new ones. Everything else on this page is a distant second.

Each URL carries history: how long it has existed, who links to it, how people behave when they land on it. When a URL disappears, that history goes with it unless a redirect says where it went. A 404 on a page with ten years of inbound links is a page that starts over from zero.

The second cause is a redirect that exists but points somewhere unhelpful, classically every old service page to the homepage. The engine follows it, finds a page about something else, and treats it as a soft 404. The third is quieter: the page moved correctly, but its title, structured data, or internal links changed at the same time, so the engine sees a different page at a new address. That doesn't cause a cliff. It causes a slow, unexplained slide over a month or two.

What goes in the redirect map?

Every URL on the old site that has had a visit or an inbound link in the last twelve months, mapped to exactly one destination.

Build it from the inventory, not from memory. In practice the inventory is three lists joined on URL:

  • A full crawl of the old site. Screaming Frog, Sitebulb, or npx sitemap-generator-cli for a small site. Don't trust the CMS sitemap alone; WordPress sites accumulate URLs nobody knows about (attachment pages, tag archives, paginated feeds, author pages).
  • Search Console's Pages report for the last twelve months. Anything with impressions has a place in the map.
  • Search Console's Links report and, if you have it, a backlink tool. Anything with an external link has a place in the map even if it gets no traffic.

Every row gets one of three fates:

  • Keep. Same path on the new site. The best outcome. Most provider, location, and service pages should be here if the new URL scheme is designed with the old one in mind.
  • Move. Redirect to the closest equivalent page. Closest means a page about the same thing, not a category page above it and never the homepage.
  • Retire. Return a 410 (Gone) or a plain 404. This is the honest answer for the 2019 holiday-hours post with zero traffic and zero links. Redirecting it to the homepage doesn't preserve anything; it just makes the engine work harder to figure out that the page is gone.

Two categories people forget:

  • Assets linked from outside. PDFs (patient forms, price transparency files, research posters), images that appear in other sites' articles, and anything a partner or directory linked to. Either keep them at the same path or redirect each one. A 404 on a PDF that a hospital system linked to is a real loss.
  • Query-string URLs. WordPress's ?p=123 style links and tracked campaign URLs. Path-based redirect systems, including the one in the starter, match paths only. Expand these from your analytics data into explicit rows.

The map itself is a CSV with three columns: from, to, permanent. A few hundred pages takes a day; several thousand takes a week and a spreadsheet the client's team helps mark up. That is why the inventory is the contract for the migration, not a side task.

Exact-match or wildcard redirects?

Exact-match. Always. If you have a pattern, expand it into rows.

Wildcard rules are how sites lose half their rankings. /services/* to /care/* looks correct until a third of the old service pages turn out to have been renamed, a dozen merged, and two retired; the rule sends all of them to URLs that 404 behind a redirect. An exact-match list is longer, but every row is verifiable: you can test it, hand it to the client and ask "is this right?", and diff it against the crawl to know it's complete. None of that is true of a regex.

The starter treats redirects as content: they're redirect documents in Sanity that editors can see and manage, compiled into the Next.js config at build time. The CSV is bulk-loaded with npm run import-redirects -- redirects.csv, which uses deterministic IDs so you can re-run it after fixing rows without creating duplicates, and a --dry-run flag that validates the file (malformed paths, loops, duplicates) without writing anything.

301, 308, or 410?

Use a permanent redirect for anything that moved, a 410 for anything retired, and don't overthink the difference between 301 and 308.

Both 301 and 308 mean "moved permanently"; 308 additionally preserves the request method, which matters for form posts and not for a page a search engine is fetching. Google treats both as permanent and passes ranking signals through either. Next.js emits 308 by default and that's what the starter ships. If a client's SEO agency insists on 301, it's not worth a fight, but it's also not worth an engineering change.

What does matter:

  • No temporary redirects for permanent moves. 302 and 307 say "this might come back", and engines keep the old URL indexed. If your host or CDN defaults to 302, change it.
  • No chains. Old URL to intermediate URL to final URL loses a little signal at each hop and slows every request. If a page moved in a previous migration and moves again now, point the original directly at the final destination. Crawl the old site's existing redirects and fold them into the map.
  • No loops. The dry-run catches these; a redirect that eventually points back at itself takes the page down entirely.
  • 410 over redirect-to-homepage for retired content. A 410 tells the engine to drop the page promptly. A redirect to the homepage tells it to spend crawl budget discovering that the page is a soft 404.

What besides the URL has to stay the same?

Everything the engine used to understand the page. Change the address and the contents at once and you've moved twice. For each kept or moved page, carry over:

  • Title and meta description. Rewrite them later, after the migration settles, as its own project. Migration day is not the day to improve titles.
  • Canonical URL. Pointed at the new page's own address, absolute, using the production domain. The starter derives canonicals, the sitemap, and structured data from one NEXT_PUBLIC_SITE_URL setting so they can't disagree with each other.
  • Structured data. Healthcare sites usually carry MedicalOrganization, Physician, MedicalClinic, and FAQPage markup, and those rich results are worth protecting. Compare the old and new JSON-LD on one provider page, one location page, and one FAQ page. Missing markup after launch is a common cause of the slow slide.
  • Internal links. Every link inside your own content that pointed at an old URL should point at the new one directly, not through the redirect. Content migration scripts should rewrite these; check a sample.
  • Image alt text and filenames. Image search is a meaningful traffic source for some healthcare content (conditions, procedures, facilities). Keep alt text, and keep image paths where you can.
  • Headings and body content. Same H1, same substantive content. Reorganizing and trimming is fine; replacing the page's subject is a new page.
  • Sitemap and robots. The new sitemap should list only final URLs (no redirecting ones) and robots.txt should not be carrying a staging-era Disallow: /. That last one has taken down more launches than any redirect mistake.

What does Search Console need before and after?

Before: a snapshot. After: the new sitemap, and your attention for a month.

Before launch, export from the old property: the Pages report (twelve months), the Links report, and the Core Web Vitals report. This is your baseline. Keep the old property; don't delete or merge it.

On launch day, verify the new domain as a property if the domain changed, submit the new sitemap, and use the URL Inspection tool to request indexing on the twenty or so most important pages. Don't request indexing on everything; it doesn't help and it's rate-limited.

In the first week, watch the Pages report for 404s and "Page with redirect" entries that don't match your map. Every unexpected 404 is a row you missed. Add it to the CSV, re-import, redeploy. This is why redirects live in the CMS in the starter rather than in code: fixing a missing row shouldn't need a developer.

Over the first month, compare impressions and clicks week over week against the baseline, per page for the top fifty and in aggregate for the rest.

What does a normal dip look like, and what's a cliff?

Normal is a 10 to 20 percent drop in impressions for one to two weeks, then recovery to baseline within four to six. A cliff is 40 percent or more in the first few days, or a dip that hasn't started recovering by week three.

Those ranges are from migrations I've run, not from a study. Where most pages kept their paths the dip can be close to nothing; where every URL moved, expect the deeper end.

A cliff has a cause, and it's nearly always one of three things: a missing block of redirects (usually a whole template or section), a noindex or robots disallow left over from staging, or canonicals pointing at the old domain or the staging domain. All three are findable in an afternoon with the Pages report and a crawl of the new site. Start there before anyone starts talking about "algorithm updates".

What's the launch gate?

An automated check that every URL in the old sitemap returns a 200 or a permanent redirect on the new host, with a zero exit code. Not a spot check, not a spreadsheet someone eyeballed. A script that fails the build.

The starter ships this as npm run check-url-parity -- --old https://www.oldsite.com --new https://preview.vercel.app. It reads the old site's sitemap, requests every path on the new host, and reports anything that isn't a 200, 301, or 308. Run it against a preview deployment two weeks before launch and keep fixing rows until it passes. Run it again on launch day against the live domain. Run it once more a week later, because someone will have "tidied up" something.

The gate changes the conversation. Without it, "are the redirects done?" is answered by whoever is most tired. With it, the answer is a number: 1,214 URLs checked, 0 failures.

Checklist

Four weeks out

Two weeks out

Launch day

After

Companion code in the Healthcare Sanity Starter: docs/MIGRATION.md (the full launch checklist), scripts/import-redirects.ts, scripts/check-url-parity.ts.