2 min read
Web DevelopmentNext.js SEO & Metadata Optimization
SEO für Next.js 15. Metadata API, Open Graph, Twitter Cards und dynamische Meta-Tags für bessere Suchmaschinenplatzierung.
Next.js SEOMetadata APIOpen GraphTwitter CardsMeta TagsgenerateMetadata

Next.js SEO & Metadata Optimization
Meta-Description: SEO für Next.js 15. Metadata API, Open Graph, Twitter Cards und dynamische Meta-Tags für bessere Suchmaschinenplatzierung.
Keywords: Next.js SEO, Metadata API, Open Graph, Twitter Cards, Meta Tags, generateMetadata, SEO Optimization
Einführung
Next.js 15 bietet eine revolutionäre Metadata API für SEO. Type-safe, automatisch optimiert und perfekt für dynamische Inhalte – von Blogs bis E-Commerce.
Metadata API Overview
┌─────────────────────────────────────────────────────────────┐
│ NEXT.JS METADATA SYSTEM │
├─────────────────────────────────────────────────────────────┤
│ │
│ Static Metadata: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ export const metadata = { │ │
│ │ title: 'Page Title', │ │
│ │ description: 'Page description' │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Dynamic Metadata: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ export async function generateMetadata({ params }) │ │
│ │ return { title: await getTitle(params.id) } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Inheritance: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ layout.tsx (Base) │ │
│ │ ↓ │ │
│ │ page.tsx (Override/Extend) │ │
│ │ ↓ │ │
│ │ Final <head> (Merged & Deduplicated) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Generated: │
│ ├── <title> │
│ ├── <meta name="description"> │
│ ├── <meta property="og:*"> │
│ ├── <meta name="twitter:*"> │
│ ├── <link rel="canonical"> │
│ └── <script type="application/ld+json"> │
│ │
└─────────────────────────────────────────────────────────────┘Static Metadata
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
// Basis
title: {
default: 'Meine Website',
template: '%s | Meine Website' // Für Child Pages
},
description: 'Die beste Website für ...',
// Keywords (weniger wichtig für Google, aber für andere)
keywords: ['Next.js', 'React', 'SEO'],
// Autoren
authors: [{ name: 'Max Mustermann', url: 'https://example.com' }],
creator: 'Max Mustermann',
publisher: 'Meine Firma',
// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1
}
},
// Icons
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png'
},
// Manifest
manifest: '/site.webmanifest',
// Open Graph
openGraph: {
type: 'website',
locale: 'de_DE',
url: 'https://example.com',
siteName: 'Meine Website',
title: 'Meine Website',
description: 'Die beste Website für ...',
images: [
{
url: 'https://example.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'Meine Website'
}
]
},
// Twitter
twitter: {
card: 'summary_large_image',
title: 'Meine Website',
description: 'Die beste Website für ...',
images: ['https://example.com/twitter-image.jpg'],
creator: '@username'
},
// Verification
verification: {
google: 'google-site-verification-code',
yandex: 'yandex-verification-code',
other: {
'msvalidate.01': 'bing-verification-code'
}
},
// Alternates (für i18n)
alternates: {
canonical: 'https://example.com',
languages: {
'de-DE': 'https://example.com/de',
'en-US': 'https://example.com/en'
}
},
// Category
category: 'technology'
};
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body>{children}</body>
</html>
);
}Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
import { getBlogPost, getAllPosts } from '@/lib/blog';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ slug: string }>;
}
// Dynamische Metadata Generation
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getBlogPost(slug);
if (!post) {
return {
title: 'Not Found'
};
}
// Parent Metadata für Merge
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
tags: post.tags,
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title
},
...previousImages
]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage]
},
alternates: {
canonical: `https://example.com/blog/${slug}`
}
};
}
// Static Params für Build-Zeit Generation
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
const post = await getBlogPost(slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Dynamic OG Images
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getBlogPost } from '@/lib/blog';
export const runtime = 'edge';
export const alt = 'Blog Post Image';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OGImage({
params
}: {
params: { slug: string };
}) {
const post = await getBlogPost(params.slug);
// Font laden
const interBold = fetch(
new URL('./Inter-Bold.ttf', import.meta.url)
).then(res => res.arrayBuffer());
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
backgroundColor: '#1a1a2e',
padding: 60,
backgroundImage: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
}}
>
{/* Logo */}
<div
style={{
position: 'absolute',
top: 60,
left: 60,
display: 'flex',
alignItems: 'center',
gap: 16
}}
>
<div
style={{
width: 48,
height: 48,
backgroundColor: '#00d9ff',
borderRadius: 8
}}
/>
<span style={{ color: '#ffffff', fontSize: 24 }}>
Meine Website
</span>
</div>
{/* Title */}
<div
style={{
fontSize: 64,
fontWeight: 'bold',
color: '#ffffff',
lineHeight: 1.2,
maxWidth: '80%'
}}
>
{post?.title || 'Blog Post'}
</div>
{/* Meta */}
<div
style={{
marginTop: 24,
display: 'flex',
gap: 24,
color: '#888888',
fontSize: 24
}}
>
<span>{post?.author.name}</span>
<span>•</span>
<span>{post?.readingTime} min read</span>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: await interBold,
style: 'normal',
weight: 700
}
]
}
);
}Sitemap Generation
// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/blog';
import { getAllProducts } from '@/lib/products';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com';
// Static Pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.5
}
];
// Blog Posts
const posts = await getAllPosts();
const blogPages: MetadataRoute.Sitemap = posts.map(post => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7
}));
// Products
const products = await getAllProducts();
const productPages: MetadataRoute.Sitemap = products.map(product => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(product.updatedAt),
changeFrequency: 'daily',
priority: 0.9
}));
return [...staticPages, ...blogPages, ...productPages];
}Robots.txt
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://example.com';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/private/']
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/admin/']
}
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl
};
}JSON-LD Structured Data
// components/JsonLd.tsx
import Script from 'next/script';
interface ArticleJsonLdProps {
title: string;
description: string;
publishedTime: string;
modifiedTime: string;
authorName: string;
images: string[];
url: string;
}
export function ArticleJsonLd({
title,
description,
publishedTime,
modifiedTime,
authorName,
images,
url
}: ArticleJsonLdProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: title,
description,
image: images,
datePublished: publishedTime,
dateModified: modifiedTime,
author: {
'@type': 'Person',
name: authorName
},
publisher: {
'@type': 'Organization',
name: 'Meine Website',
logo: {
'@type': 'ImageObject',
url: 'https://example.com/logo.png'
}
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url
}
};
return (
<Script
id="article-jsonld"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
// Verwendung in Page
export default function BlogPost({ post }) {
return (
<>
<ArticleJsonLd
title={post.title}
description={post.excerpt}
publishedTime={post.publishedAt}
modifiedTime={post.updatedAt}
authorName={post.author.name}
images={[post.coverImage]}
url={`https://example.com/blog/${post.slug}`}
/>
<article>...</article>
</>
);
}SEO Komponenten Library
// lib/seo.ts
import { Metadata } from 'next';
interface SEOConfig {
title: string;
description: string;
canonical?: string;
image?: string;
type?: 'website' | 'article' | 'product';
publishedTime?: string;
modifiedTime?: string;
author?: string;
tags?: string[];
noindex?: boolean;
}
export function generateSEO(config: SEOConfig): Metadata {
const {
title,
description,
canonical,
image = 'https://example.com/default-og.jpg',
type = 'website',
publishedTime,
modifiedTime,
author,
tags,
noindex = false
} = config;
return {
title,
description,
robots: noindex ? {
index: false,
follow: false
} : {
index: true,
follow: true
},
alternates: canonical ? {
canonical
} : undefined,
openGraph: {
title,
description,
type,
url: canonical,
images: [{ url: image, width: 1200, height: 630 }],
...(publishedTime && { publishedTime }),
...(modifiedTime && { modifiedTime }),
...(author && { authors: [author] }),
...(tags && { tags })
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [image]
}
};
}
// Verwendung
export const metadata = generateSEO({
title: 'Mein Artikel',
description: 'Beschreibung...',
canonical: 'https://example.com/artikel',
type: 'article',
author: 'Max Mustermann'
});SEO Checklist
| Element | Empfehlung |
|---|---|
| **Title** | 50-60 Zeichen, Keyword am Anfang |
| **Description** | 150-160 Zeichen, CTA |
| **H1** | Nur eine pro Seite |
| **Images** | Alt-Text, optimierte Größe |
| **URLs** | Kurz, beschreibend, Bindestrich |
| **Canonical** | Immer setzen |
| **OG Image** | 1200x630px |
| **Sitemap** | Alle wichtigen Seiten |
| **robots.txt** | Korrekt konfiguriert |
| **JSON-LD** | Strukturierte Daten |
Fazit
Next.js SEO bietet:
- Metadata API: Type-safe, automatisch optimiert
- Dynamic OG Images: Generiert on-the-fly
- Sitemap/Robots: File-based Generation
- JSON-LD: Strukturierte Daten Support
SEO-Optimierung war nie einfacher.
Bildprompts
- "SEO dashboard showing metadata configuration, Next.js interface"
- "Open Graph preview on social media, rich snippet display"
- "Sitemap visualization with page hierarchy, SEO structure"