Next.js SEO: A Practical Guide
Next.js gives you real control over SEO — if you use the right rendering strategy and the App Router's built-in metadata, sitemap, and robots primitives. Here's the practical playbook.

TL;DR — Next.js gives you genuine control over SEO, but only if your rendering strategy serves real HTML to crawlers and you use the App Router's built-in metadata, sitemap, and robots primitives. Get the rendering right first; everything else is configuration.
- Render server-side (SSG/SSR/ISR) so crawlers and AI assistants see content, not an empty shell.
- Use the Metadata API for titles, descriptions, canonicals, and Open Graph — plus
sitemap.ts,robots.ts, and JSON-LD. - The most common Next.js SEO bug is client-only data fetching that ships blank HTML.
Next.js is one of the best frameworks for SEO because so much of the hard part — server rendering, metadata, sitemaps — is built in. But the framework will happily let you ship a fast site that search engines can't read, so the wins come from using the right primitives in the right places. This guide is the practical version: what you control, why rendering strategy is the foundation, and a checklist you can run against any App Router project. It's a focused companion to the broader SEO for developers playbook.
What you control in Next.js for SEO
SEO in Next.js breaks into three layers, and you own all of them in code:
- Rendering — whether a route is statically generated, server-rendered per request, incrementally regenerated, or rendered on the client. This decides what HTML a crawler receives.
- Metadata — titles, descriptions, canonical URLs, Open Graph and Twitter cards, robots directives. The App Router's Metadata API turns these into typed exports instead of hand-written
<head>tags. - Site-level signals —
sitemap.ts,robots.ts, structured data (JSON-LD), image optimization, and your internal link graph.
The examples below use the App Router (app/ directory), which is the current default and where the SEO primitives are strongest. The older Pages Router has equivalents (next/head, getStaticProps), but if you're starting fresh, use the App Router.
Why rendering strategy matters
Search crawlers and AI assistants are far better at reading HTML that's present in the initial response than HTML that only appears after JavaScript runs. Googlebot can render JavaScript, but it does so on a deferred queue, and AI crawlers — the ones feeding ChatGPT, Perplexity, and Google's AI Overviews — are much less reliable at it. If your content depends on a client-side fetch, you're betting your indexing on a render pass that may never come consistently.
Next.js gives you four strategies. Match the route to the right one:
| Strategy | When it renders | Best for |
|---|---|---|
| SSG (static) | At build time | Marketing pages, docs, anything stable |
| ISR (incremental) | At build, then revalidated on a schedule | Blogs, product catalogs that change occasionally |
| SSR (dynamic) | On every request | Pages with per-request or auth-gated data |
| CSR (client) | In the browser, after load | Dashboards behind login — not indexable content |
The rule of thumb: anything you want ranked or cited should render its main content on the server. SSG and ISR are ideal because the HTML is complete and cacheable. SSR works too. Client-side rendering is fine for app surfaces behind a login, but it's the wrong default for public, crawlable pages. In the App Router, Server Components render on the server by default — the trap is reaching for "use client" and a useEffect fetch out of habit.
The practical checklist
Here's the concrete work, in roughly the order you'd do it on a new project.
1. Set titles and descriptions with the Metadata API
Export a metadata object (static) or a generateMetadata function (dynamic) from any layout.tsx or page.tsx:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `https://example.com/blog/${post.slug}` },
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.ogImage],
},
};
}
Set a title.template in your root layout (e.g. "%s | Your Brand") so child pages only supply their own title. Keep descriptions to roughly 150–160 characters.
2. Add canonical URLs
Duplicate content — trailing slashes, query parameters, www vs non-www — splits ranking signals. Set a canonical via alternates.canonical on every indexable route (shown above). For sites with many parameterized URLs, canonicalize to the clean version so crawlers consolidate on one address.
3. Generate a sitemap with app/sitemap.ts
The App Router turns a single file into a valid sitemap at /sitemap.xml:
// app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
return [
{ url: 'https://example.com', lastModified: new Date() },
...posts.map((p) => ({
url: `https://example.com/blog/${p.slug}`,
lastModified: p.updatedAt,
})),
];
}
Generate it from your real data source so new pages appear automatically, then submit the sitemap URL in Google Search Console once.
4. Add app/robots.ts
Control crawling and point crawlers at your sitemap:
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/admin' },
sitemap: 'https://example.com/sitemap.xml',
};
}
Be careful here — an overly broad disallow is one of the fastest ways to deindex pages by accident.
5. Add JSON-LD structured data
Structured data helps both rich results and AI extraction. There's no dedicated Metadata API field, so inject a <script type="application/ld+json"> directly in a Server Component:
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.date,
author: { '@type': 'Person', name: post.author },
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
Article, BreadcrumbList, FAQPage, and Organization are the high-value types for most sites. Because it renders server-side, it's in the HTML crawlers receive.
6. Optimize images with next/image
Use next/image for automatic resizing, modern formats, and lazy loading — all of which feed Core Web Vitals, a ranking input. Always set descriptive alt text, give above-the-fold images priority, and provide width/height (or fill) to prevent layout shift.
7. Plan internal links
Internal links spread authority and give crawlers a path to every page. Link new pages from existing relevant ones, use descriptive anchor text instead of "click here," and use Next's <Link> component so links are real <a href> elements in the HTML — not click handlers a crawler can't follow.
Common mistakes
- Client-only data fetching that renders empty HTML. A
"use client"page that fetches content inuseEffectships a blank shell on first paint. The fix: fetch in a Server Component (or viagenerateStaticParams/server actions) so the content is in the initial response. - Missing or duplicated metadata. No
generateMetadatameans Next falls back to a generic title, and copy-pasting one metadata block across routes gives every page the same title and description. Generate metadata per route from real data. - Accidentally blocking routes. A stray
disallowinrobots.ts, anoindexleft over from staging, or a build that doesn't statically generate dynamic routes can quietly hide pages. Verify with Search Console's URL Inspection after deploys. - Forgetting
generateStaticParams. Dynamic routes ([slug]) without it may render on demand instead of being pre-built and fully crawlable — pre-render the known paths. - Treating Core Web Vitals as optional. Unoptimized images, oversized client bundles, and layout shift hurt both ranking and the experience. Lean on
next/image, Server Components, and code-splitting.
A useful habit: after each deploy, view-source on a few key pages and confirm the title, description, canonical, and main content are all present in the raw HTML. This is also where tooling helps — SEOAgent runs inside your coding agent to audit metadata, structured data, and internal links directly in the repo and proposes fixes you approve as commits, so SEO maintenance stays in version control instead of a separate dashboard. If you're curious how your pages read to AI assistants specifically, you can run them through an AI-readiness checker.
Frequently asked questions
Is Next.js good for SEO?
Yes — it's one of the strongest frameworks for SEO because server rendering, the Metadata API, sitemaps, and robots files are all built in. The caveat is that you have to use them: a Next.js site that renders its content only on the client can still be hard for crawlers to read.
Should I use SSG, SSR, or ISR for SEO?
For SEO, all three work because the HTML is generated on the server. Prefer SSG for stable pages, ISR for content that changes occasionally (blogs, catalogs), and SSR when each request needs fresh or personalized data. Avoid pure client-side rendering for public content you want indexed.
How do I add meta tags in the Next.js App Router?
Export a metadata object or a generateMetadata function from a layout.tsx or page.tsx. Next.js renders the corresponding <head> tags for you, including title, description, canonical (alternates.canonical), and Open Graph data. You don't write <head> tags by hand.
Does Next.js handle sitemaps and robots.txt automatically?
It generates them from code, not magically. Add app/sitemap.ts and app/robots.ts, and Next serves /sitemap.xml and /robots.txt from those functions. You still decide what URLs to include and which routes to allow or disallow.
Will client-side rendering hurt my SEO?
For public, crawlable pages, yes — content that only appears after JavaScript runs may not be indexed reliably, and AI crawlers handle it worse than Googlebot. Render the main content on the server (Server Components, SSG, SSR, or ISR) and reserve client rendering for interactive, logged-in app surfaces.
Conclusion
Next.js removes most of the friction from technical SEO, but the responsibility shifts to choosing the right rendering strategy and wiring up the built-in primitives. Get the foundation right — server-rendered content, per-route metadata, a generated sitemap, sane robots rules, structured data, optimized images, and a real internal link graph — and the framework does the heavy lifting. For the bigger picture of building search-ready sites in code, the SEO for developers guide is the place to go deeper.
Put SEO on autopilot in your own editor
SEOAgent runs as a free skill inside Claude Code, Cursor, and Codex — on the model you already pay for. Audit, plan, and write SEO content right in your repo, with every change reviewed before it ships. No second AI subscription.
Get SEOAgent free