|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/metadata-api
$cat docs/next.js-—-metadata-api.md
updated Last week·15 min read·published

Next.js — Metadata API

Next.jsIntermediate🎯Free Tools
Introduction

The Metadata API in Next.js lets you define metadata for your pages to improve SEO and web shareability. You can export a static metadata object or a generateMetadata function for dynamic metadata. Next.js automatically renders the corresponding <head> elements.

Static Metadata
app/my-page/page.tsx
TSX
1// Export a metadata object from any page.tsx or layout.tsx
2import type { Metadata } from "next";
3
4export const metadata: Metadata = {
5 title: "My Page Title",
6 description: "A comprehensive guide to my page",
7 keywords: ["nextjs", "react", "tutorial"],
8 authors: [{ name: "John Doe", url: "https://example.com" }],
9 creator: "John Doe",
10 publisher: "My Company",
11
12 // Canonical URL
13 alternates: {
14 canonical: "https://example.com/my-page",
15 },
16
17 // Open Graph
18 openGraph: {
19 title: "My Page Title",
20 description: "A comprehensive guide to my page",
21 url: "https://example.com/my-page",
22 siteName: "My Site",
23 locale: "en_US",
24 type: "website",
25 images: [
26 {
27 url: "https://example.com/og-image.png",
28 width: 1200,
29 height: 630,
30 alt: "My Page",
31 },
32 ],
33 },
34
35 // Twitter
36 twitter: {
37 card: "summary_large_image",
38 title: "My Page Title",
39 description: "A comprehensive guide to my page",
40 creator: "@username",
41 images: ["https://example.com/twitter-image.png"],
42 },
43
44 // Robots
45 robots: {
46 index: true,
47 follow: true,
48 googleBot: {
49 index: true,
50 follow: true,
51 "max-video-preview": -1,
52 "max-image-preview": "large",
53 "max-snippet": -1,
54 },
55 },
56
57 // Additional metadata
58 icons: {
59 icon: "/favicon.ico",
60 shortcut: "/favicon-16x16.png",
61 apple: "/apple-touch-icon.png",
62 },
63 manifest: "/site.webmanifest",
64 viewport: {
65 width: "device-width",
66 initialScale: 1,
67 maximumScale: 1,
68 },
69 themeColor: "#000000",
70};
71
72export default function MyPage() {
73 return <div>Content</div>;
74}
Dynamic Metadata with generateMetadata
app/blog/[slug]/page.tsx
TSX
1// Use generateMetadata for dynamic metadata based on params, data, etc.
2
3import type { Metadata } from "next";
4import { db } from "@/lib/database";
5
6// Dynamic metadata for blog posts
7export async function generateMetadata({
8 params,
9}: {
10 params: Promise<{ slug: string }>;
11}): Promise<Metadata> {
12 const { slug } = await params;
13 const post = await db.post.findUnique({ where: { slug } });
14
15 if (!post) {
16 return { title: "Post Not Found" };
17 }
18
19 return {
20 title: post.title,
21 description: post.excerpt,
22 keywords: post.tags,
23 alternates: {
24 canonical: "https://example.com/blog/" + slug,
25 },
26 openGraph: {
27 title: post.title,
28 description: post.excerpt,
29 type: "article",
30 publishedTime: post.publishedAt.toISOString(),
31 modifiedTime: post.updatedAt.toISOString(),
32 authors: [post.author.name],
33 tags: post.tags,
34 images: [
35 {
36 url: post.coverImage,
37 width: 1200,
38 height: 630,
39 alt: post.title,
40 },
41 ],
42 },
43 twitter: {
44 card: "summary_large_image",
45 title: post.title,
46 description: post.excerpt,
47 images: [post.coverImage],
48 },
49 };
50}
51
52// Dynamic metadata for user profiles
53export async function generateMetadata({
54 params,
55}: {
56 params: Promise<{ username: string }>;
57}): Promise<Metadata> {
58 const { username } = await params;
59 const user = await db.user.findUnique({ where: { username } });
60
61 if (!user) {
62 return { title: "User Not Found" };
63 }
64
65 return {
66 title: user.displayName + " — Profile",
67 description: user.bio,
68 openGraph: {
69 title: user.displayName,
70 description: user.bio,
71 type: "profile",
72 images: [{ url: user.avatar }],
73 },
74 };
75}

info

