Introduction đ
Good metadata is what makes a page look great in search results, social shares, and browser tabs. Next.js provides a dedicated Metadata API that handles titles, descriptions, Open Graph tags, icons, and more â both statically and dynamically. This tutorial covers everything needed to make your app discoverable and shareable.
Information
What is Metadata? đ§
Metadata is information about a page rather than its visible content â things like the page title, description, and preview image. It lives in the <head> of the HTML document and is read by browsers, search engines, and social platforms.
- SEO metadata â helps search engines understand and rank a page.
- Social metadata â controls how a link preview appears when shared.
Why Metadata Matters đ
Well-crafted metadata directly impacts click-through rates from search results, how professional your links look when shared on social media, and how well search engines can categorize your content.
Tip
Metadata API đ ī¸
Next.js exposes two ways to define metadata: exporting a static metadata object, or exporting a dynamic generateMetadata function, both from a page.tsx or layout.tsx file.
app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "My App",
description: "The best app for managing your tasks.",
};
export default function Page() {
return <h1>Welcome</h1>;
}Static Metadata đ§
Use the exported metadata object when a page's title and description are fixed and known ahead of time â this is the simplest and most common case.
app/about/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us",
description: "Learn more about our mission and team.",
};
export default function About() {
return <h1>About Us</h1>;
}Dynamic Metadata đ
When metadata depends on runtime data â like a blog post's title fetched from a database â use generateMetadata instead of the static object.
app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <h1>{post.title}</h1>;
}generateMetadata đ§Ŧ
generateMetadata is an async function that receives the same params and searchParams as the page itself, and can fetch data to build a fully personalized metadata object.
Note
Metadata Merging & Inheritance đ§Ŧđ
Metadata exported from a layout.tsx is merged with metadata from the matching page.tsx, with the page's values taking precedence for any key defined in both. Most fields merge shallowly â an object like openGraph is replaced wholesale by the page's version rather than deep-merged field by field, so a page that sets its own openGraph must repeat any values it still wants from the layout.
app/layout.tsx
export const metadata = {
title: "My App",
openGraph: { siteName: "My App", images: ["/default-og.png"] },
};app/blog/page.tsx
export const metadata = {
// This REPLACES the layout's openGraph entirely â siteName is lost
// unless it's repeated here.
openGraph: { images: ["/blog-og.png"] },
};Best Practice
Page Titles đ
Titles can be set as a simple string, or as a template object that combines a parent layout's title pattern with each page's specific title.
app/layout.tsx
export const metadata = {
title: {
template: "%s | My App",
default: "My App",
},
};app/dashboard/page.tsx
export const metadata = {
title: "Dashboard", // renders as "Dashboard | My App"
};Absolute Titles đ¯
Sometimes a page needs to opt out of a parent's title template entirely â a landing page or a co-branded page that shouldn't get the usual %s | My App suffix. The title.absolute field ignores any inherited template and renders exactly what you give it.
app/partner-landing/page.tsx
export const metadata = {
title: {
absolute: "Partner Co. Ã My App", // ignores the "%s | My App" template
},
};Viewport & Theme Color đą
Since Next.js 14, viewport and themeColor are no longer part of the metadata object â they live in a separate viewport export (or a generateViewport function for dynamic values). Keeping them separate avoids blocking metadata resolution on values that don't affect SEO or social sharing.
app/layout.tsx
import type { Viewport } from "next";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#000000",
};Tip
Meta Descriptions đ
The description field controls the short summary shown beneath a page's title in search results. Aim for a concise, compelling sentence under roughly 160 characters.
app/pricing/page.tsx
export const metadata = {
description: "Simple, transparent pricing plans for teams of any size.",
};Keywords đ
The keywords field lets you list relevant terms for a page, though modern search engines rely far more heavily on content quality and structure than on this field.
app/page.tsx
export const metadata = {
keywords: ["task management", "productivity", "team collaboration"],
};Note
Authors, Publisher & Other Basic Fields âī¸
A handful of smaller fields round out a page's basic metadata: authors and creator credit who made the content, publisher names the organization behind it, category classifies the content type, and formatDetection controls whether phone numbers, addresses, or emails get auto-linked by mobile browsers.
app/blog/[slug]/page.tsx
export const metadata = {
authors: [{ name: "Jane Doe", url: "https://example.com/jane" }],
creator: "Jane Doe",
publisher: "My App Inc.",
category: "technology",
formatDetection: { telephone: false },
};Site Verification đ
The verification field injects the meta tags that search consoles and webmaster tools use to confirm you own a site â Google Search Console, Bing Webmaster Tools, Yandex, and others each look for their own tag.
app/layout.tsx
export const metadata = {
verification: {
google: "google-site-verification-code",
yandex: "yandex-verification-code",
other: { me: ["my-email@example.com"] },
},
};Tip
Canonical URLs đ
A canonical URL tells search engines which version of a page is the "official" one, preventing duplicate-content issues when the same content is reachable via multiple URLs.
app/products/[id]/page.tsx
export async function generateMetadata({ params }: { params: { id: string } }) {
return {
alternates: {
canonical: `https://example.com/products/${params.id}`,
},
};
}metadataBase đ§ą
Fields like openGraph.images, twitter.images, and alternates.canonical often use relative paths for convenience. Next.js needs a base URL to resolve those into absolute URLs, which is exactly what metadataBase provides â set once on the root layout, it applies to every relative URL used anywhere in the app's metadata.
app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
openGraph: {
images: "/og.png", // resolves to https://example.com/og.png
},
};Best Practice
Open Graph Metadata đŧī¸
Open Graph tags control how a link appears when shared on platforms like Facebook, LinkedIn, and Slack â including the preview image, title, and description.
app/page.tsx
export const metadata = {
openGraph: {
title: "My App",
description: "The best app for managing your tasks.",
url: "https://example.com",
siteName: "My App",
images: [{ url: "https://example.com/og.png", width: 1200, height: 630 }],
type: "website",
},
};Twitter Cards đĻ
The twitter metadata field customizes how a link appears specifically when shared on X, independent of the general Open Graph settings.
app/page.tsx
export const metadata = {
twitter: {
card: "summary_large_image",
title: "My App",
description: "The best app for managing your tasks.",
images: ["https://example.com/twitter-card.png"],
},
};Dynamic Open Graph Images đŦ
Beyond a static opengraph-image.png, an opengraph-image.tsx file can generate a social preview image on the fly using JSX â perfect for embedding a blog post's title or a product's price directly into the share image.
app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return new ImageResponse(
(
<div style={{ fontSize: 64, background: "#000", color: "#fff", width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
{post.title}
</div>
),
{ ...size }
);
}Tip
Icons đ¨
Next.js automatically detects icon files placed in the app directory â like icon.png or icon.tsx â and injects the appropriate <link> tags without manual configuration.
Favicons đ
Placing a favicon.ico file directly in the app directory is the simplest way to set a site-wide favicon, automatically picked up with no extra configuration needed.
App Icons đą
An icon.tsx file can generate an icon dynamically at request time using JSX, letting you programmatically create favicons that reflect app state or branding variables.
app/icon.tsx
import { ImageResponse } from "next/og";
export const size = { width: 32, height: 32 };
export const contentType = "image/png";
export default function Icon() {
return new ImageResponse(
<div style={{ fontSize: 24, background: "black", color: "white" }}>A</div>,
{ ...size }
);
}Apple Touch Icons đ
An apple-icon.png file provides a dedicated, higher-resolution icon specifically used when a user adds your site to their iOS home screen.
Manifest File đ
A manifest.ts (or static manifest.json) file describes your app for Progressive Web App installation, including its name, icons, and theme colors.
app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
short_name: "MyApp",
icons: [{ src: "/icon.png", sizes: "192x192", type: "image/png" }],
theme_color: "#000000",
background_color: "#ffffff",
display: "standalone",
};
}Static Manifest (manifest.json) đ
If your manifest data never changes at runtime, you can skip the function entirely and drop a plain manifest.json file straight into the app directory. Next.js detects it automatically and serves it at /manifest.webmanifest, exactly like the generated version.
app/manifest.json
{
"name": "My App",
"short_name": "MyApp",
"icons": [{ "src": "/icon.png", "sizes": "192x192", "type": "image/png" }],
"theme_color": "#000000",
"background_color": "#ffffff",
"display": "standalone"
}Tip
Per-Page Robots Meta Tag đĻ
Don't confuse this with the site-wide robots.txt file below â the metadata.robots field controls a per-page <meta name="robots"> tag, letting you keep an individual page out of the index (e.g. a thank-you page or an internal preview) without touching the crawl rules for the rest of the site.
app/preview/page.tsx
export const metadata = {
robots: {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
"max-video-preview": -1,
"max-image-preview": "large",
},
},
};Note
Robots.txt đ¤
A robots.ts file generates the robots.txt file that tells search engine crawlers which parts of your site they're allowed â or not allowed â to index.
app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/", disallow: "/admin" },
sitemap: "https://example.com/sitemap.xml",
};
}Static Robots.txt đ
Rules that never change can just be a plain robots.txt file placed in the app directory â no function needed. Next.js serves it as-is at the site root.
app/robots.txt
User-agent: *
Allow: /
Disallow: /admin
Sitemap: https://example.com/sitemap.xmlTip
Sitemap đēī¸
A sitemap.ts file generates an XML sitemap listing all the important URLs on your site, helping search engines discover and crawl pages more efficiently.
app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: "https://example.com", lastModified: new Date() },
{ url: "https://example.com/about", lastModified: new Date() },
];
}Static Sitemap (sitemap.xml) đ§
For a small, unchanging set of URLs, a hand-written sitemap.xml file dropped into the app directory works just as well as the generated version, and avoids running any code at build or request time.
app/sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
</url>
<url>
<loc>https://example.com/about</loc>
</url>
</urlset>Note
Dynamic Sitemap đ
For sites with many pages â like a blog with hundreds of posts â sitemap.ts can fetch data and generate URLs dynamically, rather than listing every route by hand.
app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
return posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
}));
}Sitemap Size Limits & Splitting đ§Š
A single sitemap file is capped at 50,000 URLs by the sitemap protocol. For large sites that exceed this, Next.js supports generateSitemaps to split the URL set into multiple sitemap files, each served at its own indexed route and automatically linked together.
app/sitemap.ts
import type { MetadataRoute } from "next";
export async function generateSitemaps() {
const totalPosts = await getPostCount();
const pageCount = Math.ceil(totalPosts / 50000);
return Array.from({ length: pageCount }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts({ offset: id * 50000, limit: 50000 });
return posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
}));
}Note
Metadata Routes đˇī¸
Beyond sitemap.ts and robots.ts, Next.js supports several other special files â like opengraph-image.tsx and manifest.ts â collectively known as Metadata Routes, generated automatically at build or request time.
| File | Static Alternative | Generates |
|---|---|---|
| sitemap.ts | sitemap.xml | sitemap.xml |
| robots.ts | robots.txt | robots.txt |
| manifest.ts | manifest.json | manifest.webmanifest |
| opengraph-image.tsx | opengraph-image.png | social preview image |
| twitter-image.tsx | twitter-image.png | X/Twitter preview image |
| icon.tsx | icon.png | favicon |
Tip
Structured Data (JSON-LD) đ§ž
JSON-LD embeds machine-readable structured data directly in the page, helping search engines display rich results like star ratings, breadcrumbs, or product prices.
app/products/[id]/page.tsx
export default async function Product({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
offers: { "@type": "Offer", price: product.price },
};
return (
<>
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
<h1>{product.name}</h1>
</>
);
}Tip
Alternate Languages đ
The alternates.languages field defines hreflang tags, telling search engines which URL to serve for users in different languages or regions.
app/page.tsx
export const metadata = {
alternates: {
languages: {
"en-US": "https://example.com/en-US",
"es-ES": "https://example.com/es-ES",
},
},
};Social Sharing đ¤
Combining Open Graph and Twitter Card metadata ensures a link looks polished and professional no matter which platform it's shared on.
Best Practice
SEO Best Practices â
- Write unique, descriptive titles and descriptions for every page.
- Use generateMetadata for pages whose content is fetched dynamically.
- Keep your sitemap.ts (or static sitemap.xml) in sync with your actual published content.
- Add structured data for content types like products, articles, or events where it applies.
- Set metadataBase on the root layout so relative image URLs resolve correctly in production.
Performance & SEO âĄ
Search engines increasingly factor in Core Web Vitals â loading speed, interactivity, and visual stability â as ranking signals, making Next.js's rendering and streaming features directly beneficial to SEO.
Accessibility & SEO âŋ
Semantic HTML, meaningful alt text on images, and a logical heading hierarchy all improve both accessibility and how well search engines can parse and understand your content.
Common SEO Mistakes â ī¸
- Leaving every page with the same generic title and description.
- Forgetting to update sitemap.ts / sitemap.xml as new content is added.
- Missing Open Graph images, resulting in blank or broken social link previews.
- Blocking important pages accidentally via an overly broad robots.ts / robots.txt rule.
- Forgetting metadataBase, leaving relative OG/Twitter image URLs unresolved in production.
- Confusing the per-page robots meta field with the site-wide robots.txt file â they solve different problems.