Tutorial10 min read · Updated May 11, 2026

The Complete Next.js SEO Setup (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.

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.

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.

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.

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.

Ready to build?

Try InBuild for free — describe what you want, get a complete site in 30 seconds, export the code anytime.

Start free

More guides