# 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 (Merged & Deduplicated) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Generated: │ │ ├── │ │ ├── <meta name="description"> │ │ ├── <meta property="og:*"> │ │ ├── <meta name="twitter:*"> │ │ ├── <link rel="canonical"> │ │ └── <script type="application/ld+json"> │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Static Metadata ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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: 1. **Metadata API**: Type-safe, automatisch optimiert 2. **Dynamic OG Images**: Generiert on-the-fly 3. **Sitemap/Robots**: File-based Generation 4. **JSON-LD**: Strukturierte Daten Support SEO-Optimierung war nie einfacher. --- ## Bildprompts 1. "SEO dashboard showing metadata configuration, Next.js interface" 2. "Open Graph preview on social media, rich snippet display" 3. "Sitemap visualization with page hierarchy, SEO structure" --- ## Quellen - [Next.js Metadata Documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) - [Strapi Next.js SEO Guide](https://strapi.io/blog/nextjs-seo) - [Medium SEO Guide](https://medium.com/@thomasaugot/the-complete-guide-to-seo-optimization-in-next-js-15-1bdb118cffd7) - [Google Search Central](https://developers.google.com/search/docs)