Next.js SEO

How to make a Next.js site crawlable, indexable, and rankable — the two routers, rendering modes (SSG/SSR/ISR/Server Components), the App Router Metadata API, sitemap.ts, robots.ts, next/image, next/link, and the mistakes that quietly break it.

First published: Jun 26, 2026 · Last updated: Jul 18, 2026 · Advanced
demand #2 in JavaScript Frameworks#14 in Platform SEO#94 in Technical SEO#125 on the site

Next.js solves the hardest JavaScript SEO problems by default — if you use it right. The App Router's Server Components and SSG/ISR put content in the HTML so there's no render-queue delay; the native Metadata API resolves titles, canonicals, and Open Graph on the server; sitemap.ts and robots.ts are file conventions. The framework gives you the infrastructure but writes none of your tags for you. The failures are predictable: missing metadataBase, no priority on the LCP image, metadata exported from a Client Component (silently does nothing), and 404 views returning 200.

TL;DR — Next.js solves the hardest JavaScript SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. problems by default when you use it right. The App Router defaults to Server Components and supports SSG/SSR/ISR — all of which ship rendered HTML, so there’s no render-queue delay. Set metadata with the native Metadata API (metadata / generateMetadata), and remember metadataBase or your canonicals and OG images go relative. Use app/sitemap.ts and app/robots.ts, pre-build dynamic routes with generateStaticParams, set priority on the LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good. image, and keep links as real next/link anchors. The Pages Router is also SEO-capable via next/head. The failures are predictable — and most of them aren’t Next.js’s fault, they’re yours.

Where Next.js fits

Next.js is a React framework, so everything in JavaScript SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. applies. What makes it worth its own guide is that Next.js ships first-class answers to most JS-SEO problems: server renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM., static generation, a metadata system, and sitemapA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing./robots conventions. The hard part isn’t whether Google can read it — Google has rendered JavaScript for years — it’s choosing the right renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. mode and not leaving the SEO basics unwired. This is a specialized case of headless CMS SEOA headless CMS decouples content storage and editing (the backend) from how that content is rendered and delivered (the frontend), serving content over an API instead of a built-in templated 'head'. Its SEO outcomes come almost entirely from how the separate frontend renders pages.: the CMSA content management system (CMS) is software that lets users create, manage, and publish digital content — like blog posts and pages — without writing raw code. WordPress, Drupal, and Joomla are the most common open-source CMS platforms. barely matters, the frontend’s rendering decisions decide everything.

One framing to keep straight: “this is a Next.js site” doesn’t tell you how any single URL is delivered. Rendering mode, cachingCaching stores a copy of a page or resource — in a browser, a CDN edge node, or a search crawler's own cache — so it can be served again without regenerating or re-downloading it. It isn't a direct ranking factor, but it feeds page speed and crawl efficiency., and Server/Client Component boundaries are set per route (sometimes per segment) — a project can mix a static marketing page, an SSR product page, and a Client Component dashboard. Don’t extrapolate one route’s behavior to “the whole app”; test the specific URL.

Two routers, two sets of mechanics

Next.js has two routers, and they handle SEO differently:

  • Pages Router (the older model) — data fetching via getStaticProps / getServerSideProps; metadata via <Head> from next/head (or the next-seo package); no Server Components.
  • App Router (v13+, the current and recommended approach) — React Server Components by default; the native Metadata API (metadata export / generateMetadata); file conventions for app/sitemap.ts and app/robots.ts; generateStaticParams for dynamic routes. Evidence for this claim The App Router uses Server Components and supports generateStaticParams plus metadata file conventions. Scope: Current Next.js App Router behavior; route rendering can become dynamic based on APIs used. Confidence: high · Verified: Next.js: Server and Client Components Next.js: generateStaticParams

Both can rank well. The App Router gives you a cleaner, integrated metadata system (no next/head juggling) and Server Components out of the box, which is why I’d reach for it on a new build. But “App Router or you can’t do SEO” is a myth — plenty of Pages Router sites rank fine.

