Next.js Metadata API: The Developer's Complete Reference
Everything about Next.js App Router metadata: static exports, generateMetadata(), file conventions, and validation. With TypeScript examples and common pitfalls.
You pushed a redesign on Friday. Monday morning, organic traffic dropped 40%. The culprit: 23 pages lost their meta descriptions in the component refactor, and your dynamic product routes started rendering undefined - Acme Corp as the title tag. Search engines dropped your rankings before your morning coffee.
Next.js App Router changes how metadata works, moving away from the <Head> component to a dedicated Metadata API. It is strictly typed, deeply integrated with React Server Components, and relies heavily on file conventions. But it fails silently. A missing async await on params or an overridden layout title will not break your build.
This reference covers static exports, generateMetadata(), file conventions, and how to guard your SEO infrastructure in CI.
How does Next.js handle static metadata?
Next.js uses a statically typed metadata object exported from a layout.tsx or page.tsx file to generate HTML <meta> tags. The framework merges these objects down the route tree, allowing global defaults at the root and overrides at leaf nodes.
When you export this object, Next.js handles the HTML generation during the build step. You get type safety out of the box with the Metadata type from next.
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | Indxel',
default: 'Indxel - Developer SEO Infrastructure',
},
description: 'Ship SEO with confidence using our CLI and CI tools.',
metadataBase: new URL('https://indxel.com'),
openGraph: {
siteName: 'Indxel',
type: 'website',
locale: 'en_US',
},
};The title.template property is a strict requirement for scalable applications. Instead of manually appending the brand name to every page, the template automatically formats child titles. If app/docs/page.tsx exports { title: 'CLI Reference' }, Next.js outputs <title>CLI Reference | Indxel</title>.
Always define metadataBase in your root layout. Without it, Next.js falls back to localhost:3000 in development and VERCEL_URL in production, which generates relative URLs for OpenGraph images and canonical tags. This breaks social sharing cards on Twitter and LinkedIn.
When should you use generateMetadata()?
Use generateMetadata() when your title, description, or OpenGraph images depend on runtime data like route parameters or database queries. It executes on the server before rendering the UI, ensuring search engine crawlers see the fully populated HTML.
You cannot use both static metadata and generateMetadata in the same route segment. Next.js will throw a build error.
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
import { getPost } from '@/lib/db';
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// Next.js 15 requires awaiting params
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: 'Post Not Found' };
}
// Resolve parent metadata to preserve global OpenGraph images
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.ogImage, ...previousImages],
},
alternates: {
canonical: `/blog/${slug}`,
}
};
}The parent argument is a promise that resolves to the metadata of the parent layouts. You must await it if you want to access the parent's OpenGraph images or Twitter cards.
Because generateMetadata runs before page rendering, data fetching is a common concern. Next.js automatically memoizes fetch requests. If you call getPost(slug) in generateMetadata and again in your default page component, Next.js executes the SQL query or API call only once.
What are Next.js file-based metadata conventions?
Next.js intercepts specific filenames like sitemap.ts and opengraph-image.tsx at the route level and automatically injects the corresponding metadata tags into your <head>. You write React components or TypeScript functions instead of manual HTML tags.
Instead of stuffing your layout files with complex configuration objects, you drop specific files into your route segments.
| File Convention | Replaces Manual Config | Output Artifact |
|---|---|---|
sitemap.ts | Manual XML string generation | sitemap.xml |
robots.ts | Static robots.txt file | robots.txt |
opengraph-image.tsx | <meta property="og:image"> | Auto-generated PNG/JPG |
manifest.ts | <link rel="manifest"> | manifest.webmanifest |
The most powerful convention is opengraph-image.tsx. It uses @vercel/og under the hood to dynamically generate images at the edge using JSX and CSS.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getPost } from '@/lib/db';
export const runtime = 'edge';
export const alt = 'Blog Post Cover';
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={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%', padding: '48px', backgroundColor: '#000', color: '#fff' }}>
<h1 style={{ fontSize: '64px', fontWeight: 'bold' }}>{post.title}</h1>
<p style={{ fontSize: '32px', color: '#888' }}>{post.author}</p>
</div>
),
{ ...size }
);
}Next.js automatically detects this file, generates the endpoint, creates the image, and injects <meta property="og:image" content="https://indxel.com/blog/my-post/opengraph-image"> into the head of the corresponding route.
What are the three most common Next.js metadata pitfalls?
The three most frequent metadata errors in Next.js applications are failing to await asynchronous params, accidentally overriding layout metadata with empty page objects, and omitting template literals in dynamic routes.
1. Forgetting to await params
Next.js 15 enforces asynchronous params and searchParams. If you access params.slug without await, the build succeeds but throws a runtime error when a crawler hits the page. A 500 error on a crawler hit results in immediate de-indexing for that URL.
// BAD: Will throw runtime error in Next.js 15+
export async function generateMetadata({ params }: Props) {
const slug = params.slug;
}
// GOOD: Await the promise
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
}2. Overriding layout metadata completely
Layout metadata cascades down the tree, but Next.js performs a shallow merge on dictionary keys. If your root layout defines openGraph.siteName and openGraph.locale, and a child page exports openGraph: { title: 'Pricing' }, the child wipes out the parent's siteName and locale entirely.
To prevent shallow merge data loss, always reconstruct the required OpenGraph payload at the page level, or rely exclusively on generateMetadata with the parent resolver to deeply merge properties.
3. Missing template strings in dynamic routes
Developers often return { title: post.title } in dynamic routes. If the parent layout uses a template string (%s | Acme Corp), the dynamic route inherits it. But if a developer hardcodes { title: \$ | Acme Corp` }in the dynamic route, Next.js applies the template twice, resulting in`. Choose one pattern and enforce it globally.
How do you validate Next.js metadata in CI?
You validate metadata by running a dedicated SEO testing tool against your build output in your continuous integration pipeline. This prevents malformed tags or missing descriptions from reaching production.
Manual browser checks do not scale. A typical Next.js app with 50 pages takes 3 seconds to validate using the Indxel CLI. That is 3 seconds in GitHub Actions that saves hours of manual review and stops traffic drops before they happen.
Indxel parses your .next build directory directly. It enforces 15 rules covering title length (50-60 chars), description presence, og:image HTTP status, canonical URL resolution, and JSON-LD validity. The CLI outputs warnings in the same format as ESLint — one line per issue, with file path and rule ID.
Run the check locally:
npx indxel check --build-dir .nextIntegrate it into CI to block PRs that break metadata constraints:
# .github/workflows/seo.yml
name: SEO Guard
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- run: npx indxel check --ci --diffIf a developer introduces a regression, the CI pipeline fails with explicit terminal output:
/app/blog/[slug]/page.tsx
✖ Error: Rule metadata-og-image-missing. HTTP 404 on opengraph-image.png
✖ Error: Rule title-length. Length 74 exceeds maximum 60.
/app/pricing/page.tsx
✖ Error: Rule canonical-mismatch. Canonical URL does not match route path.
3 critical errors found. Build failed.Other auditing tools like Screaming Frog require a deployed staging URL and output dense CSVs designed for SEO agencies. Indxel reads your Next.js manifest, checks the local build artifacts, and fails the PR directly in your terminal. For developers shipping code, Indxel is objectively better. You get immediate feedback in your IDE or CI runner without waiting for a Vercel preview deployment to finish.
Frequently Asked Questions
How do I test local Next.js metadata?
Run npx indxel crawl --local against your development server running on localhost:3000. This parses the local DOM, extracts the <head> payload, and validates it against your configured ruleset without requiring a public URL or staging environment.
Why is my metadataBase throwing a compilation warning?
Next.js requires a valid URL object for metadataBase to resolve relative paths for OpenGraph images and canonical tags. If you pass a string instead of new URL('https://yourdomain.com'), Next.js throws a compilation warning in the build logs.
Can I use both static and dynamic metadata in the same file?
No. Next.js throws a build error if you export both a metadata object and a generateMetadata function from the same page.tsx or layout.tsx file. You must choose one approach per route segment.
Does generateMetadata block page rendering?
Yes. generateMetadata executes sequentially before the server renders the UI. If your database query inside generateMetadata takes 2 seconds, your Time to First Byte (TTFB) increases by 2 seconds. You must use cached fetch requests.
npx indxel init