Quick answer
Use a static metadata export when a page's title and description are fixed. Use generateMetadata when they depend on route params or fetched data. Set metadataBase once for relative metadata URLs, but declare each canonical explicitly with alternates.canonical.
Last verified: 24 August 2026. The examples below were checked against the current Next.js App Router metadata documentation.
What you'll learn
By the end of this you'll know how to set titles and descriptions that actually show up in Google, how to configure Open Graph cards so your links look decent when shared on Slack or LinkedIn, and how to wire up a canonical URL so you don't accidentally compete with yourself in search results. You'll also know the two or three mistakes that trip up everyone when they first switch to the App Router.
The short version: export a static metadata object for fixed pages, use generateMetadata when fields depend on data, set metadataBase once so relative metadata URLs can become absolute, and declare each canonical with alternates.canonical. If you're used to Pages Router's <Head>, the App Router API is a different model.
Who this is for
- You're building a site with the App Router and want it to rank in Google
- You've heard "metadata" but aren't sure if that means the
<title>tag, the description, Open Graph, or all of the above (it's all of the above) - You just switched from Pages Router and your
next/headtricks stopped working
You can skip this if you already export generateMetadata fluently and you're only here to look up exact field names. The Next.js metadata reference is honestly faster for that.
What is metadata?
Metadata is information about your page that lives in the <head> — not visible in the browser window, but read by search engines, social platforms, and browser tabs. The big ones:
<title>— the blue link text in Google results and the browser tab label<meta name="description">— the gray snippet under the title in Google- Open Graph tags (
og:title,og:image, etc.) — what Twitter, LinkedIn, and Slack use when someone pastes your URL
Plain English: metadata is the stuff that makes your page findable and shareable without it appearing in the visible page content.
Prerequisites
- You know how the App Router works (layouts, pages, Server Components)
- You have a Next.js project running locally
- Basic TypeScript comfort — nothing beyond reading types
If the App Router is still new to you, the App Router basics guide is worth reading first. It'll save an hour of confusion about why things aren't where you expect them.
Setup from zero
We'll add metadata to a fresh page so you see every piece in context.
Step 1 — Create a test page
Make src/app/seo-test/page.tsx:
export default function SeoTestPage() {
return (
<main>
<h1>SEO test page</h1>
<p>We will add metadata to this.</p>
</main>
);
}
Visit /seo-test. Bare page, no real title in the tab yet (or it inherits from the layout — we'll fix both).
Step 2 — Add static metadata to the page
In the same file, add an export above the component:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "SEO test",
description: "A scratch page for testing Next.js metadata.",
};
export default function SeoTestPage() {
return (
<main>
<h1>SEO test page</h1>
<p>We will add metadata to this.</p>
</main>
);
}
Save and check the browser tab. The title changed. That's the whole API for static pages.
Step 3 — Add metadataBase to the root layout
Open src/app/layout.tsx and add metadataBase plus a title template:
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://yoursite.com"),
title: {
default: "AI Voice Pro Learn",
template: "%s — AI Voice Pro Learn",
},
description: "Practical Next.js guides for developers.",
};
The template means any page that exports title: "SEO test" will automatically get "SEO test — AI Voice Pro Learn" in the tab and in Google. You stop repeating the site name everywhere.
Core idea / mental model
Next.js metadata flows down the route tree. Whatever you define in the root layout.tsx is the baseline. Child layouts and pages override specific fields. If a page doesn't set a title, it falls back to the layout's title.default.
Mental model: parent sets defaults, children override what they need.
That's why metadataBase belongs in the root layout — it tells Next.js the full domain so relative paths in OG images resolve correctly. And if you forget it, you'll get a console warning and broken social cards every time someone shares a link.
Key terms
- Static metadata —
export const metadata: Metadata = { ... }in a page or layout; values are fixed rather than fetched from route data - Dynamic metadata —
export async function generateMetadata({ params })in a page or layout; resolves as part of rendering and can fetch data - metadataBase — root URL of your site; composes relative URL-based metadata fields into absolute URLs but does not create a canonical by itself
- title template —
{ default, template }object that auto-appends a site name to child titles - canonical URL — the "official" version of a page; prevents duplicate-content issues when the same content lives at multiple paths
- Open Graph — protocol for controlling link preview cards on social platforms
Step-by-step
Dynamic metadata for a blog post
When the title depends on data (a blog post slug, a product name), use generateMetadata. In current App Router versions, params is a Promise:
import type { Metadata } from "next";
import { cache } from "react";
type Props = { params: Promise<{ slug: string }> };
const getPost = cache(async (slug: string) => {
return db.post.findUniqueOrThrow({ where: { slug } });
});
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${slug}` },
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.ogImage ?? "/og-default.png"],
},
};
}
generateMetadata runs on the server before the page renders. You can await database calls or fetch from an API inside it, no problem.
Little tip: Next.js memoizes identical fetch requests across generateMetadata, generateStaticParams, layouts, pages, and Server Components. An arbitrary database helper is not automatically the same thing; wrap it with React's cache, as above, if metadata and the page call it with the same arguments.
Canonical URLs
metadataBase supplies the origin for relative URL fields; it does not infer the current route's canonical. Declare the canonical explicitly, then Next.js composes the relative value with the base URL:
export const metadata: Metadata = {
alternates: {
canonical: "/blog/this-specific-post",
},
};
The canonical should match the successful public URL used by internal links, the sitemap, breadcrumbs, and structured data. Redirecting or alternate tracking URLs should point to that same preferred address.
Robots directives
Use the metadata API for page-level indexing rules. For example, a useful internal search page can remain crawlable for link discovery without being indexed:
export const metadata: Metadata = {
title: "Search",
robots: { index: false, follow: true },
};
Do not use robots.txt to block a page that crawlers must reach to see a noindex directive. Normal public articles should remain indexable unless there is a verified reason otherwise.
Sitemap and robots
These aren't <head> metadata in the traditional sense, but they're part of the same SEO job. In the App Router, both are TypeScript files:
src/app/sitemap.ts— exports a function returning your page URLs; Next.js converts it to XMLsrc/app/robots.ts— exports your crawling rules; Next.js generates the text file
No library needed, no XML to write by hand. Check the MetadataRoute.Sitemap type in the Next.js docs for the exact return shape.
Working examples
Full static metadata for a hub page:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Next.js tutorials",
description: "Practical Next.js guides from setup to production.",
openGraph: {
title: "Next.js tutorials",
description: "Practical Next.js guides from setup to production.",
url: "https://baseline.so/developers/nextjs",
siteName: "AI Voice Pro Learn",
images: [
{
url: "/og/nextjs-tutorials.png",
width: 1200,
height: 630,
alt: "Next.js tutorials on AI Voice Pro Learn",
},
],
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Next.js tutorials",
description: "Practical Next.js guides from setup to production.",
images: ["/og/nextjs-tutorials.png"],
},
};
Little tip: twitter.card: "summary_large_image" gives you the wide image preview on X. Without it you get a tiny thumbnail that most people scroll past. Use the large card on any content page.
Patterns
Static metadata is for pages where the title and description don't come from a database — home page, about page, static landing pages. Runs at build time, zero per-request overhead.
generateMetadata is for pages where the title comes from data — blog posts, product pages, user profiles. It's async so you can fetch freely.
Metadata in layout vs. page: defaults, site name, and OG image fallbacks go in the root layout. Post-specific titles and descriptions go in the individual page or generateMetadata.
Common mistakes
Missing metadataBase — OG images won't resolve in social previews. Add it to the root layout before anything else. This one mistake causes the majority of "my link preview is broken" questions.
Using next/head in App Router pages — That's Pages Router. It doesn't work in App Router. Everything goes through the metadata export or generateMetadata. If you see import Head from "next/head" in an App Router file, delete it.
Assuming nested metadata objects deep-merge — metadata resolves from root to page, but nested fields are replaced rather than recursively merged. If a child defines its own openGraph object, include the fields it still needs or reuse a shared helper. A layout title and a page title do not create two title tags; the more specific value resolves through the metadata cascade.
Exporting both metadata APIs in one segment — a single layout.tsx or page.tsx cannot export both metadata and generateMetadata. Use the static object when values are fixed and the function only when runtime data is required.
Descriptions that are too long — Google shows roughly 150–160 characters. Anything longer gets truncated with an ellipsis and you lose control of the snippet. Write descriptions in that range.
No Open Graph description — <meta name="description"> is for Google. Social platforms use og:description. Set both via the description field and openGraph.description — they can be the same text, but they need to be present separately.
Troubleshooting
OG image not showing on Slack or X: First, confirm metadataBase is in the root layout. Then check the image path — it must be absolute or relative with a valid metadataBase to resolve. Social platforms also cache OG data aggressively; use the Facebook Sharing Debugger or Twitter Card Validator to force a fresh fetch.
Title shows as "undefined": You're using title.template in the layout but the page has no title export. Add a title.default to the layout so there's always a fallback string.
generateMetadata isn't being called: Export it directly from a route segment's page.tsx or layout.tsx, not from a nested component, and make sure that same segment does not also export metadata. Only page segments receive searchParams.
Checklist
- [ ]
metadataBaseset in rootlayout.tsx - [ ] Title template (
{ default, template }) configured in root layout - [ ] Every public page has a unique
titleanddescription - [ ]
openGraph.imagespoints to a real 1200×630 image - [ ]
twitter.cardset to"summary_large_image"on content pages - [ ] Dynamic pages use
generateMetadata, not hardcoded strings - [ ] Every canonical is explicit and agrees with the public route, sitemap, breadcrumbs, and schema
- [ ] Page-level
robotsdirectives are used deliberately; public articles remain indexable - [ ] No route segment exports both
metadataandgenerateMetadata - [ ] No
import Head from "next/head"in any App Router file - [ ]
src/app/sitemap.tslists all published public URLs
Practice task
Pick one dynamic detail page in your project that's missing a description. Add generateMetadata using the same data source as the page; use a static metadata object instead if every value is fixed. Open Chrome DevTools → Elements, confirm the description and canonical are present, then check the page in an Open Graph preview tool.
FAQ
Do I need a library for sitemaps?
No. Write src/app/sitemap.ts that exports a function returning an array of URL objects. Next.js serves the return value as sitemap XML; whether that route is generated ahead of time or dynamically depends on its data and route configuration. The MetadataRoute.Sitemap type shows the exact shape.
Should the root layout use static metadata or generateMetadata?
Usually static metadata, because site-wide defaults are fixed and do not need route data. Use generateMetadata in a layout only when those fields genuinely depend on data available to that segment.
What's the difference between title: "My Site" and title: { default: "My Site", template: "%s — My Site" }?
The plain string makes every page under that layout show "My Site" if they don't override it. The object version lets child pages set a short title (like "About") and automatically get " — My Site" appended. Use the object in root layouts so you stop repeating the brand name in every page file.
Do social platforms read <meta name="description">?
No — they read og:description. Set both: description for Google, openGraph.description for social. The Next.js Metadata type makes them separate fields so you can't accidentally miss one.
What to learn next
Once metadata is dialed in, caching and revalidate is the next SEO-adjacent win — static vs per-request rendering affects crawl speed and TTFB, and the correct API depends on whether Cache Components is enabled.
Building an HTTP surface too? The Next.js Route Handlers guide covers methods, validation, secrets, and the separate caching decision for route.ts. After that, images and fonts and the Lighthouse performance cheat sheet connect metadata work to measured Core Web Vitals.
Official Next.js sources checked
Takeaways
The App Router metadata API is genuinely clean once you understand the cascade. Defaults live in layouts, specifics live in pages, and generateMetadata handles anything that needs data fetched before you can build the tags.
If you remember only one thing: metadataBase resolves relative metadata URLs, but it does not invent the canonical for each route. Set the base once, declare canonical paths explicitly, and keep those paths aligned with internal links, the sitemap, breadcrumbs, and schema.