Rendering modes and what each means for SEO

How Google handles JavaScript is a three-phase pipeline — crawl, then a deferred render wave, then indexStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed.. “All pages with a 200 HTTP status codeAn HTTP status code is the three-digit number a server returns with every response to tell a browser or crawler what happened to its request — success, redirect, client error, or server error. For SEO the code matters as much as the content: it tells Google and Bing whether to index a page, follow a redirect, retry later, or drop the URL from the index. are sent to the rendering queue.” The whole game in Next.js is choosing a mode that puts your content in the HTML before that render wave, so there’s nothing to wait for. Evidence for this claim Google crawls, renders, and indexes JavaScript pages, and successful pages can enter the rendering queue. Scope: Google Search processing, not a promise that a URL will be indexed. Confidence: high · Verified: Google: JavaScript SEO basics

  • Static Site Generation (SSG) — pages pre-rendered at build time. HTML is immediately available with no render-queue risk. Best for content that doesn’t change every minute. generateStaticParams() (App Router) / getStaticPaths() (Pages Router) decides which dynamic routes get pre-built.
  • Incremental Static Regeneration (ISR) — static pages that revalidate after a set interval (export const revalidate = 3600). CrawlersA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. get static HTML with low TTFBTime to First Byte — the time from the start of a request to when the first byte of the response arrives. It's a diagnostic metric (not a Core Web Vital) and a major input to FCP and LCP; ≤0.8 s is good. and the content stays fresh. A strong default — with one trap: after the window expires the next request (possibly GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer.) still gets the stale page; the fresh version serves on the request after that. For genuinely volatile data (prices, stock), SSR is safer.
  • Server-Side Rendering (SSR) — HTML rendered per request. CrawlersA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. get fully rendered HTML immediately; the tradeoff is server latency, so watch TTFB and LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good.. export const dynamic = 'force-dynamic' or using request-time APIs (cookies, headers) opts a route into SSR.
  • React Server Components (App Router default) — render on the server and send HTML; no JavaScript ships for the component itself. Content is in the initial response with no hydrationActivating server-rendered HTML in the browser by attaching JavaScript handlers. gap. This is the best default for SEO. Interactivity lives in Client Components marked 'use client'.
  • Client-Side Rendering (CSR) — rendered entirely in the browser. Googlebot can index it after the render wave (median ~10 seconds, but the 90th percentile stretches to hours), and other crawlers — BingbotBingbot is Microsoft Bing's primary web crawler — the bot that discovers, fetches, and renders pages to build the Bing index. That index also powers Yahoo, DuckDuckGo, Ecosia, and Microsoft Copilot, so Bingbot's reach is far wider than Bing's own search-market share., AI bots, social preview bots — may get an empty page. In the App Router, CSR is opt-in ('use client'); in the Pages Router, avoid fetching primary content in useEffect. Don’t use it for content you want ranked.

A reminder I keep coming back to: “Googlebot can render it” is not the same as “you should make Googlebot render it.” Rendering is expensive, deferred, and not universal across crawlers.

The Metadata API (App Router)

The Metadata API is Server Component only — metadata resolves on the server before the page renders, so it lands in the initial HTML. Export metadata from layout.js or page.js:

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
}

Or, when the tags depend on fetched data, use generateMetadata():

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  return { title: post.title, description: post.description }
}

The fields that matter for SEO:

  • title — supports a string, a template ('%s | Brand'), a default, and an absolute override. Set the template once in the root layout and per-page titles inherit it.
  • description, alternates.canonical (the correct way to set a canonical in the App Router), openGraph (images must resolve to absolute URLs), twitter (also used by LinkedIn and Slack previews), and robots (index/follow plus googleBot-specific directives like max-snippet, max-image-preview).
  • metadataBaserequired for canonical and OG image URLs to resolve correctly. Forgetting it is the single most common Next.js metadata bug: relative URLs leak into your canonical and Open GraphOpen Graph (OG) tags are `<meta>` elements in a page's head, defined by the Open Graph protocol (ogp.me, created by Facebook), that describe a page as a shareable object — its title, description, image, URL, and type. They control how a link preview card looks when the page is shared on Facebook, LinkedIn, Slack, Discord, WhatsApp, and iMessage. They are not a direct Google ranking factor, though Google reads og:title, og:image, and og:site_name as inputs to how a result appears. tags, breaking social previews and muddying canonical signals.

