--- title: "Next.js SEO: A Practical Guide" description: "A practical Next.js SEO guide: rendering strategy, the Metadata API, sitemaps, robots, JSON-LD, images, and the mistakes that hide content from crawlers." author: "Alec Lindsay" date: "2026-06-23" tags: "Next.js, SEO, SEO for Developers" url: "https://seoagent.com/blog/nextjs-seo" --- # Next.js SEO: A Practical Guide **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](/blog/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: 1. **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. 2. **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 `` tags. 3. **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`: ```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`: ```tsx // app/sitemap.ts import type { MetadataRoute } from 'next'; export default async function sitemap(): Promise { 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: ```tsx // 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 `