Next.js App Router SEO

The Next.js App Router makes server rendering the default and replaces the old next/head approach with a declarative Metadata API. For SEO this is a strong foundation: routes ship populated HTML and metadata in the response, and you express per-route titles, descriptions, and structured data through typed exports rather than imperative head manipulation. This guide covers the App Router’s SEO surface within framework-specific SEO implementations, applying the rendering-strategy decisions in Next’s model.

Prerequisites

  • Next.js 13+ using the app/ directory (App Router).
  • Route segments organized so metadata can be colocated with each page.
  • An understanding of the fetch cache and revalidate for per-route rendering mode.
  • Search Console access to verify rendered metadata.
What each App Router SEO prerequisite unlocks Four prerequisites — the app directory, colocated route segments, the fetch cache, and Search Console access — must all be in place before per-route metadata is verifiable. Four things to have in place before you start Next.js 13+ with the app/ directory metadata colocated per route segment fetch cache and revalidate understood Search Console access to verify all in place? Per-route metadata verifiable end to end
All four prerequisites gate the same outcome: only when they are in place can per-route metadata be verified end to end.

How it breaks

The App Router’s most common SEO regression is marking a layout or page 'use client' at too high a level, which opts whole subtrees out of server metadata and pushes rendering toward the client. Another is putting generateMetadata work behind an uncached, slow fetch so the route’s TTFB balloons and rendering stalls.

Next.js App Router metadata flow A request triggers generateMetadata and the server component render; both resolve on the server and emit the head and HTML in the response. request generateMetadata() async, per route server component renders content response head + HTML bot
generateMetadata and the server component both resolve server-side and emit the head and HTML together in the response.

Step-by-step fix

  1. Export static metadata for fixed routes.

    // app/about/page.jsx
    export const metadata = {
      title: 'About — Example',
      description: 'Who we are and what we build.',
      alternates: { canonical: 'https://example.com/about' },
    };
  2. Use generateMetadata for data-driven routes. Detailed in dynamic metadata with the Next.js Metadata API.

    // app/products/[id]/page.jsx
    export async function generateMetadata({ params }) {
      const product = await getProduct(params.id);
      return { title: product.name, description: product.summary };
    }
  3. Set the rendering mode per route with the fetch cache.

    export const revalidate = 3600; // ISR; 0 = SSR; force-static = SSG
  4. Keep 'use client' low in the tree so metadata and content stay server-rendered.

The four steps to correct App Router metadata A numbered sequence: export static metadata, add generateMetadata for dynamic routes, set the rendering mode with the fetch cache, and keep the use client directive low in the tree. Four steps, applied per route segment 1 Static metadata for fixed routes 2 generateMetadata data-driven routes 3 Fetch-cache mode revalidate config 4 'use client' low keep leaves only
The four steps run in order per route: static metadata, generateMetadata for dynamic data, the fetch-cache mode, then keeping 'use client' at the leaves.

Gotchas & edge cases

  • 'use client' on a layout. Opts whole subtrees out of server metadata; push the directive down to leaf interactive components.
  • Slow uncached fetch in generateMetadata. Blocks the response; cache or deduplicate the fetch (Next dedupes identical fetch calls within a request).
  • Missing canonical. Set alternates.canonical per route to avoid duplicates — see canonical URL management.
  • JSON-LD placement. Render a <script type="application/ld+json"> in the server component body; it ships in the response.
Placing the use client directive high versus low in the tree A use client directive on the layout turns the whole subtree into client components and drops server metadata, while pushing it to a single leaf button keeps the layout and page server-rendered. Where 'use client' sits decides what stays server-rendered 'use client' on the layout layout — client directive at the top page — forced client content — forced client ✗ subtree opts out of server metadata 'use client' on a leaf layout — server metadata resolves here page — server Button — client ✓ metadata and content stay server-rendered
Pushing the directive down to the single interactive leaf keeps the layout and page server-rendered, so metadata is never opted out of the response.

Validation checklist

The App Router SEO validation checklist Five checks confirm a route is correctly server-rendered: curl shows the head, GSC rendered HTML matches, no SEO route is forced client-side, JSON-LD validates, and TTFB stays low. Five checks that a route ships correct metadata curl of the route shows title, description, and canonical GSC rendered HTML matches the live head no SEO-critical route is forced client-side JSON-LD validates in the Rich Results Test TTFB stays low; generateMetadata fetches are cached
All five checks pass together only when the route's metadata is genuinely server-rendered rather than client-injected.

Performance & crawl-budget notes

App Router routes render metadata and content on the server, so they index in the first wave and skip the render queue, protecting crawl budget. The fetch cache lets you serve most routes as static or ISR and reserve full SSR for genuinely dynamic ones, keeping server cost proportional to how dynamic each route really is.

The fetch cache maps a revalidate setting to a rendering mode Three rows map a route segment config to a rendering mode and its crawl outcome: force-static gives SSG prebuilt HTML, revalidate 3600 gives ISR cached pages, and revalidate 0 gives per-request SSR reserved for dynamic routes. One config controls the rendering mode per route route segment config rendering mode crawl outcome dynamic = 'force-static' SSG prebuilt HTML, first-wave index revalidate = 3600 ISR cached HTML, periodically fresh revalidate = 0 SSR per request, reserve for dynamic
The same route file becomes SSG, ISR, or SSR by its fetch-cache setting alone, so you match server cost to how dynamic each route really is.

Go deeper

Two deeper App Router metadata topics branch from this guide This guide branches into dynamic metadata with generateMetadata and streaming metadata with Suspense, the two next topics to read. App Router SEO this guide Dynamic metadata with the Metadata API generateMetadata, OG images, JSON-LD Streaming metadata with Suspense how the head resolves when a route streams
Two deeper reads branch from this guide: dynamic metadata generation and streaming metadata resolution.

Frequently Asked Questions

How does SEO work in the Next.js App Router? The App Router renders on the server by default and exposes a Metadata API: you export a static metadata object or an async generateMetadata function per route segment, and Next renders the resulting tags into the document head in the response. Rendering mode is controlled per route via the fetch cache and route segment config.

Do I need next/head in the App Router? No. next/head is a Pages Router API. In the App Router you use the Metadata API — the metadata export or generateMetadata — instead, which is server-rendered and typed.

← Back to Framework-Specific SEO Implementations