Menu
Zurück zum Blog
2 min read
Web Development

Next.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

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

ElementEmpfehlung
**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