Tutorial13 min read · Updated September 7, 2026

Next.js SEO: The Complete Setup Guide (2026)

Next.js has the strongest built-in SEO support of any React framework — but you still have to set it up correctly. This guide covers the complete SEO checklist for an App Router site: per-page metadata, sitemap, robots.txt, structured data, Open Graph images, Core Web Vitals tuning, and the gotchas.

Step 1 — Per-page metadata via the Metadata API

Next.js App Router's Metadata API is the canonical way to set `<title>`, meta description, Open Graph, and Twitter card tags. Every page can either: export a `metadata` constant (for static pages), or export a `generateMetadata` async function (for dynamic pages where the title depends on the slug).

Set `metadataBase` in your root `app/layout.tsx` to your production URL — this resolves relative URLs in OG images and canonical tags. Without it, social shares get broken absolute URLs.

For dynamic pages, `generateMetadata({ params })` lets you fetch the relevant data and return title + description per slug. Always handle the not-found case: return `{ title: 'Not found' }` so 404 pages don't leak the 200-page metadata. Before you ship a title and description, preview how they truncate in a Google result — a title that reads fine in code is often cut mid-word in the SERP.

Do you still need the next-seo package?

Probably not, if you are on the App Router. `next-seo` is a community package that was written for the Pages Router, where Next.js had no built-in way to set metadata per page: you rendered a `<NextSeo />` component inside each page and it emitted the title, description, canonical, Open Graph and Twitter tags into `<head>`. The App Router's Metadata API (Step 1) covers every one of those fields natively — `title`, `description`, `alternates.canonical`, `openGraph`, `twitter`, `robots` — with no extra dependency, and it is resolved on the server at build time rather than in a client component.

Where `next-seo` still earns its place: you are on the Pages Router and do not plan to migrate, or you want its JSON-LD helper components (`ArticleJsonLd`, `FAQPageJsonLd`, `ProductJsonLd` and so on) as typed wrappers. On the App Router you get the same result with a plain `<script type="application/ld+json">` (Step 4); the helpers save a little typing, nothing more. Do not install it only for metadata on a new App Router project — you would be maintaining two systems that write the same tags.

Step 2 — Sitemap.xml via app/sitemap.ts

App Router's file convention for sitemaps: create `app/sitemap.ts` exporting a default function returning `MetadataRoute.Sitemap` — an array of `{ url, lastModified, changeFrequency, priority }` entries. Next.js serves it at `/sitemap.xml` automatically.

Enumerate every page — static and dynamic. For dynamic pages, query your DB or content arrays inside `sitemap()` to build URLs. Sites with under 50,000 URLs use a single sitemap file; larger sites split into multiple sitemaps referenced by a sitemap index (`app/sitemap.ts` returns the index, `app/sitemap-products.xml/route.ts` returns the products, etc.).

Submit your sitemap URL in Google Search Console and Bing Webmaster Tools. Google will crawl new URLs faster when you submit a sitemap than when relying on link discovery alone.

Step 3 — robots.txt via app/robots.ts

Create `app/robots.ts` exporting a default function returning `MetadataRoute.Robots`. Allow public paths, disallow authenticated paths (e.g. `/dashboard`, `/api/`), and reference your sitemap. If you'd rather not write the rules from memory, generate a robots.txt with each line explained and port the result into `app/robots.ts`.

Important: robots.txt prevents crawling, not indexing. A page that's disallowed in robots can still appear in search results if other sites link to it (Google indexes the URL without crawling the content). To prevent indexing entirely, add a `noindex` meta tag to the page itself.

Step 4 — Structured data (JSON-LD)

Embed JSON-LD in your pages to make them rich-result eligible. Sitewide: emit Organization and WebSite schema in your root layout. Per-page: emit the specific schema that fits — Article on blog posts, Product on pricing, FAQPage on FAQ pages, BreadcrumbList on dynamic pages, HowTo on tutorials.

