This post is for the CTO, digital head or technical marketer at a hospital group that is rebuilding its website, and for the agency or in-house team that will build it. The scenario is a hypothetical group with 25 locations across several cities, several hundred doctors, dozens of departments and a marketing team that also needs local autonomy. The question is how to structure a Next.js front end and a headless CMS so that the group can publish consistently, local teams can edit safely, and search engines see clean, distinct pages.
It is a reference architecture, not a product recommendation. The principles hold whichever headless CMS you choose, and the Next.js specifics are checked against the current documentation. The strategic side of multi-location marketing is covered in SEO and branding for hospital chains and scaling without chaos; campaign structure is in multi-department PPC architecture. This article stays on the build.
Why headless for a group of this size
A traditional single CMS works until three things collide: many editors with different rights, content shared across many pages (a doctor appears at two locations, a department at ten), and a front end that must be fast on mobile networks. A headless CMS separates the content store and editing interface from the presentation layer. The front end fetches structured content through an API and renders it. For a hospital group, the main benefit is not novelty; it is that content is modelled once and reused in many places without copy and paste.
The cost is real. You build and maintain a front end, you need a preview workflow, and the team needs conventions. If the group has three locations and a handful of doctors, a well-run conventional site may be the better choice. The architecture below pays off when duplication and governance are the daily problem. Our headless CMS development service and multi-location website work cover how we scope this.
The content model: entities before pages
The most important decision is the content model. Pages are views over entities. A workable set of types for a hospital group:
| Entity | Key fields | Relationships |
|---|---|---|
Group | name, brand assets, default SEO templates | has many Locations |
Location | name, slug, address, geo, phone, opening hours, services offered, emergency flag | belongs to Group; has many Placements and LocationServices |
Department | name, slug, summary, body | has many Services; has many Doctors |
Service (condition or procedure) | name, slug, summary, body, reviewed-by, reviewed-on | belongs to Department |
Doctor | name, slug, registration details, qualifications, languages, bio, photo | has many Placements |
Placement | doctor, location, department, consultation days, booking link | joins Doctor to Location |
LocationService | location, service, local notes, price page link | joins Location to Service |
Article | title, body, author, reviewer | tagged with Departments and Services |
The two join types matter most. A doctor is one person with one profile, but may consult at three locations on different days. If you model "doctor" as a field on each location, you create three copies that drift. With a Placement entity, there is one Doctor record holding the bio and qualifications, and the location-specific data (days, booking link) lives on the placement.
A LocationService entity serves a similar purpose: the group-level Service content (what the procedure is, clinical review) is written once, while the local page can add what is specific to that site (equipment available, the local contact). Patients get a page that is distinct for their location without 25 hand-written copies of the same clinical text.
Clinical fields such as reviewed-by and reviewed-on are there because clinical content needs a named reviewer; the provider's clinical team must approve what is published. Make them required for publishing, not optional.
URL design
URLs should express the entities and stay stable. A pattern that works:
/locations/{city}/{location}for location pages/departments/{department}for the group-level department/departments/{department}/{service}for group-level service content/doctors/{doctor-slug}for the single canonical doctor profile/locations/{city}/{location}/{department}for the local department view
The doctor profile lives at one URL. Location pages link to it and show the placement details. The point is that the model, not the template, prevents duplicate doctor pages across branches. Where a local page legitimately reproduces group-level content, give it a self-referencing canonical only if it has meaningfully different content; otherwise point it to the group-level page. Google's guidance on consolidating duplicate URLs describes the rel="canonical" link element as a strong signal and the sitemap as a weak one, and notes that Google makes the final decision, so the content must also be genuinely distinct.
Slugs should be generated once and locked after publishing. If an editor can rename a slug freely, links break. Store a redirect when a slug does change.
Rendering: static pages with on-demand updates
Most hospital pages change infrequently: a service page is edited a few times a year. Incremental Static Regeneration fits. The Next.js ISR guide explains that pages are prerendered, served from cache, and regenerated after invalidation, with generateStaticParams listing the pages to prerender and dynamicParams controlling what happens for paths not prerendered at build time. At 25 locations and hundreds of doctors you will have thousands of pages. Prerender the high-traffic ones (locations, departments, the top doctors) and let the rest generate on the first request.
Use time-based revalidation as a safety net and on-demand revalidation as the real mechanism. The same guide recommends a high time-based interval and on-demand updates when you need precision.
Tag design
On-demand invalidation by tag is the useful part for a connected content model. In the current Next.js documentation, `revalidateTag` takes a tag and a profile; the recommended profile is max, which serves stale content while fresh data loads in the background. The single-argument form is deprecated. When a webhook from outside the app calls a Route Handler, the documentation says updateTag is not available, and you can pass { expire: 0 } if the data must go immediately.
Tag every fetch with the entities it depends on. A scheme that scales:
doctor:{id}on any fetch that includes a doctorlocation:{id}on any fetch that includes a locationdepartment:{id}andservice:{id}likewiseplacement:{id}for the join records- a coarse
navtag for the global navigation
When an editor publishes a change to a doctor, the CMS webhook sends the entity type and ID to a Route Handler. The handler validates a shared secret, maps the event to tags, and calls revalidateTag for each. Every page that fetched that doctor is marked stale and regenerates on its next request. When a doctor is moved between locations, you invalidate both locations and the doctor. Keep the mapping in one function with a test, because wrong tags cause the worst kind of bug: pages that look fine and are quietly out of date.
Two caveats from the ISR documentation matter for a group site. First, a revalidation is triggered by a request, not by the call, so pages refresh as they are visited. Second, when you run multiple instances, the default file-system cache is per instance, so on-demand revalidation only invalidates the instance that received the call. If you self-host behind a load balancer, plan a shared cache handler or deploy on a platform that handles it. Test this before launch by publishing a change and requesting the page through the load balancer several times.




