Next.js — Metadata API
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.
| 1 | // Export a metadata object from any page.tsx or layout.tsx |
| 2 | import type { Metadata } from "next"; |
| 3 | |
| 4 | export 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 | |
| 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 | |
| 72 | export default function MyPage() { |
| 73 | return <div>Content</div>; |
| 74 | } |
| 1 | // Use generateMetadata for dynamic metadata based on params, data, etc. |
| 2 | |
| 3 | import type { Metadata } from "next"; |
| 4 | import { db } from "@/lib/database"; |
| 5 | |
| 6 | // Dynamic metadata for blog posts |
| 7 | export 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 |
| 53 | export 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
| 1 | // Template titles — add a suffix to all page titles |
| 2 | |
| 3 | // app/layout.tsx |
| 4 | import type { Metadata } from "next"; |
| 5 | |
| 6 | export 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 |
| 16 | export 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 |
| 22 | export 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 |
| 31 | export const metadata: Metadata = { |
| 32 | metadataBase: new URL("https://example.com"), |
| 33 | }; |
| 34 | |
| 35 | // app/blog/page.tsx |
| 36 | export 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 |
| 47 | const 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 | |
| 53 | export const metadata: Metadata = { |
| 54 | metadataBase: new URL(baseUrl), |
| 55 | }; |
| 1 | // OG images — static, dynamic, or conditional |
| 2 | |
| 3 | // Static image |
| 4 | export const metadata: Metadata = { |
| 5 | openGraph: { |
| 6 | images: "https://example.com/og.png", |
| 7 | }, |
| 8 | }; |
| 9 | |
| 10 | // Multiple images for different sizes |
| 11 | export 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 |
| 22 | import { ImageResponse } from "next/og"; |
| 23 | |
| 24 | export const contentType = "image/png"; |
| 25 | export const size = { width: 1200, height: 630 }; |
| 26 | export const alt = "Blog Post"; |
| 27 | |
| 28 | export 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 |
| 75 | export 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 | } |
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.