This post is for the technical lead, agency partner or in-house SEO of a hospital that has moved, or is about to move, to a Next.js front end with a headless CMS. Headless builds are a sensible choice for hospital groups: one content source, many templates, fast pages. They also move a set of responsibilities from the CMS plugin to your own code, and those responsibilities are where the SEO failures live.
What follows are five failure modes, each with how it appears, how to confirm it with a command or a report, and how to fix it. They come from our own Next.js build for this site and from audits of sites built the same way. We do not report counts or percentages here, because the point is the diagnosis, not a league table. Version-specific behaviour is taken from the Next.js documentation current at the time of writing; check the version you run.
None of these are bugs in Next.js. Each is an interaction between the framework, a proxy or CDN, and a content workflow, which is why a staging test often misses them.
1. Metadata that streams after the first byte
In current App Router versions, generateMetadata can be streamed. The Next.js generateMetadata reference explains that streaming lets Next.js send the initial UI without waiting for metadata, and appends the resulting tags to the <body>. The documentation says this has been verified with bots that execute JavaScript and inspect the full DOM, such as Googlebot. For HTML-limited bots that cannot run JavaScript, such as facebookexternalhit, metadata continues to block rendering and appears in <head>.
The risk is not Googlebot; it is everything else that reads your title, description or canonical. A crawler that does not execute JavaScript, a link-preview service, a monitoring tool or an SEO audit script may find no <title> in the head. The documentation notes that Next.js detects HTML-limited bots by user agent and that the htmlLimitedBots option overrides the list.
How to confirm. Fetch a doctor, department and blog URL with curl and look for the title, description and canonical in the response. Repeat with a user agent from the HTML-limited list. Then compare with the rendered HTML from the URL Inspection tool in Search Console.
Fix. Decide deliberately. If any tool that matters to you reads only the raw HTML, keep metadata blocking for it by extending htmlLimitedBots, or disable streaming for the whole site, accepting the documented trade-off in response time. Also keep generateMetadata fast: a slow CMS call there delays the entire document for bots that block.
2. Trailing-slash redirect loops
By default, Next.js redirects URLs with a trailing slash to the version without. The trailingSlash configuration reverses that: with trailingSlash: true, URLs without a slash redirect to the slashed version.
The loop appears when another layer holds the opposite opinion. A web server, CDN rule or platform setting that appends a slash will fight an application that strips it. The browser shows "too many redirects"; a crawler logs a chain that ends in an error. Because each layer works correctly in isolation, the fault only shows when they run together, typically after a CDN change or a migration.
How to confirm. Run curl -sIL on a URL with and without the trailing slash and read every hop. Check the status code of each hop, the Location header, and whether the final URL differs from the canonical in the page.
Fix. Choose one form and enforce it at the outermost layer that handles the request. Make the application configuration match, list only that form in sitemaps and internal links, and make the canonical tag produce it. Test the root path, deep paths, paths with query strings, and file URLs such as the sitemap and robots.txt, which are exempt from slash rules in some setups.
3. ISR pages that stay stale
Incremental Static Regeneration serves a prerendered page and regenerates it in the background. The ISR guide describes the sequence: after the revalidate period passes, the next request still receives the cached stale page, and regeneration starts in the background; the following request gets the new page. That is the intended behaviour. For a hospital site it has consequences.
- A page for a doctor who has left can remain live for at least one more request after the CMS record is unpublished, and longer if regeneration fails. The guide notes that when revalidation throws an error, the last successful data keeps being served.
- The documentation says that when you run several instances, the default file-system cache is per instance, so on-demand revalidation invalidates only the instance that receives the call. Without a shared cache handler, some visitors see a new page and others see an old one.
- The guide also notes that proxy logic is not executed for on-demand ISR requests, so revalidating a rewritten path rather than the real one does nothing.
- Dynamic routes for unknown slugs should return a 404. Check how
dynamicParamsand your not-found handling behave for deleted records.
How to confirm. The x-nextjs-cache response header reports HIT, STALE, MISS or REVALIDATED, as the guide describes. Request the same URL several times from different network paths and compare header values and the page body. Change a record in the CMS, trigger your webhook, and measure the time until every instance serves the new content.
Fix. Use on-demand revalidation by path or tag on publish and unpublish events, not only time-based revalidation. Revalidate the exact public path. Use a shared cache handler if you run more than one instance. Log failed revalidations. Make sure lastmod in the sitemap changes when real content changes, not on every build.