A title-template example:

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: { template: '%s | Brand Name', default: 'Brand Name' },
}
// app/blog/page.tsx
export const metadata: Metadata = { title: 'My Blog Post' }
// Output: <title>My Blog Post | Brand Name</title>

Two gotchas. First, metadata is shallowly merged from layout to page — a nested object like openGraph defined in a child segment replaces the parent’s entirely, so a page-level openGraph: { title: 'Home' } quietly drops any openGraph.images set in the layout. Second — and this one bites people — metadata only works in Server Components. Export it from a 'use client' file and it silently does nothing.

Streaming metadata. For dynamically rendered pages, generateMetadata can stream the metadata after the initial HTML. Googlebot executes JavaScript and inspects the full DOM, so streamed metadata works for Google. But Next.js detects “HTML-limited bots” — Bingbot, Twitterbot, Slackbot, facebookexternalhit — and ships them blocking metadata in the <head> instead. Per the Next.js docs, “streaming metadata is disabled for bots and crawlers that expect metadata to be in the <head> tag.” This is automatic; no configuration needed. It’s a detail almost no competing guide covers, and it’s why streaming metadata isn’t a risk for the bots that can’t wait for it. Prerendered pages are a different case entirely — metadata there resolves at build time, so there’s no stream to worry about. This behavior is version-specific (current as of Next.js 16.2.10); re-check the generateMetadata docs when you upgrade. And because delivery paths differ by entry point, verify metadata two ways, not one: a direct/production request (curl -I or View Source) and a client-side navigation to the same route — the head can update differently between the two.

Metadata with next/head (Pages Router)

On the Pages Router, metadata lives in <Head> from next/head:

import Head from 'next/head'

export default function Page() {
  return (
    <>
      <Head>
        <title>My Page | Brand</title>
        <meta name="description" content="Description" />
        <link rel="canonical" href="https://example.com/my-page" />
      </Head>
      {/* page content */}
    </>
  )
}

Set title and description per page (not just in _app.js), and put a canonical on every page including paginated variants. The next-seo package standardizes this with a <NextSeo> component and structured-data helpers. Migrating to the App Router mostly means trading next/head and next-seo for the native metadata export.

Do you need the next-seo package? It’s a third-party plugin (not part of Next.js itself) — actively maintained, at v7.2.0 as of this writing, not archived. Its own docs are explicit about where it fits: for standard meta tagsMeta tags are HTML elements in a page's head that pass metadata about the page to search engines and browsers. For SEO only a few matter — the title element, the meta description, and the robots meta tag — while meta keywords and most others are ignored. on the App Router, the package’s README recommends using Next.js’s built-in generateMetadata/metadata export instead of <NextSeo>; on the Pages Router, <NextSeo> is still a reasonable convenience layer over next/head. The one App Router use case the package still covers is its JSON-LDJSON-LD (JavaScript Object Notation for Linked Data) is a script-based structured data format, typically paired with the schema.org vocabulary to describe page content for search engines and AI systems. Google recommends it over Microdata and RDFa because it's the easiest format to implement and maintain at scale — but all three work, and structured data isn't a ranking signal. helper components (ArticleJsonLd, FAQPageJsonLd, etc. with useAppDir), which some teams prefer over hand-rolling <script type="application/ld+json">. Bottom line: on a new App Router build, reach for the native Metadata API first — the package is optional, not a requirement, and its own maintainers say so.

Sitemaps

