Files
Portfolio/blog-posts/74-nextjs-seo-metadata.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

662 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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)