SEO for Next.js Sites: Metadata, Sitemaps and Core Web Vitals
November 7, 2026 · 9 min read

Next.js gives you most of what you need for SEO out of the box: a built-in Metadata API, file-based sitemaps and robots files, and tools for fast pages. But the defaults are not enough on their own. You still have to set titles, canonical URLs, social previews, structured data and performance correctly.
This guide walks through the setup for the App Router, with code you can copy. It is written for Next.js 16.
The Next.js SEO Checklist
Set site-wide metadata and
metadataBasein the root layout.Generate unique titles, descriptions and canonical URLs for dynamic pages.
Add a
sitemap.tsand arobots.ts.Add JSON-LD structured data to articles and products.
Pass Core Web Vitals with
next/image,next/fontand less client-side JavaScript.Check everything in Google Search Console after launch.
1. Metadata
Site-wide defaults in the root layout
The Metadata API automatically creates the right <head> tags for each page. You can use a static metadata object or a generateMetadata function. Both only work in Server Components.
Start with defaults in your root layout:
tsx
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('https://yourdomain.com'),
title: {
default: 'Your Site Name',
template: '%s | Your Site Name',
},
description: 'A clear one-sentence description of your site.',
openGraph: {
type: 'website',
siteName: 'Your Site Name',
images: ['/og-default.png'],
},
twitter: { card: 'summary_large_image' },
};metadataBase lets you use relative paths in URL-based fields. Without it, using a relative path in one of those fields causes a build error. The title.template adds your site name to every child page title, and a default is required when you use a template.
One mistake to avoid: do not set alternates.canonical in the root layout. Child pages inherit it, and every page would point to the homepage as its canonical URL. Set canonicals per page instead.
Dynamic metadata for blog posts
For pages that depend on data, use generateMetadata:
tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPost } from '@/lib/posts'; // wrap in React cache()
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return {
title: post.metaTitle ?? post.title,
description: post.metaDescription ?? post.excerpt,
alternates: { canonical: `/blog/${post.slug}` },
openGraph: {
type: 'article',
title: post.title,
description: post.excerpt,
publishedTime: post.publishedAt,
images: [post.thumbnail],
},
twitter: { card: 'summary_large_image' },
};
}The ?? fallbacks mean an empty SEO field falls back to the post title or excerpt. If you fetch the same post in the page itself, wrap the fetch in React’s cache so it only runs once.
Open Graph images
For social previews, you can add a static opengraph-image.jpg file to a route folder. The more specific image wins over the one above it. For per-post images, add an opengraph-image.tsx file in blog/[slug] and use ImageResponse from next/og. A size of 1200 x 630 works well.
Metadata gotchas
Metadata objects from different segments are shallowly merged. If a page sets
openGraph, it replaces the wholeopenGraphobject from the layout, so add the fields you still need.You cannot export both
metadataandgenerateMetadatafrom the same segment.File-based metadata, such as
opengraph-image, takes priority over the object and function.For dynamic pages, Next.js streams metadata after the first UI. It detects bots that cannot run JavaScript, such as
facebookexternalhit, and blocks rendering for them so metadata is in the<head>. Googlebot runs JavaScript, and Next.js says it verified streamed metadata works for it.With Cache Components on, if
generateMetadatareads runtime data or uncached data while the page is otherwise static, Next.js raises an error. Either cache the data with'use cache'or mark the page as intentionally dynamic.
2. Sitemaps and robots.txt
sitemap.ts
Create app/sitemap.ts and return an array of URLs:
ts
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { getPublishedPosts } from '@/lib/posts';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://yourdomain.com';
const posts = await getPublishedPosts(); // published posts only
return [
{ url: baseUrl },
{ url: `${baseUrl}/blog` },
...posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt ?? post.publishedAt,
})),
];
}Tips:
sitemap.tsis a special Route Handler and is cached by default, unless it uses request-time APIs or dynamic config.Use real
lastModifieddates from your content, notnew Date()on every build. Dates that always change are not useful to crawlers.Include only URLs you want indexed: published, canonical and returning a 200 status.
Google’s limit is 50,000 URLs per sitemap. For bigger sites, use
generateSitemaps, which creates files such as/product/sitemap/1.xml. In Next.js 16, theidargument is a promise.Fields like
changeFrequencyandpriorityare supported, but Google has said it ignores them, so do not spend time tuning them.You can also add
images,videosand localizedalternatesto sitemap entries.
robots.ts
ts
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
};
}Remember that robots.txt controls crawling, not indexing. To keep a page out of search results, use a noindex robots meta tag through the robots field in metadata, and do not block that page in robots.txt, or crawlers will never see the tag.
After deploying, submit your sitemap in Google Search Console and check the Pages report for errors.
3. Structured Data (JSON-LD)
JSON-LD helps search engines understand what a page is about. The Next.js recommendation is to render it as a <script> tag in your layout or page. For blog posts, use the BlogPosting type:
tsx
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.thumbnail,
datePublished: post.publishedAt,
dateModified: post.updatedAt ?? post.publishedAt,
author: { '@type': 'Person', name: 'Your Name' },
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
}}
/>
{/* page content */}
</article>
);The replace step matters. JSON.stringify does not protect against XSS in strings, so replacing < with its unicode form is the recommended fix. Use a normal <script> tag here, not next/script, because JSON-LD is data and not executable code. Validate your markup with Google’s Rich Results Test or the Schema Markup Validator. For type safety, you can use the community package schema-dts.
4. Core Web Vitals
The targets
Core Web Vitals measure three things. Pages pass when they meet the target at the 75th percentile of real page loads, for all three:
LCP (loading): 2.5 seconds or less
INP (responsiveness): 200 milliseconds or less
CLS (visual stability): 0.1 or less
A note on 2026: some blogs claim Google lowered the INP threshold to 150 ms. Google’s own documentation still lists 200 ms, and Google says stable Core Web Vitals do not change more than once a year. Use the official numbers, check web.dev for updates, and treat anything well under 200 ms as a safe goal.
Improve LCP
Preload the hero image. If one image is the LCP element, add
preloadtonext/image. In Next.js 16,priorityis deprecated in favor ofpreload.
tsx
<Image
src="/hero.webp"
alt="Describe the image for users and search engines"
width={1200}
height={630}
sizes="100vw"
preload
/>If several images could be the LCP element depending on screen size, use
loading="eager"orfetchPriority="high"instead.Add the
sizesprop to responsive images. Without it, the browser assumes the image is as wide as the viewport and may download an image that is too large.Serve pages from static or cached output where you can, so the server responds fast.
Use
next/fontfor fonts so they are self-hosted and do not block rendering.
Improve INP
Keep Client Components small. Use
'use client'only where you need state, effects or event handlers.Load third-party scripts with
next/scriptand a lazy strategy, and remove ones you do not need.Check what you ship. Next.js 16.4 added a Turbopack Bundle Analyzer with a route summary, a sortable table and the ability to diff bundles over time.
Break up long tasks, and give users immediate visual feedback when they click.
Improve CLS
Always give images a
widthandheight, or usefillinside a positioned parent. Next.js uses them to reserve space.If you use the
styleprop for a custom width, also setheight: 'auto'.Reserve space for ads, embeds, banners and cookie notices.
Load fonts with
next/fontto avoid layout jumps when the font swaps in.
Measure the right way
Field data from real users is what counts. Use the Core Web Vitals report in Search Console, PageSpeed Insights and the Chrome User Experience Report.
Lighthouse cannot measure INP, because it does not interact with the page. Total Blocking Time is a useful lab proxy.
For your own monitoring, use the
web-vitalslibrary or the Next.jsuseReportWebVitalshook and send results to your analytics.Lab and field numbers will differ. Fix field problems first.
5. Launch Checklist
Every page has a unique title and description.
metadataBaseis set, and canonicals are set per page.Open Graph and Twitter images work. Test them by sharing a link.
sitemap.xmlandrobots.txtload in the browser.JSON-LD passes the Rich Results Test.
The LCP image uses
preload, and all images have dimensions.Sitemap is submitted in Search Console.
Core Web Vitals are green in the field report after a few weeks.
Frequently Asked Questions
Do I need a package like next-seo?
For the App Router, usually no. The built-in Metadata API, sitemap and robots files cover most needs.
Does Next.js create a sitemap automatically?
No. You add a sitemap.ts or sitemap.xml file yourself.
Can I use metadata in a Client Component?
No. metadata and generateMetadata only work in Server Components. Keep the page as a Server Component and move interactive parts into a separate Client Component.
Are Core Web Vitals a ranking factor?
They are one signal among many. Relevant, useful content still matters most, but slow or unstable pages hurt both users and search performance.
Does Google use changeFrequency and priority in sitemaps?
Google has said it ignores them. Accurate lastModified dates are more useful.
Conclusion
Good Next.js SEO is mostly careful setup: clear metadata on every page, an accurate sitemap, correct robots rules, structured data, and pages that load and respond quickly. Set these up once in your layout and shared helpers, then check Search Console regularly.
For details, read the official Next.js metadata docs and Google’s Web Vitals guide.