generateMetadata is called on the server before rendering. It can be async and access databases, APIs, or params to build dynamic metadata.
Template Titles & metadataBase
app/layout.tsx
TSX
1// Template titles — add a suffix to all page titles
2
3// app/layout.tsx
4import type { Metadata } from "next";
5
6export const metadata: Metadata = {
7 title: {
8 default: "My App", // Used when no title in child
9 template: "%s | My App", // %s is replaced with child title
10 absolute: "My App — Full", // Bypasses template
11 },
12 metadataBase: new URL("https://example.com"),
13};
14
15// app/blog/[slug]/page.tsx
16export const metadata: Metadata = {
17 title: "My Blog Post",
18 // Renders as: <title>My Blog Post | My App</title>
19};
20
21// app/about/page.tsx
22export const metadata: Metadata = {
23 title: {
24 absolute: "About Us",
25 },
26 // Renders as: <title>About Us</title> (no template suffix)
27};
28
29// metadataBase — used to resolve relative URLs in metadata
30// app/layout.tsx
31export const metadata: Metadata = {
32 metadataBase: new URL("https://example.com"),
33};
34
35// app/blog/page.tsx
36export const metadata: Metadata = {
37 openGraph: {
38 title: "Blog",
39 images: "/og-blog.png", // Resolved to https://example.com/og-blog.png
40 },
41 alternates: {
42 canonical: "/blog", // Resolved to https://example.com/blog
43 },
44};
45
46// Generate metadataBase from environment
47const baseUrl = process.env.VERCEL_URL
48 ? "https://" + process.env.VERCEL_URL
49 : process.env.NODE_ENV === "development"
50 ? "http://localhost:3000"
51 : "https://example.com";
52
53export const metadata: Metadata = {
54 metadataBase: new URL(baseUrl),
55};
Open Graph Images
og-images.tsx
TSX
1// OG images — static, dynamic, or conditional
2
3// Static image
4export const metadata: Metadata = {
5 openGraph: {
6 images: "https://example.com/og.png",
7 },
8};
9
10// Multiple images for different sizes
11export const metadata: Metadata = {
12 openGraph: {
13 images: [
14 { url: "https://example.com/og.png", width: 1200, height: 630 },
15 { url: "https://example.com/og-square.png", width: 600, height: 600 },
16 ],
17 },
18};
19
20// Dynamic OG image using Next.js file-based OG generation
21// app/blog/[slug]/opengraph-image.tsx
22import { ImageResponse } from "next/og";
23
24export const contentType = "image/png";
25export const size = { width: 1200, height: 630 };
26export const alt = "Blog Post";
27
28export default async function OGImage({
29 params,
30}: {
31 params: Promise<{ slug: string }>;
32}) {
33 const { slug } = await params;
34 const post = await getPost(slug);
35
36 return new ImageResponse(
37 (
38 <div
39 style={{
40 background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
41 width: "100%",
42 height: "100%",
43 display: "flex",
44 flexDirection: "column",
45 justifyContent: "center",
46 padding: "60px",
47 }}
48 >
49 <h1
50 style={{
51 fontSize: "64px",
52 color: "white",
53 fontWeight: "bold",
54 lineHeight: 1.2,
55 }}
56 >
57 {post.title}
58 </h1>
59 <p
60 style={{
61 fontSize: "28px",
62 color: "#a0a0a0",
63 marginTop: "24px",
64 }}
65 >
66 {post.excerpt}
67 </p>
68 </div>
69 ),
70 { width: 1200, height: 630 }
71 );
72}
73
74// Conditional OG image based on content type
75export async function generateMetadata({ params }): Promise<Metadata> {
76 const post = await getPost(params.slug);
77 return {
78 openGraph: {
79 images: [
80 {
81 url: "/api/og?title=" + encodeURIComponent(post.title),
82 width: 1200,
83 height: 630,
84 },
85 ],
86 },
87 };
88}
Best Practices

1. Always set metadataBase in the root layout — it resolves relative URLs for OG images and canonical links.

2. Use title.template for consistent branding across all pages.

3. Use generateMetadata for dynamic pages (blog posts, products, profiles) that need unique metadata.

4. Include both openGraph and twitter — they use different image sizes and preview formats.

5. Use file-based OG images (opengraph-image.tsx) for dynamic social images without an API route.

6. Set robots metadata to control indexing — use noindex for draft or private pages.

$Blueprint — Engineering Documentation·Section ID: NEXT-META·Revision: 1.0