In the App Router, app/sitemap.ts is a file convention that outputs /sitemap.xml:

import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    { url: 'https://acme.com', lastModified: new Date(), priority: 1 },
    { url: 'https://acme.com/blog', lastModified: new Date(), priority: 0.8 },
  ]
}

For large sites, generateSitemaps() shards into multiple files (Google’s limit is 50,000 URLs per sitemapA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing.), each served at /.../sitemap/[id].xml. The sitemap output also supports image sitemapsAn image sitemap is a sitemap (or an extension added to an existing sitemap) that lists the images on your pages using Google's image namespace, helping Google discover images it might otherwise miss., video sitemapsA video sitemap is an XML sitemap (or mRSS feed) that uses the video:video extension to tell Google about videos hosted on your pages — the landing page, a thumbnail, a title, a description, and a link to the video file or player. It helps Google discover and surface video content, but it doesn't guarantee indexing., and localized alternates.languages. On the Pages Router, use next-sitemap or generate pages/sitemap.xml.js with getServerSideProps.

robots.txt

app/robots.ts generates your robots file programmatically:

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [{ userAgent: '*', allow: '/', disallow: '/private/' }],
    sitemap: 'https://acme.com/sitemap.xml',
  }
}

Per-user-agent rules and multiple sitemaps are supported. The Pages Router uses a static public/robots.txt. The rule you cannot get wrong on either router: never disallow your JavaScript or CSS — Google won’t render from blocked files, and on a JS framework that can blank out the page entirely.

next/image and Core Web Vitals

next/image is one of the strongest reasons to use the framework for SEO. It lazy-loads below-the-fold images, requires width/height (or fill) so it reserves space and prevents layout shift / CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good., serves WebP/AVIF automatically, and emits a proper srcset from the sizes prop. The single most important CWV optimization is the priority prop on your hero / above-the-fold image, which preloads it for a faster LCP:

<Image src="/hero.jpg" width={1200} height={630} priority alt="Hero" />

Forgetting priority on the LCP image is the most common Next.js CWV mistake — and CWV problems are widespread on real Next.js sites (see the Stats tab for the Salt Agency data). alt is required: empty for decorative images, descriptive for content images.

next/link renders standard <a href> anchors in the HTML, so Google follows them normally, and it adds client-side navigation plus background prefetching of in-viewport links in production. The SEO rule is simple: use next/link for internal linksAn internal link is a hyperlink from one page on a website to another page on the same website. Internal links help search engines discover your pages and pass ranking signals (PageRank and anchor-text context) between them., and never substitute an onClick handler or JavaScript navigation that doesn’t produce a real anchor — those links aren’t crawlable. Use prefetch={false} on low-value links to save bandwidth if you need to.

Dynamic routes and generateStaticParams

generateStaticParams() tells Next.js which dynamic routes to pre-render at build time:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

Pages built this way are fully static HTML — best for SEO. Without it, dynamic routes are rendered on-demand (SSR) by default, which is fine but reintroduces server latency. Combine it with revalidate (ISR) for content that updates regularly. Make sure all important dynamic URLs are in generateStaticParams so nothing waits on the render queue.

Structured data (JSON-LD)

The Metadata API has no structured-data field — you inject JSON-LD as a <script> in a Server Component, which keeps it in the server-rendered HTML at zero client-bundle cost:

const jsonLd = { '@context': 'https://schema.org', '@type': 'Article', /* … */ }
return <script type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />

Article/BlogPosting, BreadcrumbList, Product, and FAQPage are the usual types. Validate with the Rich ResultsRich results (formerly 'rich snippets') are enhanced search listings — stars, images, prices, breadcrumbs, video thumbnails, and more — that Google and Bing build from structured data. They're a display feature, not a ranking factor, and eligibility never guarantees they'll show. Test after any rendering change.

Common Next.js SEO mistakes

