Next.js SEO: Rendering Strategy, Metadata, and Common CSR Traps
Next.js can serve fully indexable HTML or ship an empty CSR shell. A technical guide to SSR/SSG/ISR, the App Router metadata API, canonical and hreflang, dynamic sitemaps, and render-diff traps.
Next.js can produce four different documents from the same component tree — server-rendered per request, statically generated at build, incrementally regenerated, or client-rendered in the browser — and only three of them put your content in the HTML Google crawls first. Google reads raw HTML on the first pass and renders JavaScript later, on a separate queue with real limits. The core Next.js SEO decision is choosing a rendering mode that ships real content and metadata in the initial response, then confirming the framework did what you configured — because it silently falls back to client rendering more often than developers expect.
The four rendering modes and what Google sees
Google's pipeline is crawl, then render, then index. The crawler parses your raw HTML and queues the <a href> links it finds; JavaScript execution happens later in the Web Rendering Service, and the rendered snapshot is what gets indexed. Where your content lives in that sequence depends on the mode:
- SSR (dynamic). HTML is generated per request on the server. Content, links, and metadata are all in the raw response. In the App Router this is the default for a segment that reads request data (cookies, headers,
searchParams) or opts out of caching. - SSG (static). HTML is built ahead of time and served as a static file. Same benefit — everything is in the raw HTML — with the lowest latency. Use
generateStaticParamsto prebuild dynamic routes. - ISR (incremental static regeneration). Static HTML rebuilt in the background on a
revalidateinterval or on-demand. Google always gets served rendered HTML; freshness lags by the interval. - CSR (client-side). The server sends a near-empty shell and the browser builds the page. Content and links exist only after JavaScript runs, so they enter the render queue — and a "use client" component that fetches its own data on mount produces exactly this.
The first three are safe for search. CSR is the source of nearly every Next.js SEO failure below. Bing renders JavaScript far less reliably than Google, so CSR content may never make it into Bing at all.
The render-diff traps
Because raw HTML and rendered HTML are two different documents, the most damaging Next.js problems are the ones where they diverge. Crawlinx fetches both and diffs them, which is how these surface:
- Content that only appears after JS. A client component fetching its data on mount, or a whole route marked
"use client", ships a shell whose body fills in later. Crawlinx flags this asrender.content_js_only, and a page that depends on JavaScript to show anything asrendering.js_dependent. - Links that only appear after render. Navigation built client-side, or a list of product links fetched on mount, is invisible to the first-pass crawl. Crawlinx flags
render.links_js_only. Pages reachable only through such links can be orphaned entirely —links.orphan. - Title, canonical, or robots changed by JS. Setting
document.titleor injecting a canonical or robots meta from the client produces a value in the rendered DOM that differs from the raw HTML. Google may act on either. Crawlinx flagsrender.title_changed,render.canonical_changed, andrender.robots_changed. The fix is to emit these from the server via the Metadata API, never from a client effect.
The mechanics behind all of these are in our JavaScript SEO and rendering guide.
The Metadata API
The App Router generates <head> tags from a metadata export or an async generateMetadata function that runs on the server. Use it — it guarantees title, description, canonical, robots, Open Graph, and hreflang land in the raw HTML rather than being set client-side.
- Static per route: export a
metadataobject for fixed titles and descriptions. - Dynamic per route: export
generateMetadatafor a product or article whose title depends on fetched data. It runs server-side, so the value is in the initial response. - Canonical: set
alternates.canonical. Every indexable page needs a self-referencing canonical to a live 200 URL; a missing one shows ascanonical.missing. Do not also inject a canonical from a client component — that is what produces the render-diff. Our canonical tags guide covers the rules. - robots: set the
robotsfield rather than a client-injected meta tag. Remember you cannot reliably remove anoindexwith JavaScript — Google may skip rendering when it seesnoindexin the raw HTML — so never shipnoindexin the initial output for a page you want indexed. It surfaces asindex.noindex.
In the older Pages Router the equivalent is a server-rendered next/head; the same principle holds — metadata must be in the server output.
Canonical and hreflang in the App Router
alternates.canonical and alternates.languages in the Metadata API emit the canonical and hreflang <link> tags server-side. For internationalized routing (/en/, /de/), generate the hreflang set from generateMetadata so every localized route lists itself and all alternates. The three non-negotiable hreflang rules still apply: return tags must be bidirectional, every version must be self-referential, and URLs must be fully qualified and absolute. Our hreflang guide covers the codes and common breakage. Do not auto-redirect by IP in middleware — Googlebot crawls mostly from the US and may never see the regional variant; use distinct URLs plus hreflang and a user-selectable switcher.
Dynamic routes and sitemaps
For dynamic segments, prebuild what you can with generateStaticParams so the routes exist as static HTML rather than rendering on first request. Generate sitemap.xml from a sitemap.ts file that reads your data source, so the sitemap tracks your real catalog instead of a hand-maintained list. Two things to verify: that every URL in the generated sitemap is indexable (not noindex, returns 200), and that the routes it lists are actually reachable through crawlable server-rendered <a href> links, not only through a client-side router.
Hydration and common CSR-only pitfalls
Hydration — shipping server HTML then attaching interactivity — is the healthy default and is fine for SEO because the content is already in the raw response. The pitfalls are the shortcuts that quietly opt out of it:
- Marking a whole page
"use client"and fetching data in auseEffect, which turns SSR into CSR. - Routing or gating content on
localStorage,sessionStorage, or cookies — the render service wipes state between page loads, so this content never renders. - Rendering navigation only through the client router with no crawlable
<a href>in the server output. - Deferring primary content behind a scroll or click handler — Googlebot does not scroll or click.
Verify with URL Inspection's Rendered HTML view and by diffing raw against rendered, not with client-side analytics, which undercount Googlebot.
Takeaway
Next.js gives you fully indexable HTML for free through SSR, SSG, or ISR — the failures come from accidentally falling back to CSR and from setting metadata on the client instead of the server. Choose a server-rendering mode for content routes, emit title, canonical, robots, and hreflang through the Metadata API, generate sitemaps from your data, and diff raw against rendered HTML to catch what only your users can see. Run the technical SEO audit checklist against a deployed build to confirm the framework shipped what you configured.
Audit your own site — free
77 checks, internal PageRank, render-diff. No signup, results in ~30s.