The Metadata API
Setting page titles, descriptions, and social preview data with the static metadata export and generateMetadata.
3 min de lecture
Search engines and social platforms don't run your JavaScript to figure out what a page is about — they read <head> tags like <title> and Open Graph meta tags. Hand-writing these for every page is exactly the kind of repetitive, easy-to-forget work Next.js's Metadata API replaces with a typed, colocated export.
Static metadata
For a page whose title and description don't depend on any data, export a metadata object directly from page.tsx or layout.tsx:
// app/blog/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Blog",
description: "Articles on web development and design.",
};
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}Next.js turns this into the actual <title> and <meta name="description"> tags in the rendered HTML — you never touch <head> directly.
Metadata that depends on data: generateMetadata
A blog post's title obviously can't be hardcoded — it depends on which post is being viewed. generateMetadata is the dynamic counterpart to the static metadata export, and it receives the same params a page would:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return <article>{post.title}</article>;
}Avoiding duplicate fetches
Notice getPostBySlug gets called in both generateMetadata and the page component above — once for the title, once for the actual content. Wrapping the fetching function in React's cache() means both calls share a single underlying request instead of hitting your data source twice for the same render:
// lib/data.ts
import { cache } from "react";
export const getPostBySlug = cache(async (slug: string) => {
return db.query.posts.findFirst({ where: (p, { eq }) => eq(p.slug, slug) });
});Metadata inherits and merges down the tree
Metadata from a layout applies to every page beneath it, and a page's own metadata merges with (and can override) its ancestors'. A root layout might set a default title and description that every page inherits unless that page defines its own — you don't need to repeat site-wide defaults on every single page.
File-based metadata
Some metadata is easier to express as a file than an object — a favicon.ico or opengraph-image.jpg dropped directly into a route folder is picked up automatically, with no export required:
app/
├── favicon.ico
├── opengraph-image.jpg # used for social share previews at "/"
└── blog/
└── opengraph-image.jpg # overrides it specifically for "/blog"For images that need to be generated dynamically per page — an OG image showing a specific blog post's title rendered as an image — opengraph-image.tsx can export a component built with ImageResponse from next/og, which renders JSX and CSS into an actual PNG at request or build time.
Getting titles, descriptions, and social preview images right isn't just polish — it's often the difference between a link that looks trustworthy when shared and one that looks broken.