Drawn from real audits and the patterns above:

  1. Missing or relative canonicals — often from a forgotten metadataBase, which also breaks OG image URLs.
  2. No priority on the LCP image — the biggest CWV miss.
  3. 404 views returning 200 — use the built-in notFound() to return a real status; soft 404sA soft 404 is a URL that returns a success status code (usually 200 OK) even though the page is empty, missing, or shows a 'not found' message. It isn't a status code a server sends — it's a label search engines apply after comparing the response code against the rendered content, and they treat the page like a 404 for indexing. are rampant on Next.js sites.
  4. metadata exported from a Client Component — silently does nothing; it’s Server Component only.
  5. CSR for primary content — fetching critical content in useEffect means non-Google crawlers get empty pages.
  6. Hash (#) routing instead of the History API — those views aren’t separately crawlable.
  7. Not exporting generateStaticParams — dynamic routes render on-demand instead of being pre-built.
  8. openGraph overwritten by layout inheritance — child segments replace, not merge.
  9. Blocking JS/CSS in robots.txtA plain-text file at the root of a host that tells crawlers which URLs they may and may not request. It controls crawling, not indexing — a blocked URL can still be indexed if it's linked from elsewhere. or via a Content Security Policy that stops Googlebot’s headless Chrome from loading scripts — test with URL Inspection.

Deployment notes

Next.js is built by Vercel; hosting there gives tight integration (edge CDN for static and ISR pages, good TTFB) but isn’t required. Define permanent redirectsA redirect sends browsers and crawlers from a requested URL to a different one. An HTTP redirect specifically is a 3xx status code paired with a Location header; meta refresh and JavaScript redirects achieve a similar navigation without being a 3xx response themselves. Permanent redirects (301/308) are Google's signal the target should be canonical; temporary ones (302/303/307) aren't. in redirects() in next.config.js (returns 308, or 301 with permanent: true) for reliable signaling to crawlers, set security and caching headers via headers(), and use X-Robots-Tag response headers for path-based noindexNoindex is a directive that tells search engines to keep a page out of their index, so it won't appear in search results. It works only on pages a crawler can actually fetch — a page blocked in robots.txt can never be noindexed. rules when per-page robots metadata is awkward.

What to check where. Cache state, status codes, redirects, and streamed metadata don’t all show up in the same test — a route can look fine in one check and still be broken in another:

CheckWhere to lookWhy it can differ from what you see rendered
Cache/revalidation ageResponse headers on a direct request (curl -I)ISR can serve a stale page on the request right after the window expires
Direct HTTP statuscurl -I on the production URL, not the rendered UIA “not found” view without notFound() still returns 200
Redirect behaviorThe actual context that fires it — redirect() in a Server Action, a Route Handler, vs. a client onClickStatus code and response path differ by invocation context, not just the destination
Streamed metadataDirect request and client-side navigation to the same routeOrdinary clients can get streamed metadata; HTML-limited bots get blocking metadata; the two paths aren’t identical
Client-side transitionsNavigate in-app, then re-check the <head>A route that’s correct on first load can drift after a client transition

None of this is guaranteed by the framework — Next.js gives you the mechanisms (redirects(), notFound(), revalidation, streaming), but cache keys, invalidation, preview state, and deployment configuration are still your responsibility to get right and to test in production, not just locally.

One last note on dynamic rendering — serving prerendered HTML to bots and JavaScript to users. Google has deprecated it as a recommendation: “dynamic rendering was a workaround and not a long-term solution.” You don’t need it on Next.js anyway — SSR, SSG, ISR, and Server Components all put content in the HTML natively. Mention it so you recognize it in an audit; don’t build on it.

The happy path: App Router + Server Components + ISR + the Metadata API (with metadataBase) + next/image with priority. Get those right and most of Next.js SEONext.js SEO is the set of practices and built-in features that make a Next.js site crawlable, indexable, and rankable — rendering mode (SSG/SSR/ISR/Server Components), the App Router Metadata API, sitemap.ts/robots.ts conventions, next/image, and next/link. is handled.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.