Files
Portfolio/blog-posts/74-nextjs-seo-metadata.md
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

16 KiB
Raw Permalink Blame History

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:

  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