Add a `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />` to your page. Validate using Google's Rich Results Test (search.google.com/test/rich-results) before you ship.

Don't over-emit. Each schema type should describe what the page actually is. Marking up a non-FAQ page with FAQPage to game the SERP gets your structured data ignored or your domain manually penalized.

Step 5 — Open Graph images

Use App Router's `opengraph-image.tsx` file convention: place an `opengraph-image.tsx` file alongside a route (e.g. `app/blog/[slug]/opengraph-image.tsx`) and Next.js auto-generates an OG image for every URL matching that route. Use `next/og`'s `ImageResponse` to render JSX as an image at the standard 1200×630 size.

Without OG images, every share of your site looks generic. With them, your titles, brand, and design appear in every Slack, Twitter, LinkedIn, and Facebook preview — significantly improving CTR on social shares. If you'd rather not wire steps 1–5 by hand, describe the site and InBuild generates the metadata, sitemap, robots and OG image files as Next.js code — a paid plan is required to generate.

Step 6 — Core Web Vitals

Use `next/image` for every image — automatic responsive sizing, modern formats (WebP/AVIF), lazy loading, and `width`/`height` attributes that prevent CLS.

Use `next/font` for fonts — Next.js downloads and serves them with your site, eliminating render-blocking external requests and font swap CLS.

Keep JavaScript bundle size in check. Audit with `npm run build && npx @next/bundle-analyzer`. Move heavy client components behind dynamic imports (`next/dynamic`) where they're not above-the-fold.

Run Lighthouse on every important page before launch. Target 90+ on Performance, SEO, and Accessibility. Sub-90 is a fixable problem 95% of the time.

Step 7 — Canonical URLs

Every page should declare its canonical URL — the version Google should treat as authoritative when multiple URLs serve the same content. In the Metadata API: `alternates: { canonical: '/your-page' }`.

Common canonical bugs: forgetting to set canonical on duplicate-content pages (paginated archives, tag pages, filtered category pages), pointing canonical to the wrong domain (www vs non-www), or pointing it at a page that doesn't exist.

Use Google Search Console's URL Inspection tool to verify canonical URLs are correct on a sample of your pages. The 'User-declared canonical' should match what you intended. For a page-by-page spot check without Search Console, paste the URL into the canonical checker — it follows the redirect chain and reports the declared canonical, robots meta and final status code.

How to check that Google can crawl your Next.js site

Once the seven steps are in place, verify them from the outside — the way a crawler sees the deployed site, not the way your dev server renders it. Four checks, in order: open `yourdomain.com/robots.txt` and confirm the public paths are allowed and the sitemap line points at the real domain; open `yourdomain.com/sitemap.xml` and confirm every URL is absolute, on the canonical host, and returns 200; view the page source (not the DevTools DOM) of a dynamic route and confirm the title, description, canonical and JSON-LD are present in the server-rendered HTML; and search `site:yourdomain.com` in Google to see what is already indexed.

The third check is the one that catches App Router mistakes: metadata that is set inside a client component, or a page that renders its content only after a client-side fetch, ships an empty `<head>` or an empty body to the crawler even though the browser shows a finished page. If you want the four checks run for you, run the URL through the site audit — it fetches the page server-side and scores the title, description, canonical, OG image, JSON-LD, H1 count, robots meta, sitemap and robots.txt reachability, and HTTPS on a 0–100 scale. Search Console's URL Inspection tool remains the authority for what Google itself fetched and indexed.

Common gotchas

Environment variable newlines. If `NEXT_PUBLIC_APP_URL` ends in a `\n` (a common Vercel CLI bug), every URL in your sitemap is split across two lines, your canonical URLs are broken, and your structured data points at invalid URLs. Always `.trim()` env vars used in URLs.

Forgetting to update `metadataBase` for production. Default is `localhost:3000`; production needs to be your real domain or all OG images are broken.

Static export config. If you use `output: 'export'`, you can't use `next/image` (it requires the optimizer running on a server). Use plain `<img>` with explicit dimensions.

