C.W.K.
Stream
Lesson 06 of 08 · published

Metadata API

~20 min · metadata, OG image, SEO

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Static metadata

Export a metadata object from any layout or page. The framework merges nested layouts' metadata, with the page taking precedence. Title templates compose: %s | My App.

Dynamic metadata

Export an async generateMetadata({ params }) for routes whose tags depend on data (blog post title, product name, user page).

Open Graph images, generated server-side

Drop an opengraph-image.tsx in any segment and Next.js renders it via Vercel's Satori-based ImageResponse. No design tool, no preflight; you author OG images as React JSX.

Code

Static metadata + title template·tsx
// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: { template: '%s | My App', default: 'My App' },
  description: 'Built with Next.js',
  openGraph: {
    title: 'My App',
    description: 'Built with Next.js',
    url: 'https://myapp.com',
    siteName: 'My App',
    type: 'website',
  },
  twitter: { card: 'summary_large_image' },
};
Dynamic metadata for a blog post·tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.ogImage] },
  };
}
Dynamic OG image as JSX·tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function OG({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    (
      <div style={{
        fontSize: 64, background: '#0f172a', color: 'white',
        width: '100%', height: '100%', padding: 48, display: 'flex',
        flexDirection: 'column', justifyContent: 'flex-end',
      }}>
        <div>{post.title}</div>
      </div>
    ),
    { ...size }
  );
}

External links

Exercise

Add a title template at the root layout, dynamic metadata to one blog page, and an opengraph-image.tsx for the same route. Test the OG card with the Twitter Card validator.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.