Every published chapter
The healthcare website migration playbook
5 chapters so far, in reading order. Each one answers a question, answer first. New chapters are added to this page as they are published.
Chapter 1
Should you migrate at all?
Not necessarily. Score the site on eight areas, 1 to 5. Under 20 of 40, migrate. Over 30, fix what you have. In between, the low rows decide, not the total.
Not necessarily. Score your current site against eight things (editorial independence, content model, performance, accessibility, SEO resilience, compliance posture, integration health, operational risk) on a 1 to 5 scale. Under 20 out of 40, the platform is the problem and a migration is the cheaper fix over a three-year horizon. Over 30, keep what you have and fix the specific items. In between, it depends on which items are low, and I'll tell you how to read that below. A rebuild is expensive and disruptive; it should be the conclusion of an honest audit, not the starting assumption.
I sell migrations, so you should hold this chapter to a higher standard than the rest. The reason I can afford to be honest here is that a bad-fit migration is a bad project for me too.
What is the site actually failing at?
Usually one of two things: the team can't change it without a developer, or it broke something last time it was touched. Everything else is a symptom of those.
Before scoring anything, write down the three most recent times the site got in the way. Not "it's slow" in the abstract; the actual incidents. The new location page that took three weeks. The form that legal found out about. The redesign that lost a third of the search traffic. Those three stories tell you which rows below matter most, and they're what you'll use to explain the decision to whoever signs it off.
How do you score it?
Eight areas, each scored 1, 3, or 5, where 5 means there's nothing to do. Use the middle score when you're between descriptions. Be harsh; the score is for you.
- Editorial independence. 1: a developer is needed for any change. 3: pages are editable, structure isn't; a new section or page type needs a ticket. 5: the marketing team ships pages and sections unaided.
- Content model. 1: page-builder soup, every page a one-off, nothing reused. 3: some structured types (a provider or location type exists) but most pages are freeform. 5: structured and reusable; a provider or location entered once appears everywhere it should.
- Performance. 1: Core Web Vitals red on mobile in field data. 3: mixed. 5: green field data and a budget that keeps it that way.
- Accessibility. 1: serious WCAG failures on core templates (unlabelled forms, keyboard traps, no alternatives for media). 3: AA gaps on some templates. 5: AA, tested, with an accessibility statement.
- SEO resilience. 1: no redirect discipline, thin or duplicate metadata, no idea what ranks. 3: the basics are present. 5: structured data, canonicals, and a process for redirects when things move.
- Compliance posture. 1: health details in forms or analytics, no consent gate. 3: consent banner present, with gaps (tags fire before consent, free-text forms go to a marketing CRM). 5: PHI kept out by design, consent-aware tags.
- Integration health. 1: undocumented and brittle; nobody knows what happens when the scheduling widget changes. 3: documented, some fragility. 5: documented, owned, monitored.
- Operational risk. 1: a single vendor holds the keys, no backups, no runbook. 3: partial. 5: accounts in your name, backups tested, a runbook someone else could follow.
Add them up.
What does the total mean?
Under 20: migrate. Over 30: don't. Between: read the rows, not the total.
Under 20. Low scores across the board mean the platform and the way it was built are the cause, not any single feature. Remediating in place would cost a meaningful share of a rebuild and leave the structure untouched, so you'd be back here in two years. Plan the migration, and use the low rows as its requirements list.
Over 30. The site is in better shape than the rebuild conversation implies. Something specific is wrong, and the total tells you it's fixable in place. Fix the one or two low rows, keep the platform, and put the budget toward content or conversion instead. If a vendor pitches a rebuild at this score, ask them to show you which rows change.
20 to 30. This is most sites, and the rows that are low decide it. Low editorial independence and content model scores point to migration, because those are structural: a page-builder site doesn't become a structured one through remediation. Low performance, accessibility, or compliance scores with decent structure point to remediation, because those are fixable on any platform by someone competent. Low operational risk is urgent either way and shouldn't wait for a migration decision.
When is WordPress fine?
When the team can publish what they need to, the structure matches the content, and nobody is afraid to touch it.
WordPress runs a large share of healthcare sites and plenty of them are fine. A well-built WordPress site with a sensible custom-post-type structure, a small plugin footprint, good hosting, and someone who maintains it can score 30 or better on the rubric. If that's you, this playbook still has useful chapters (redirects, compliance, accessibility apply to any platform) but the migration chapters don't, and I'd rather you spent the money elsewhere.
The WordPress sites that score under 20 are usually not WordPress problems. They're page-builder problems, plugin-accumulation problems, and "the agency that built it is gone" problems. Migrating fixes those because it forces a rebuild of the structure, not because the new platform is magic. Be clear about which one you're buying.
What if you can't score it yourself?
Then that's the first finding: nobody on your side knows how the site works, and that's an operational risk score of 1 on its own.
You can get the score two ways. Have someone technical you trust spend a day with the rubric, the analytics account, and the CMS; that's enough for a rough but honest number. Or pay for an assessment, which is the same rubric done properly, with evidence for each score and a plan and a price at the end. I do those at a fixed fee, and the report is yours to use with any vendor, including your current one. Either way, get the number before anyone quotes you a rebuild.
Checklist
- Write down the three most recent times the site got in the way
- Score all eight areas, 1 / 3 / 5, harshly
- Under 20: plan the migration; low rows are the requirements
- Over 30: fix the specific rows in place; decline the rebuild pitch
- 20 to 30: structural rows low means migrate; fixable rows low means remediate
- Operational risk at 1: fix now, regardless of the migration decision
- Can't score it: that's the finding; get someone to score it before anyone quotes a build
No starter code for this chapter. The rubric is the same one used in the Dock90 assessment.
Reviewed August 29, 2026 · This chapter on its own page
Chapter 2
What are you actually migrating?
Not the pages: the URLs, forms, scripts, integrations, redirects, linked files, and legal pages the old site accumulated. One spreadsheet; it's the contract.
# What are you actually migrating?
Not the pages. You're migrating a set of obligations the old site accumulated while nobody was looking: URLs that rank and are linked to, forms and wherever they send what people type, scripts and pixels and what they leak, integrations nobody documented, redirects already in place from the last migration, files linked from other sites, and legal pages with dates on them. Written down in one spreadsheet, that inventory is the contract for the whole project. It defines what done means, it prices the work, and it's what you check against on launch day.
The pages are the visible part. Most of what goes wrong in a migration was never a page.
Why isn't it the pages?
Because pages are the one thing everybody already knows to move, and because on the new site most of them stop being pages.
The failures I've seen or cleaned up after were not missing pages. They were a contact form that had been quietly posting to a marketing vendor for four years, and the vendor had never signed a business associate agreement. A remarketing pixel on condition pages that nobody on the marketing team knew was there. A patient intake PDF that three referring practices linked to from their own sites, gone the day the new site launched. A hundred and forty query-string URLs from an old newsletter tool that still got clicks. An appointment widget whose account was registered to an employee who left in 2021. None of those show up when you count pages.
The other reason is chapter 3: on a structured site, sixty to eighty percent of the old pages become records (providers, locations, services, FAQs) rather than pages. So "migrate 300 pages" is the wrong unit from the start. The right unit is the row in the inventory, and each row is a URL, a form, a script, an integration, or a file, with a fate.
What goes in the inventory?
Seven lists, joined where they share a URL. The first one is the biggest; the other six are where the surprises live.
- URLs. A full crawl of the old site joined with twelve months of Search Console's Pages and Links reports, so every URL carries its traffic, its impressions, and its inbound links. Include the ones the CMS doesn't know about: attachment pages, tag archives, paginated feeds, author pages, campaign URLs with query strings. Chapter 4 covers how to build this list and turn it into the redirect map.
- Forms. Every form on the site: the page it's on, where the submission goes (an inbox, a CRM, a vendor, a plugin's database table), the fields, whether any field is free text, which fields could carry health information, what notice or consent is shown, and whether the receiver is under a BAA. Submit each one with test data and follow where it lands. Plugin-stored submissions on the old host are a data set you'll need to export and then delete.
- Tags and scripts. Export the tag manager container if there is one, then open a fresh browser profile with the network panel on and load one page of each template. Record every third-party request: vendor, what loaded it, which pages it fires on, whether it fires before any consent, and what it sends. This list is what chapter 5 is about; the inventory is where you find out how bad it is.
- Integrations. Scheduling, CRM, email marketing, careers, reviews, chat, maps, payments, patient portal links, the analytics account itself. For each: what it does, who owns the account (the organization, a vendor, an ex-employee, unknown), who has admin access, and whether it's documented anywhere.
- Existing redirects. The old site already has redirects, from a previous migration, a plugin, or the host's configuration. Export them. Any URL that already redirects needs its original address mapped straight to the final destination, not chained through the old hop.
- Assets linked from outside. PDFs (patient forms, price transparency files, research posters), images that other sites embed, and anything a partner, a directory, or a hospital system links to. The Links report and a backlink tool find most of these; the crawl finds the rest. Each one is kept at the same path or redirected individually.
- Legal pages. Privacy policy, terms, accessibility statement, notice of privacy practices, and any state-specific privacy disclosures, each with its effective date and whoever last approved it. These get re-reviewed for the new site because the new site collects different things.
An eighth list isn't content, but it belongs in the same spreadsheet: access. Who can log in to the registrar, DNS, the current host, the CMS, the analytics and Search Console properties, the tag manager, and each integration. Every migration I've run has stalled at least once on an account nobody could get into.
Who fills it in?
The crawl, the joins, the network-panel work, and the redirect export are technical and take a developer or an assessor a few days. The columns that need a human who knows the organization take the client weeks, and that is the part to start first.
The client's columns are: who owns this page or form or integration, whether it should survive, and what it should become. Those questions go to the marketing lead, the practice managers, the person who set up the CRM, and sometimes counsel. In a four-month build, this is the work that determines whether month one is productive or spent waiting. Of the three things clients reliably underestimate (content, approvals, and access; chapter 8 is about all three), two of them are exposed by the inventory in week one, which is the point of doing it in week one.
Why is this the contract?
Because every row gets a fate, and a row with a fate is a testable statement about the finished site.
Each URL is kept, merged into another, redirected, or retired. Each form is rebuilt with a named receiver, or removed. Each script is kept behind consent, replaced, or dropped. Each integration is carried over with a named owner, or cut. Each asset stays at its path or gets a redirect. Once the client has marked those columns up, the inventory does four jobs at once. It defines scope: anything not in it is a change, and a change has a cost. It prices the build: the counts by proposed type are the content model, and the number of forms and integrations is most of the integration work. It sets the launch gate: the parity check in the starter reads the old sitemap and refuses to pass until every address resolves on the new host, and the redirect import takes its rows straight from the map. And it's the record you compare against in the month after launch, when a 404 shows up in Search Console and you need to know whether it was ever supposed to exist.
A number from experience, offered as a range: a site the client describes as "about eighty pages" has between three and nine hundred URLs once the crawl and the Links report are joined. The gap between those two numbers is the migration.
What does the spreadsheet look like?
One workbook, one tab per list, and columns that force a decision. The content inventory is the core tab, one row per URL: template, content type, title, status code, indexable or not, word count, organic and total sessions for the last ninety days, conversions, external inbound links, last modified, owner on the client side, action (keep, merge, redirect, kill), target URL, proposed document type on the new site, priority, notes. The redirect map is generated from the rows where the action is merge or redirect, with columns for testing before launch, a day after, and two weeks after.
The forms tab, the tags tab, and the integrations tab carry the columns described above, each ending in a finding and a recommended action. The access tab is a checklist: item, what it's needed for, requested, granted, by whom, at what level. When I run an assessment, this workbook is half the deliverable and the client keeps it; it's the same workbook the build then runs on.
Keep the free text short. The spreadsheet carries facts and decisions; the reasoning goes in a document.
What can you skip?
Judging content quality, designing the content model, and anything with no traffic, no links, and no owner.
Content quality is a separate pass, after the inventory, and usually after launch; migration day is not the day to rewrite service pages. The content model is chapter 3, and it's designed from the inventory's counts, not alongside them; for now, assign each row a proposed type and move on. And a URL with zero sessions, zero impressions, and zero inbound links in twelve months gets marked kill and a 410, which is honest, rather than a redirect to the homepage, which is a soft 404 that costs crawl budget. Don't spend an hour deciding the fate of a 2019 holiday-hours post.
What you can't skip is the join. A crawl without Search Console data can't tell you which pages matter; Search Console without a crawl misses everything that isn't indexed. The inventory is the intersection, and doing only half of it is how a migration loses the pages nobody remembered were valuable.
Checklist
URLs and files
Everything that isn't a page
People and access - [ ] A client-side owner named for every row that needs a decision - [ ] Access checklist filled at kickoff: registrar, DNS, host, CMS, analytics, Search Console, tag manager, each integration - [ ] Accounts registered to former employees identified and transferred before the build depends on them Companion code in the Healthcare Sanity Starter: docs/MIGRATION.md ("Four weeks out: inventory" is this chapter's checklist in the repo), scripts/check-url-parity.ts (the launch gate that reads from the URL list).
Reviewed August 29, 2026 · This chapter on its own page
Chapter 3
How do you model content for a multi-audience healthcare site?
Ten document types, eight sections, audience as a field on pages, and providers, locations, and services as records entered once. That is the whole model.
# How do you model content for a multi-audience healthcare site?
Ten document types, eight section types, audience as a field on pages rather than a folder or a second site, and providers, locations, and services as structured records that are entered once and rendered everywhere they belong. That is the whole model for most clinic, practice, health-system, and life-sciences marketing sites. Anything a specific site needs beyond it is a one-type addition that takes twenty minutes, and the discipline is in what you leave out, not in what you add.
The model has one job: let the marketing team build and change pages without a developer while the site stays coherent as they do it. Every decision below is in service of that. If a modeling choice makes editors more capable but the site less consistent, or the other way round, it's the wrong choice.
Why is audience a field and not a folder?
Because the audiences share the same providers, locations, and services, and a folder (or a separate site) forces you to enter those twice and keep them in sync by hand.
A healthcare site serves patients, referring providers, partners, and, for life sciences, investors. Their pages differ; the facts behind the pages don't. A cardiologist has one bio, one set of locations, one accepting-patients status, and both the patient page and the referral page should read them from the same record. So in the starter, audience is a field on the page document: patients or providers, with a radio button in the Studio and patients preselected. That one value drives the navigation the page shows, the URL prefix (provider pages live under /for-providers), and how the Studio groups pages for editors. One dataset, one codebase, two front doors.
Two or three audiences is the right number to structure around. The others (careers, press, caregivers) get a page, not a section. I wrote about how to decide which audiences count in Patients, providers, partners; this chapter is about what the decision looks like in the schema.
Why ten types, and which ten?
Ten because that's the set that appears on almost every healthcare marketing site, and each one either has its own URL or is referenced from more than one place. That's the test for a type.
- page. Title, slug, audience, a list of sections, SEO. The page-builder document; most of the site is these.
- service. A service line, condition, or offering. Summary, body, FAQs, related providers, medical review. Has its own URL.
- provider. Name, credentials, title, specialties, accepting patients, headshot, bio, locations. Has its own URL.
- location. Name, address, coordinates, phone, structured hours, providers. Has its own URL.
- post. Title, author, body, medical review, SEO. The blog.
- person. Author or reviewer: name, credentials, role, bio. No URL of its own; rendered as bylines and in structured data.
- faq. Question, answer, a category string for grouping. Referenced from services and from FAQ sections.
- legalPage. Privacy policy, terms, accessibility statement, notice of privacy practices. Title, body, effective date.
- redirect. From, to, permanent. Redirects are content, so editors can see and manage them (chapter 4).
- settings. One document: organization name and logo, contact, social, default SEO, analytics id, consent copy, header and footer navigation per audience.
What is not on the list: insurers, jobs, press releases, events, research, clinical trials. Every one of those is real on some sites and absent on most, so they're add-ons. In the starter a new type is a schema file, a query, a route, and a line in three lists; the walkthrough in ADDING-A-TYPE.md does insurers end to end in about twenty minutes. The rule I hold to is that a type earns a place in the base model only after two real projects needed it. In your project, add what the client needs, and nothing the client might need.
Services or conditions?
One type for both, with the title and the URL doing the work.
Patients search for conditions and symptoms ("knee pain", "sleep apnea test"). Organizations think in service lines ("Orthopedics", "Sleep Medicine"). Modeling those as two types produces two trees that have to be cross-linked and kept in agreement, and within a year the condition pages are stale because nobody owns them. Modeling them as one service type means every entry gets the same fields (summary, body, FAQs, providers, review) and the same template, and the only decision per entry is what to call it. Name and slug it the way patients search; put the department name in the body.
If a site truly needs to group services, add a category string field first, the way faq has one. Add a separate taxonomy type only if the categories need their own pages, which is rare on a marketing site.
Why are providers and locations first-class?
Because they're the pages that rank for local search, they're the data behind Physician and MedicalClinic structured data, and they change more often than anything else on the site. A record entered once and referenced everywhere is the only way those three facts stay true at the same time.
A provider carries what a patient and a referrer both need: name without credentials, credentials separately, a title (Cardiologist, Nurse Practitioner), specialties as tags, whether they're accepting patients, a bio, and the locations they practice at. A location carries a structured address, coordinates, a display phone number (the dialable link is derived), and hours as days, opens, and closes rather than free text, because the openingHoursSpecification in the structured data needs them that way and a free-text "Mon–Fri 8–5" can't be turned into it reliably.
References go one direction: provider to locations. A location's provider list is derived from that unless an editor curates it explicitly. The same pattern applies to the page sections: a providers section with no references shows every provider (the directory); with references it shows a chosen subset. So when a physician moves to a different office, one document changes and the directory, the location page, the service pages that list them, and the structured data all follow. On a site where provider bios are copied into page bodies, that same move is a search-and-replace across the site and something always gets missed.
Where does medical review live?
As two fields, reviewedBy and reviewedAt, on the types that make clinical claims: service and post.
The reviewer is a person document, so a physician who reviews forty pages is one record with one set of credentials, not forty retyped bylines. The fields render as a byline on the page and go into the Article structured data, which is what search engines look at when deciding whether health content is trustworthy. The Studio requires a review date when a reviewer is set, and warns (rather than blocks) when a post has no reviewer, because some posts are operational (new parking, holiday hours) and don't need one.
The same two fields are the audit trail from chapter 5: when counsel or a regulator asks who signed off on a clinical claim, the answer is on the page.
What are sections, and why only eight?
Sections are the blocks an editor composes a page from: hero, rich text, call to action, cards, FAQs, providers, locations, form. Eight because each one either presents a structured type or holds prose, and once you go past that you're building a design tool, not a content model.
The distinction matters. A providers section doesn't contain providers; it references them, or shows them all. An FAQs section references faq documents. The section decides layout; the document holds the facts. That's what keeps a page-builder from decaying into page-builder soup, where every page is a one-off and nothing is reused.
The same principle limits the navigation. Header nav is capped at six links and one button per audience, enforced by validation in the Studio. A site that needs a mega-menu needs a different starting point, and it's better to find that out in week one than at launch.
What should stay out of the CMS?
Three things: anything a patient typed, anything that's configuration rather than content, and anything that lists everything.
Form submissions never touch the CMS. The CMS holds the form's definition (fields, which receiver it posts to); the submission goes to the receiver and nowhere else. The Studio warns when a field key looks like protected health information (date of birth, diagnosis, member id), which is a nudge for the editor and a tripwire for the reviewer. Chapter 5 covers why.
Secrets and integration configuration live in environment variables, not in settings. The analytics id and consent copy are content (marketing owns them); the webhook URL that receives forms is not.
Index pages (/providers, /locations, /services, /blog) are code, not page documents. They list everything and need no editorial input; making them editable only creates a way to accidentally hide half the providers. Those slugs are reserved so an editor can't create a page that collides with one.
How do you move existing content into this model?
Map, don't lift. Every URL in the inventory from chapter 2 gets assigned a type before anything is moved, and most old pages turn out not to be pages.
On a typical WordPress site, provider bios are pages or a plugin's custom post type; they become provider documents. Location pages become location documents. Condition and service pages, often forty or more, become service documents with the same template. FAQ blocks scattered across pages become faq documents that services reference. The blog becomes posts with an author and, where the content is clinical, a reviewer. What's left, usually the homepage, about, a handful of campaign pages, and the audience landing pages, becomes page documents built from sections.
In my experience, somewhere between sixty and eighty percent of the old page count collapses into structured records. That's the number to put in front of the client early, because it changes what "migrate the content" means: less copying, more deciding. Structured data, sitemaps, and internal linking then come from the model rather than from anyone remembering to add them.
Checklist
Model
Editing
Migration - [ ] Every inventoried URL assigned a type before any content moves - [ ] Provider and location records deduplicated: one document each - [ ] Structured data checked on one provider, one location, and one service page after import Companion code in the Healthcare Sanity Starter: sanity/schema/ (the ten types and eight sections), docs/ADDING-A-TYPE.md (adding a type end to end), docs/DECISIONS.md (the schema decisions and why).
Reviewed August 29, 2026 · This chapter on its own page
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.
Reviewed August 29, 2026 · This chapter on its own page
Chapter 5
What is a healthcare marketing site allowed to collect?
Contact details, with consent, sent somewhere you control. Not health details. Make it true by design: webhook-only forms, consent-gated tags, typed events.
Contact details, with consent, sent somewhere you control. Not health details. The rule I build to is that a marketing site should hold no protected health information at all, and the way to make that true is architecture, not policy: forms post to a receiver and never store anything, analytics loads only after consent and can only send events you've defined in advance, and nothing else loads from anyone else. A site that holds no PHI has nothing to breach, and that is a much stronger position than a site that holds a little and tries to protect it.
This chapter is what I'd tell a marketing lead before their first legal review. It is not legal advice; it's the engineering side of the conversation, so the legal side goes faster.
Why should a marketing site hold no PHI?
Because the moment it does, HIPAA applies to the site, and a marketing stack was never designed to meet it.
Protected health information is health information that can be tied to a person. On a marketing site, that's usually created by accident: someone fills in "reason for visit" on a contact form, the form emails the submission to a shared inbox, the inbox is hosted by a vendor with no business associate agreement, and now a patient's name and condition sit in three systems nobody vetted. None of that was a decision. It was a default.
The alternative is to decide, up front, that the site is a brochure with a doorbell. It tells patients what you do and how to reach you, and the reaching happens somewhere built for it: a scheduling vendor under a BAA, a phone line, a portal. Once that's the design, the compliance conversation about the website gets short, and the long conversation moves to the systems that should be having it.
Where do form submissions actually go?
To a webhook receiver you configure, and nowhere else. The receiver is the real compliance boundary, not the form.
In the starter, the form section posts to a single configured URL. The server action forwards the submission and never logs the request body, nothing is written to Sanity or to a database, and there's no submissions inbox to secure because there are no submissions stored. The site is a pass-through.
That moves the question to the receiver, which is where it belongs. Whatever sits at that URL is where the data lives: a CRM, an email service, an automation tool, your own endpoint. Ask two things about it:
- Could a patient ever type something clinical into this form? If there's a free-text field, the answer is yes, whatever the label says.
- Is the receiver allowed to see that? Meaning: is there a BAA, and is the vendor operating as a covered system?
If the answers are "yes" and "no", you have three choices, in order of preference. Remove the free-text field. Route the form to a receiver that does have a BAA. Or keep the form as contact-details-only and send anything clinical to the phone or the portal, which is what the starter's seeded form copy does in plain words: don't include medical details here.
What counts as PHI in a contact form?
Less than people fear and more than they want. The test is whether a health fact can be tied to a person, and a contact form ties everything to a person by design.
Name, email, and phone on their own are not PHI. They're contact details, and every business collects them. The health part enters through fields like these:
- A free-text "message" or "reason for visit" field. Anything a patient writes there could be clinical.
- A dropdown of services or conditions on a form that also collects a name. Choosing "oncology second opinion" next to your name is a health fact tied to a person.
- Date of birth, insurance ID, member number, medical record number. These exist to identify patients within clinical systems and have no business on a marketing form.
- The page the form sits on. A generic "contact us" form submitted from /conditions/hiv-care carries the condition with it if the page URL is included in the submission, and most form tools include it.
The starter warns editors in the Studio when a form field key looks like one of these (dob, ssn, mrn, diagnosis, condition, medication, insuranceId), with a note explaining why. It's a guard rail, not a guarantee: it looks at what the field is called, not at what a patient types. That's why the copy matters as much as the schema.
Why do analytics tags need consent on a health site?
Because a pixel on a page about a condition can send what amounts to a health fact about the visitor to a third party, and regulators have gone after health companies for exactly that.
The mechanism is mundane. A visitor reads a page about a specific condition. The advertising pixel on that page sends the page URL, and often an identifier that ties the visit to an account on the advertising platform, to the vendor. Nobody typed anything. The URL did the work. HHS published guidance on tracking technologies on health websites in 2022 and revised it in 2024; the FTC has brought enforcement actions against health companies for sharing browsing data through pixels and chat tools. Several state privacy laws now define "consumer health data" broadly enough to include inferences from browsing, Washington's My Health My Data Act being the most-cited.
The engineering answer is a consent gate, and it has to be real:
- Nothing loads before the visitor agrees. Not "loads but doesn't send". The script isn't on the page. In the starter, GA4 is the only analytics tag and it loads only after analytics consent; Consent Mode v2 signals are sent so Google honours the choice on its side too.
- The gate can't be bypassed by a marketer. On most sites the tag manager is where compliance dies: someone adds a pixel for a campaign and it fires on every page, unconditionally. The starter ships no tag manager and no third-party scripts beyond GA4 and Turnstile on form pages. Adding one is a code change that goes through review, which is the point.
- The consent tool itself sets no cookie and calls no vendor. Consent is stored in the browser's local storage. The tool that asks permission to track shouldn't track.
Check it after launch, from a fresh browser profile, with the network tab open. If anything from a third party loads before you click accept, the gate isn't real.
How do you keep names and emails out of analytics?
Make it impossible to send an arbitrary string.
The usual way PII reaches analytics is an event like track('form_submit', { email: user.email }), written by someone who wanted a conversion count and didn't think about the parameter. In the starter, track() accepts a closed set of event names and typed properties for each. There's no free-form event and no free-form string property, so the mistake can't be made without changing the type definitions, which is again a reviewed change. It's a small amount of code and it removes an entire category of incident.
What about chat widgets, session replay, maps, and scheduling embeds?
Each one is a third party on the page, and each needs the same two questions as the form receiver: what can it see, and is it allowed to?
- Chat widgets see whatever the visitor types, which on a health site includes symptoms. Several of the enforcement actions above involved chat tools. If you need chat, it needs a BAA and it should load after consent, on the pages where it's needed, not everywhere.
- Session replay records the screen, including form fields. On a health site that's a recording of patients typing. I don't put it on healthcare marketing sites.
- Map embeds and scheduling widgets are usually fine, but they load third-party scripts and often set cookies. Confirm what they set, gate them if they track, and confirm the scheduling vendor's BAA since that's where the clinical conversation goes.
The default in the starter is that none of these exist until someone adds them on purpose.
When do you need a BAA?
When a vendor will handle PHI on your behalf. Not for the marketing site itself, if it's built the way this chapter describes, and yes for the systems the site hands off to.
A business associate agreement is a contract that makes a vendor responsible for protecting PHI they handle for a covered entity. A repository can't sign one and a website builder can't make you compliant. What the design does is keep the marketing site out of scope so the BAA conversation is about the right systems:
- Needs one: the scheduling vendor, the portal, any form receiver that could see clinical text, a chat tool that talks to patients, an email service that sends anything clinical.
- Doesn't, if the site holds no PHI: the hosting platform, the CMS, the analytics tool, the bot-protection service. These see marketing content and consented, typed events.
One caveat on the CMS. Sanity stores marketing content. If an editor pastes a patient's story into a post, that's PHI in the CMS, and no architecture prevents it. Train editors, and if your policies require it, Sanity offers HIPAA-eligible plans.
What should legal review before launch?
A short list, if the design above is in place. A long one if it isn't.
- The privacy policy describes what the site actually does: which analytics, gated how, what the forms collect, where submissions go. The starter seeds the privacy policy, terms, accessibility statement, and Notice of Privacy Practices with realistic placeholder text and a "replace before launch" banner; counsel replaces them for your jurisdiction and your state's privacy law.
- Every form's receiver, and whether it's allowed to see what the form could collect.
- Every third-party script, with the page it loads on and whether it loads before consent.
- Who reviews clinical content, and whether that's reflected on the page. The starter carries reviewedBy and reviewedAt on services and posts, rendered as bylines and in structured data. That's for search engines, and it's also how you show a regulator that clinical claims were reviewed.
Checklist
Design
Before launch
After
Companion code in the Healthcare Sanity Starter: docs/COMPLIANCE.md (this chapter's mechanics, in the repo), lib/consent.ts, lib/track.ts.
Reviewed August 29, 2026 · This chapter on its own page