Dynamic routes returning 200 for unknown slugs. If `/blog/[slug]` returns a 200 with empty content for non-existent slugs, search engines will index thousands of soft-404 pages. Always call `notFound()` for unknown slugs.

How to do it

  1. 1

    Set metadataBase in your root layout

    In app/layout.tsx, export metadata with `metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!)`. Without it, relative URLs in OG images and canonicals break in production.

  2. 2

    Add per-page metadata

    Static pages: export `metadata`. Dynamic pages: export `generateMetadata({ params })`. Always include title, description, openGraph, and twitter fields. Add `alternates: { canonical: '/your-page' }` for canonical.

  3. 3

    Build app/sitemap.ts

    Default-export a function returning MetadataRoute.Sitemap. Enumerate every static page and iterate your dynamic data to add URLs for each. Submit at search.google.com/search-console.

  4. 4

    Build app/robots.ts

    Default-export a function returning MetadataRoute.Robots. Allow public paths, disallow /api/, /dashboard, /admin. Reference your sitemap URL in the response.

  5. 5

    Add structured data

    Emit Organization + WebSite JSON-LD in your root layout. Add per-page schemas: Article on blog, Product on pricing, FAQPage on FAQ pages, BreadcrumbList on dynamic pages. Validate at search.google.com/test/rich-results.

  6. 6

    Generate OG images

    For each dynamic route, create opengraph-image.tsx using next/og ImageResponse. The auto-generated images appear in social shares automatically.

  7. 7

    Tune Core Web Vitals

    Use next/image and next/font. Audit JS bundle size. Run Lighthouse and fix anything below 90 on Performance, SEO, Accessibility.

  8. 8

    Validate everything

    Rich Results Test for structured data. Google Search Console for indexing + sitemap status. PageSpeed Insights for real-user Core Web Vitals. Fix issues before launch, not after.

Frequently asked questions

How long until my site ranks?

Google takes 2–12 weeks to fully crawl and rank a brand new site, depending on your domain authority and how many other sites link to you. New domains rank slower; aged domains with content history rank faster. The structured-data setup above gets you eligible for rich results immediately; ranking is a longer game.

Do I need both Open Graph and Twitter cards?

Twitter cards have largely converged with Open Graph in 2026 — Twitter parses og:title and og:image when no twitter:* tags are present. You technically don't need separate twitter:* tags, but the cost of including them is trivial and it gives you per-platform control if you ever need it.

What's the most common SEO mistake on Next.js sites?

Forgetting to set canonical URLs on dynamic pages, leading to duplicate-content competing-version situations. Second-most common: metadataBase missing or wrong, causing all OG image URLs to resolve to localhost in production.

Should I use Next.js Edge runtime for everything?

No. Edge runtime is great for short, fast responses (auth checks, A/B testing). But it has API limitations (no Node.js APIs, smaller dependency budget) that make it the wrong default for most routes. Use the Node.js runtime by default; opt into Edge for specific routes where TTFB matters.

Do I need next-seo with the App Router?

No. The App Router's built-in Metadata API sets title, description, canonical, Open Graph, Twitter and robots tags per page without any package. next-seo was built for the Pages Router; keep it there if that is where you are, or use it only for its typed JSON-LD helper components.

How do I check whether Google can crawl my Next.js site?

Open /robots.txt and /sitemap.xml on the deployed domain, view the server-rendered source of a dynamic page and confirm the metadata and JSON-LD are in the HTML, then use Search Console's URL Inspection tool on a sample of URLs. A search for site:yourdomain.com shows what is indexed today.

Free tools used in this guide

No signup — each runs in the browser or as a single server-side fetch.

Ready to build?

See a real InBuild template rendered live, with the exact exported code, before you pick a plan — no account needed.

Or skip the tutorial: describe it and InBuild generates the Next.js code for you. A paid plan is required to generate.

Build this with InBuild

More Next.js guides