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
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
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
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
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
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
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
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
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?
See a real InBuild template rendered live, with the exact exported code, before you pick a plan — no account needed.
More guides
The Ultimate Guide to AI Website Builders (2026)
How AI website builders work, what they're good at, where they fall short, and how to pick one — Lovable, v0, Bolt, Replit, InBuild compared with honest tradeoffs.
ReadTutorialThe Complete Guide to SaaS Landing Pages (2026)
Anatomy of a SaaS landing page that converts: hero, social proof, features, pricing, FAQ. What every section does, what good looks like, and how to build one with AI instead of a blank page.
Read