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?
Try InBuild for free — describe what you want, get a complete site in 30 seconds, export the code anytime.
Start freeMore 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 it in 30 seconds.
Read