# 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)
