Next.js Metadata and SEO
Set page titles, descriptions, Open Graph images, sitemaps, and robots rules with the Next.js Metadata API.
TL;DR
- Export a
metadataobject from a layout or page for static tags. - Export
generateMetadata()when the title depends on fetched data. - Add
sitemap.tsandrobots.tsinappto guide search engines.
Static Metadata
metadataExport this object from a layout or page to set its title and description.
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Acme Store',
description: 'Handmade goods, shipped worldwide.',
};title.templateAdd your site name to every page title from the root layout.
export const metadata: Metadata = {
title: {
default: 'Acme Store',
template: '%s | Acme Store',
},
};metadataBaseSet the base URL so relative image and canonical paths become full URLs.
export const metadata: Metadata = {
metadataBase: new URL('https://acme.com'),
};Dynamic Metadata
generateMetadata()Build the title from fetched data for pages with dynamic content.
export async function generateMetadata(
props: PageProps<'/blog/[slug]'>
): Promise<Metadata> {
const { slug } = await props.params;
const post = await getPost(slug);
return { title: post.title };
}alternates.canonicalPoint search engines to the main URL when a page has several addresses.
return {
title: post.title,
alternates: {
canonical: `/blog/${slug}`,
},
};robotsKeep one page out of search results by setting index to false.
export const metadata: Metadata = {
robots: { index: false, follow: false },
};Social Sharing
openGraphControl the title, text, and image shown when a link is shared.
export const metadata: Metadata = {
openGraph: {
title: 'Acme Store',
description: 'Handmade goods.',
images: ['/og.png'],
},
};twitterRequest the large image card when a link is shared on X.
export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
images: ['/og.png'],
},
};opengraph-image.tsxGenerate the share image from code by adding this file to a route.
// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export default function Image() {
return new ImageResponse(
<div style={{ fontSize: 64 }}>Acme Store</div>,
size
);
}Sitemap and Structured Data
sitemap.tsReturn your URLs from this file to serve them at /sitemap.xml.
// app/sitemap.ts
import type { MetadataRoute } from 'next';
type Sitemap = MetadataRoute.Sitemap;
export default function sitemap(): Sitemap {
return [
{ url: 'https://acme.com' },
{ url: 'https://acme.com/blog' },
];
}robots.tsTell crawlers what they may visit and where to find your sitemap.
// app/robots.ts
import type { MetadataRoute } from 'next';
type Robots = MetadataRoute.Robots;
export default function robots(): Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://acme.com/sitemap.xml',
};
}JSON-LDAdd structured data so search engines can show rich results for a page.
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd),
}}
/>Tips
- Set
metadataBaseonce in the root layout, so relative Open Graph image paths resolve to full URLs. - Use a
title.templatein the root layout, so every page title gets your site name without repeating it.
Warnings
- The
metadataexport only works in Server Components, so it is ignored in any file marked'use client'. - Awaiting
paramsis required insidegenerateMetadata(), because it is a promise just like in the page component.
In Practice
Give every blog post its own title, description, canonical URL, and share image.
- generateMetadata awaits params and loads the post, just like the page does.
- The title feeds the template from the root layout, which adds the site name.
- The canonical path tells search engines which URL is the main one.
- The openGraph block controls how the link looks when it is shared.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPost } from '@/lib/posts';
export async function generateMetadata(
props: PageProps<'/blog/[slug]'>
): Promise<Metadata> {
const { slug } = await props.params;
const post = await getPost(slug);
return {
title: post.title,
description: post.summary,
alternates: { canonical: `/blog/${slug}` },
openGraph: {
title: post.title,
description: post.summary,
images: [post.coverImage],
type: 'article',
},
};
}FAQ
Export a metadata object from layout.tsx or page.tsx with title and description fields. Next.js writes the matching tags into the page .
Export an async generateMetadata() function from the page. Await params, fetch your data, and return the metadata object.
Add app/sitemap.ts and export a default function that returns an array of URLs. Next.js serves the result at /sitemap.xml.
Yes. Pages are rendered on the server, so search engines receive full HTML. The Metadata API also handles titles, social tags, sitemaps, and robots rules.