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.