Skip to main content
Chapter 7 Deployment and Performance

Metadata API and SEO

16 min read Lesson 27 / 28

SEO with Next.js Metadata API

Next.js 15 provides a powerful, type-safe Metadata API that generates <head> tags for SEO, social sharing (Open Graph, Twitter Cards), and rich search results.

Static Metadata

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
    metadataBase: new URL('https://myapp.com'),
    title: {
        template: '%s | My App',
        default: 'My App — Build Better Things',
    },
    description: 'A modern full-stack app built with Next.js 15',
    keywords: ['Next.js', 'React', 'TypeScript', 'full-stack'],
    authors: [{ name: 'Your Name', url: 'https://myapp.com' }],
    openGraph: {
        type: 'website',
        locale: 'en_US',
        url: 'https://myapp.com',
        siteName: 'My App',
        images: [
            {
                url: '/og-image.jpg',
                width: 1200,
                height: 630,
                alt: 'My App',
            },
        ],
    },
    twitter: {
        card: 'summary_large_image',
        creator: '@yourhandle',
    },
    robots: {
        index: true,
        follow: true,
        googleBot: { index: true, follow: true },
    },
};

Dynamic Metadata for Content Pages

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata({
    params,
}: {
    params: Promise<{ slug: string }>;
}): Promise<Metadata> {
    const { slug } = await params;
    const post = await db.post.findUnique({
        where: { slug },
        select: {
            title: true,
            excerpt: true,
            author: { select: { name: true } },
        },
    });

    if (!post) return { title: 'Post Not Found' };

    return {
        title: post.title,
        description: post.excerpt ?? undefined,
        authors: [{ name: post.author.name }],
        openGraph: {
            title: post.title,
            description: post.excerpt ?? undefined,
            type: 'article',
            authors: [post.author.name],
            images: [`/api/og?title=${encodeURIComponent(post.title)}`],
        },
    };
}

Dynamic OG Image with ImageResponse

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
import type { NextRequest } from 'next/server';

export const runtime = 'edge';

export function GET(req: NextRequest) {
    const { searchParams } = new URL(req.url);
    const title = searchParams.get('title') ?? 'My App';

    return new ImageResponse(
        (
            <div
                style={{
                    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
                    width: '100%',
                    height: '100%',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    padding: '60px',
                }}
            >
                <h1 style={{ color: 'white', fontSize: 72, textAlign: 'center' }}>
                    {title}
                </h1>
            </div>
        ),
        { width: 1200, height: 630 }
    );
}

Every blog post now gets a unique, dynamically generated social sharing image — with zero manual work.