Next.js Metadata and SEO
Generate metadata, open graph tags, and improve SEO automatically.
TL;DR
- 01Export metadata constant to set page titles and descriptions.
- 02Use generateMetadata for dynamic metadata from data.
- 03Use Open Graph tags for social media sharing.
Tips
- 01Use generateMetadata with dynamic data to create unique, SEO-friendly titles and descriptions for each page.
Warnings
- 01Always include Open Graph images with correct dimensions (1200x630) to ensure proper display on social media.
Static Metadata
- Export metadata constant for static pages.
import { Metadata } from "next"; export const metadata: Metadata = { title: "About Us", description: "Learn more about our company and mission.", keywords: ["about", "company", "mission"] }; export default function About() { return <div>About content</div>; } - Metadata sets HTML head tags automatically.
- Improves SEO and social media sharing.
- Use a layout-level metadata object to apply defaults across all pages.
// app/layout.tsx export const metadata: Metadata = { title: { default: "My Site", template: "%s | My Site" }, description: "Default description for all pages." }; - Child page metadata merges with and overrides layout metadata.
// app/about/page.tsx export const metadata: Metadata = { title: "About" // becomes "About | My Site" via template };
Dynamic Metadata
- Generate metadata from data or parameters.
import { Metadata } from "next"; export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { const { slug } = await params; // params is a Promise in Next.js 15 const post = await getPost(slug); return { title: post.title, description: post.excerpt, authors: [{ name: post.author }] }; } export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); return <article>{post.content}</article>; } - generateMetadata runs on the server.
- Has access to params and other request data.
- Fetch and cache data inside generateMetadata to avoid duplicate requests.
export async function generateMetadata({ params }): Promise<Metadata> { const product = await getProduct(params.id); // Next.js deduplicates this fetch return { title: product.name, description: product.description }; } export default async function Page({ params }) { const product = await getProduct(params.id); // same fetch, cached return <ProductDetail product={product} />; } - Return notFound() inside generateMetadata to trigger a 404 page early.
export async function generateMetadata({ params }): Promise<Metadata> { const post = await getPost(params.slug); if (!post) notFound(); return { title: post.title }; }
Open Graph Tags
- Set Open Graph tags for social sharing.
export const metadata: Metadata = { title: "My Post", description: "Read my latest blog post", openGraph: { title: "My Post", description: "Read my latest blog post", url: "https://example.com/blog/my-post", siteName: "My Blog", images: [ { url: "https://example.com/og-image.png", width: 1200, height: 630 } ], type: "article" } }; - Open Graph improves how links look on social media.
- Include images for better engagement.
- Use dynamic OG images with Next.js ImageResponse for per-page previews.
// app/og/route.tsx import { ImageResponse } from "next/og"; export async function GET(req: Request) { const { searchParams } = new URL(req.url); return new ImageResponse(<div>{searchParams.get("title")}</div>); } - Reference the dynamic OG route in your metadata image field.
openGraph: { images: [`/og?title=${encodeURIComponent(post.title)}`] }
Twitter Card Tags
- Add Twitter Card metadata for tweets.
export const metadata: Metadata = { title: "My Post", description: "Read my latest blog post", twitter: { card: "summary_large_image", title: "My Post", description: "Read my latest blog post", images: ["https://example.com/og-image.png"], creator: "@myhandle" } }; - Twitter Cards make tweets more engaging.
- Use summary_large_image for best results.
- Set twitter.site to your handle for attribution on shared links.
twitter: { card: "summary_large_image", site: "@mycompany", creator: "@authorhandle" } - Reuse the same image URL for both OG and Twitter to reduce duplication.
const ogImage = "https://example.com/og-image.png"; export const metadata: Metadata = { openGraph: { images: [ogImage] }, twitter: { images: [ogImage] } };
Structured Data
- Add JSON-LD structured data for search engines.
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); const schema = { "@context": "https://schema.org", "@type": "BlogPosting", headline: post.title, description: post.excerpt, image: post.image, author: { "@type": "Person", name: post.author }, datePublished: post.date }; return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} /> ); } - Structured data helps search engines understand content.
- Use schema.org types for consistency.
- Add BreadcrumbList schema to help search engines show site hierarchy.
const breadcrumb = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [ { "@type": "ListItem", position: 1, name: "Home", item: "/" }, { "@type": "ListItem", position: 2, name: "Blog", item: "/blog" } ] }; - Validate structured data with Google's Rich Results Test tool before deploying.
FAQ
Export an async generateMetadata function from your page component that receives params and searchParams, fetches any needed data, and returns a metadata object with title and description fields. This runs server-side and generates unique metadata per route.
The exported metadata constant is for static values known at build time, while generateMetadata is an async function for values that depend on dynamic data like route params or database fetches. Use metadata when the values never change, generateMetadata when they do.
Include an openGraph object inside your metadata export or generateMetadata return, with fields like title, description, url, and images. The images array should contain objects with url, width: 1200, and height: 630 for correct social previews.
Render a <script type='application/ld+json'> tag inside your page component using JSX, with dangerouslySetInnerHTML set to __html: JSON.stringify(yourSchemaObject). Place it in the component's return alongside your content, not inside the metadata export.
Yes, add a twitter object to your metadata with fields like card, title, description, and images. If omitted, many platforms fall back to Open Graph values, but explicitly setting twitter gives you control over how your content appears specifically on X/Twitter.