Next.js SEO: the deploy-level changes that move rankings
A client-only React SPA fails SEO loudly — the crawler gets an empty shell. Next.js fails it quietly. It renders on the server by default, so the page looks fine to you and to Google right up until a one-line diff changes how it's rendered, and nobody notices for a quarter.
If you've already got a Next.js app, you've cleared the bar most SPAs trip over: your pages ship real HTML. So the SEO failures that actually cost you rankings aren't "the content isn't there." They're regressions — a single line in a pull request that silently changes a page's rendering mode or where its meta tags live, with no error, no failing test, and no visible difference in the browser. These are the ones worth knowing by sight, because they're invisible in code review unless you're looking for them.
Static by default — and one line flips a whole subtree dynamic
Next.js prerenders pages to static HTML at build time unless something tells it the page depends on the request. The list of things that flip a route from static to dynamic (rendered fresh on every request) is short and easy to trip:
| Trigger | Where it sneaks in |
|---|---|
cookies() / headers() | An auth check or A/B flag read at the top of a layout |
searchParams prop | A page that reads ?ref= or ?page= from the URL |
connection() / draftMode() | A preview toggle, a "is this a bot" check |
unstable_noStore() | Copy-pasted in to "fix" a stale-data bug |
fetch(url, { cache: 'no-store' }) | One data call that didn't want caching |
export const dynamic = 'force-dynamic' | Pasted in to make a build error go away |
Each one is legitimate somewhere. The problem is scope. Put any of these in a layout, and it doesn't just affect that layout — it opts every page nested under it out of static rendering. Add cookies() to a root layout to read a session, and your marketing pages, your blog, and your docs all quietly become per-request renders.
Why does this matter for rankings? A dynamic page is rendered on demand instead of served as a pre-built file. That's slower at the edge, it burns server cost on traffic that could've been cached, and — as we'll see in a moment — it changes when your meta tags reach the crawler. The build still succeeds. The page still works. You find out months later that your fast static site became a sluggish per-request one because of a session check three layers up.
The guardrail is cheap: keep request-time APIs out of shared layouts. Read cookies() in the specific child Server Component that needs it, or wrap the dynamic part in a <Suspense> boundary so Partial Prerendering keeps the rest of the page static. And in CI, watch the build output — Next.js prints a per-route legend marking each route as static (○) or dynamic (ƒ). A route that flips from ○ to ƒ in a PR is a rendering regression in plain sight.
'use client' on a page silently deletes its metadata
Next.js's Metadata API — the static metadata export and the generateMetadata function — is the right way to ship per-page titles, descriptions, canonicals, and Open Graph tags into the server-rendered HTML. There's one rule that bites hard: it only works in Server Components.
So when someone needs a useState or an onClick on a page and reaches for the fastest fix — drop 'use client' at the top of page.tsx — the metadata export on that file stops doing anything. No build error. The page renders. But its <title>, description, and canonical are simply gone from the HTML, and every page sharing that pattern inherits the site-wide defaults instead of its own. It's the Next.js version of the oldest SPA bug: meta tags that exist only after JavaScript runs.
The fix is the idiomatic one: keep page.tsx a Server Component that exports metadata, and push the interactive bits into a small 'use client' child component that the page renders. The page stays crawlable; the button still works.
Streaming metadata: a dynamic page misses the first wave
These two failures compound, and this is the part most Next.js SEO guides miss. As of Next.js 15, when a page is dynamically rendered, the framework streams its metadata separately — it sends the page body and injects the resolved generateMetadata tags afterward, rather than blocking on them. For a page that's correctly static, the metadata is baked into the initial HTML response, exactly where you want it.
That distinction lines up precisely with how Google indexes JavaScript: in two waves, where wave one parses the raw HTML on first crawl and wave two — rendering — is queued for hours to weeks later. Meta tags that arrive in the initial static HTML are read in wave one. Meta tags streamed in after the body are not guaranteed to be in that first parse. So a page that silently went dynamic (per the cascade above) doesn't just get slower — it can lose its title and description from the wave-one snapshot that decides how it shows up in results. The two regressions you can't see in the browser stack on top of each other.
The short list to check on every Next.js SEO deploy
When a PR touches routing, layouts, data fetching, or page components, these are the deploy-level regressions worth a 60-second look — none of them fail the build:
- A route flipped
○ → ƒin the build output (static → dynamic) without a real per-request reason. - A
'use client'added to apage.tsxthat exportsmetadataorgenerateMetadata. - A request-time API (
cookies(),headers(),searchParams) added to a shared layout instead of a leaf component. - A
force-dynamicorunstable_noStore()pasted in to silence a caching error rather than fix it. - JSON-LD moved into a client
useEffectinstead of rendered server-side into the page. - A canonical or redirect change in
next.config.jsor middleware that quietly drops a previously-indexed URL.
Then confirm it in the rankings, not the build log
Here's the trap that makes all of this expensive: the deploy that introduces one of these regressions is green, and the one that fixes it is also green. The build log can't tell you whether a page got faster, kept its title in wave one, or started climbing — only Search Console can, and it reports that on a lag of days to weeks. That gap is exactly why a one-line rendering change ships, gets forgotten, and is never connected to the traffic it cost or earned.
Tying a specific pull request — "added cookies() to the root layout," "moved interactivity into a client child and restored metadata" — to the ranking movement that followed weeks later is what Code Results is built to do: changepoint detection on your Search Console history, matched back to the deploy that moved the number. Ship the fix, then watch the page you fixed actually recover.
Sources: Next.js — generateMetadata, Next.js — Static and Dynamic Rendering, Next.js — app/ Static to Dynamic Error, Next.js — SEO Rendering Strategies, Strapi — The Complete Next.js SEO Guide.
See which of your PRs actually moved rankings.
Code Results connects your GitHub deploys to Google Search Console with causal attribution — so you stop guessing which code change moved organic search, and start measuring it.
Start for freeSEO A/B testing for developers
You can A/B test a checkout flow by splitting users. You cannot test an SEO change the same way — the visitor you are optimizing for is Googlebot, and splitting it by request is cloaking. Real SEO split testing randomizes pages, not users, and measures the result against a forecast counterfactual. How it works, and when you cannot run one at all.
Shipping JSON-LD structured data from your codebase
Structured data is the rare SEO task that belongs to engineers — it ships with your build. But it is an eligibility signal, not a ranking boost, and Google has been retiring rich-result types, not adding them. What JSON-LD actually buys you in 2026, and how to generate it from your app so it never drifts out of sync.
Technical SEO for React and Vite SPAs Googlebot actually rewards
Googlebot runs JavaScript — but "can render" and "reliably indexes" are different promises, and the gap is where a React SPA leaks traffic. The two-wave indexing pipeline, why dynamic rendering is dead, and the SPA-specific mistakes (hash routing, soft 404s, onClick nav) that silently tank rankings.