Przejdź do treści
Backend

WordPress als Headless CMS mit Next.js - Vollstaendiger Leitfaden

Veröffentlicht am:
·5 Min. Lesezeit·Autor: MDS Software Solutions Group

WordPress als Headless

backend

WordPress als Headless CMS mit Next.js

WordPress betreibt ueber 40% aller Websites im Internet. Die traditionelle monolithische Architektur, bei der WordPress sowohl fuer die Inhaltsverwaltung als auch fuer die Darstellung zustaendig ist, weicht jedoch zunehmend dem modernen Headless-Ansatz. In diesem Artikel zeigen wir, wie Sie die Leistungsfaehigkeit von WordPress als Content-Management-System mit der Performance und Flexibilitaet von Next.js im Frontend kombinieren koennen.

Was ist ein Headless CMS?#

Ein Headless CMS ist ein Content-Management-System, das die Backend-Schicht (Inhaltserstellung und -speicherung) von der Frontend-Schicht (Inhaltsdarstellung) trennt. Anstatt HTML-Seiten direkt zu generieren, stellt ein Headless CMS Inhalte ueber eine API bereit -- typischerweise REST oder GraphQL.

Warum WordPress als Headless CMS verwenden?#

  • Vertraute Oberflaeche -- Redakteure kennen bereits das WordPress-Admin-Panel
  • Reichhaltiges Plugin-Oekosystem -- Tausende von Plugins zur Funktionserweiterung
  • Plattformreife -- ueber 20 Jahre Entwicklung und Stabilitaet
  • Grosse Community -- einfacher Zugang zu Support und Dokumentation
  • Volle Frontend-Kontrolle -- beliebige Technologie fuer die Inhaltsdarstellung
  • Bessere Performance -- von Next.js generierte statische Seiten sind blitzschnell
  • Sicherheit -- das Frontend ist vom Admin-Panel getrennt

Headless vs Traditionelles WordPress#

| Merkmal | Traditionelles WordPress | Headless WordPress | |---------|--------------------------|-------------------| | Rendering | Serverseitiges PHP | SSG/SSR/ISR in Next.js | | Performance | Abhaengig von Server und Plugins | Statische Seiten, CDN | | Frontend-Flexibilitaet | Auf PHP-Templates beschraenkt | Volle Freiheit (React, Vue, etc.) | | SEO | Erfordert Plugins (Yoast) | In Next.js integriert | | Sicherheit | Admin-Panel exponiert | Backend verborgen, statisches Frontend | | Skalierbarkeit | Erfordert Caching und Optimierung | Natuerlich skalierbar ueber CDN |

WordPress REST API#

Seit Version 4.7 verfuegt WordPress ueber eine integrierte REST API, die alle grundlegenden Inhaltstypen bereitstellt. Die API ist unter /wp-json/wp/v2/ verfuegbar.

Grundlegende Endpunkte#

# Beitraege abrufen
GET /wp-json/wp/v2/posts

# Einzelnen Beitrag abrufen
GET /wp-json/wp/v2/posts/123

# Seiten abrufen
GET /wp-json/wp/v2/pages

# Kategorien abrufen
GET /wp-json/wp/v2/categories

# Medien abrufen
GET /wp-json/wp/v2/media

# Benutzer abrufen
GET /wp-json/wp/v2/users

Filterung und Paginierung#

Die REST API bietet umfangreiche Filtermoeglichkeiten:

# Beitraege aus einer bestimmten Kategorie
GET /wp-json/wp/v2/posts?categories=5

# Suche
GET /wp-json/wp/v2/posts?search=nextjs

# Paginierung (10 Beitraege pro Seite)
GET /wp-json/wp/v2/posts?per_page=10&page=2

# Sortierung
GET /wp-json/wp/v2/posts?orderby=date&order=desc

# Einbettung verwandter Daten (Autor, Medien, Kategorien)
GET /wp-json/wp/v2/posts?_embed

Registrierung eigener Endpunkte#

Sie koennen die REST API um eigene Endpunkte erweitern:

// functions.php
add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/featured-posts', [
        'methods'  => 'GET',
        'callback' => 'get_featured_posts',
        'permission_callback' => '__return_true',
    ]);
});

function get_featured_posts($request) {
    $posts = get_posts([
        'meta_key'   => 'is_featured',
        'meta_value' => '1',
        'numberposts' => 6,
    ]);

    $data = [];
    foreach ($posts as $post) {
        $data[] = [
            'id'        => $post->ID,
            'title'     => $post->post_title,
            'excerpt'   => get_the_excerpt($post),
            'slug'      => $post->post_name,
            'thumbnail' => get_the_post_thumbnail_url($post, 'large'),
        ];
    }

    return rest_ensure_response($data);
}

WPGraphQL -- GraphQL fuer WordPress#

Obwohl die REST API funktional ist, bietet GraphQL eine deutlich flexiblere Datenabfrage. Das Plugin WPGraphQL fuegt WordPress volle GraphQL-Unterstuetzung hinzu.

WPGraphQL installieren#

# Ueber WP-CLI
wp plugin install wp-graphql --activate

# Oder herunterladen von https://www.wpgraphql.com/

Nach der Installation ist der GraphQL-Endpunkt unter /graphql verfuegbar.

Beispiel-GraphQL-Abfragen#

# Beitraege mit Autor und Kategorien abrufen
query GetPosts {
  posts(first: 10, where: { orderby: { field: DATE, order: DESC } }) {
    nodes {
      id
      databaseId
      title
      slug
      excerpt
      date
      featuredImage {
        node {
          sourceUrl
          altText
          mediaDetails {
            width
            height
          }
        }
      }
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
      categories {
        nodes {
          name
          slug
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

# Einzelnen Beitrag nach Slug abrufen
query GetPostBySlug($slug: ID!) {
  post(id: $slug, idType: SLUG) {
    title
    content
    date
    modified
    seo {
      title
      metaDesc
      opengraphImage {
        sourceUrl
      }
    }
  }
}

Vorteile von GraphQL gegenueber REST API#

  • Genau die benoetigten Daten abrufen -- kein Over-Fetching
  • Eine Abfrage statt vieler -- weniger HTTP-Anfragen
  • Starke Typisierung -- bessere Validierung und IDE-Autovervollstaendigung
  • Introspektion -- automatische Schema-Dokumentation

Next.js mit WordPress einrichten#

Projektinitialisierung#

npx create-next-app@latest wordpress-frontend --typescript --app
cd wordpress-frontend
npm install graphql-request graphql

Umgebungsvariablen konfigurieren#

# .env.local
NEXT_PUBLIC_WORDPRESS_URL=https://cms.ihredomain.de
WORDPRESS_GRAPHQL_URL=https://cms.ihredomain.de/graphql
WORDPRESS_AUTH_REFRESH_TOKEN=ihr-refresh-token
WORDPRESS_PREVIEW_SECRET=ihr-vorschau-geheimnis
REVALIDATION_SECRET=ihr-revalidierungs-geheimnis

GraphQL-Client#

// lib/wordpress.ts
import { GraphQLClient, gql } from 'graphql-request';

const client = new GraphQLClient(
  process.env.WORDPRESS_GRAPHQL_URL!,
  {
    headers: {
      'Content-Type': 'application/json',
    },
  }
);

// Typen
export interface WPPost {
  id: string;
  databaseId: number;
  title: string;
  slug: string;
  excerpt: string;
  content: string;
  date: string;
  modified: string;
  featuredImage: {
    node: {
      sourceUrl: string;
      altText: string;
      mediaDetails: {
        width: number;
        height: number;
      };
    };
  } | null;
  author: {
    node: {
      name: string;
      avatar: { url: string };
    };
  };
  categories: {
    nodes: Array<{ name: string; slug: string }>;
  };
  seo?: {
    title: string;
    metaDesc: string;
    opengraphImage?: { sourceUrl: string };
  };
}

// Alle Beitraege abrufen
export async function getAllPosts(first = 20): Promise<WPPost[]> {
  const query = gql`
    query GetAllPosts($first: Int!) {
      posts(first: $first, where: { orderby: { field: DATE, order: DESC } }) {
        nodes {
          id
          databaseId
          title
          slug
          excerpt
          date
          featuredImage {
            node {
              sourceUrl
              altText
              mediaDetails {
                width
                height
              }
            }
          }
          author {
            node {
              name
              avatar {
                url
              }
            }
          }
          categories {
            nodes {
              name
              slug
            }
          }
        }
      }
    }
  `;

  const data = await client.request<{ posts: { nodes: WPPost[] } }>(
    query,
    { first }
  );

  return data.posts.nodes;
}

// Beitrag nach Slug abrufen
export async function getPostBySlug(slug: string): Promise<WPPost | null> {
  const query = gql`
    query GetPostBySlug($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        id
        databaseId
        title
        slug
        content
        excerpt
        date
        modified
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
        author {
          node {
            name
            avatar {
              url
            }
          }
        }
        categories {
          nodes {
            name
            slug
          }
        }
        seo {
          title
          metaDesc
          opengraphImage {
            sourceUrl
          }
        }
      }
    }
  `;

  const data = await client.request<{ post: WPPost | null }>(
    query,
    { slug }
  );

  return data.post;
}

// Alle Beitrags-Slugs abrufen (fuer generateStaticParams)
export async function getAllPostSlugs(): Promise<string[]> {
  const query = gql`
    query GetAllSlugs {
      posts(first: 1000) {
        nodes {
          slug
        }
      }
    }
  `;

  const data = await client.request<{
    posts: { nodes: Array<{ slug: string }> };
  }>(query);

  return data.posts.nodes.map((node) => node.slug);
}

Blog-Listenseite#

// app/blog/page.tsx
import { getAllPosts } from '@/lib/wordpress';
import Image from 'next/image';
import Link from 'next/link';

export const revalidate = 3600; // ISR: Revalidierung jede Stunde

export default async function BlogPage() {
  const posts = await getAllPosts();

  return (
    <main className="container mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {posts.map((post) => (
          <article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden">
            {post.featuredImage && (
              <Image
                src={post.featuredImage.node.sourceUrl}
                alt={post.featuredImage.node.altText || post.title}
                width={post.featuredImage.node.mediaDetails.width}
                height={post.featuredImage.node.mediaDetails.height}
                className="w-full h-48 object-cover"
              />
            )}

            <div className="p-6">
              <div className="flex gap-2 mb-3">
                {post.categories.nodes.map((cat) => (
                  <span key={cat.slug} className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
                    {cat.name}
                  </span>
                ))}
              </div>

              <h2 className="text-xl font-semibold mb-2">
                <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
                  {post.title}
                </Link>
              </h2>

              <div
                className="text-gray-600 text-sm mb-4"
                dangerouslySetInnerHTML={{ __html: post.excerpt }}
              />

              <div className="flex items-center text-sm text-gray-500">
                <span>{post.author.node.name}</span>
                <span className="mx-2">|</span>
                <time dateTime={post.date}>
                  {new Date(post.date).toLocaleDateString('de-DE')}
                </time>
              </div>
            </div>
          </article>
        ))}
      </div>
    </main>
  );
}

Einzelne Beitragsseite#

// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPostSlugs } from '@/lib/wordpress';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { Metadata } from 'next';

export const revalidate = 3600;

interface PageProps {
  params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
  const slugs = await getAllPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) return {};

  return {
    title: post.seo?.title || post.title,
    description: post.seo?.metaDesc || post.excerpt.replace(/<[^>]*>/g, ''),
    openGraph: {
      title: post.seo?.title || post.title,
      description: post.seo?.metaDesc || post.excerpt.replace(/<[^>]*>/g, ''),
      images: post.seo?.opengraphImage
        ? [{ url: post.seo.opengraphImage.sourceUrl }]
        : post.featuredImage
          ? [{ url: post.featuredImage.node.sourceUrl }]
          : [],
    },
  };
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) notFound();

  return (
    <article className="container mx-auto px-4 py-12 max-w-3xl">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>

        <div className="flex items-center text-gray-600 mb-6">
          <span>{post.author.node.name}</span>
          <span className="mx-2">|</span>
          <time dateTime={post.date}>
            {new Date(post.date).toLocaleDateString('de-DE', {
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })}
          </time>
        </div>

        {post.featuredImage && (
          <Image
            src={post.featuredImage.node.sourceUrl}
            alt={post.featuredImage.node.altText || post.title}
            width={post.featuredImage.node.mediaDetails.width}
            height={post.featuredImage.node.mediaDetails.height}
            className="w-full rounded-lg"
            priority
          />
        )}
      </header>

      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content }}
      />
    </article>
  );
}

Custom Post Types und Advanced Custom Fields (ACF)#

Einer der staerksten Aspekte von WordPress sind benutzerdefinierte Inhaltstypen in Kombination mit ACF-Feldern. Um diese ueber GraphQL bereitzustellen, benoetigen Sie das zusaetzliche Plugin WPGraphQL for ACF.

Registrierung eines Custom Post Type#

// functions.php
add_action('init', function () {
    register_post_type('portfolio', [
        'labels' => [
            'name'          => 'Portfolio',
            'singular_name' => 'Projekt',
        ],
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true, // Erforderlich fuer REST API
        'show_in_graphql' => true, // Erforderlich fuer WPGraphQL
        'graphql_single_name' => 'project',
        'graphql_plural_name' => 'projects',
        'supports'     => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
    ]);
});

ACF-Konfiguration mit GraphQL#

Nach der Installation von WPGraphQL for ACF sind ACF-Felder automatisch im GraphQL-Schema verfuegbar:

query GetProjects {
  projects(first: 12) {
    nodes {
      title
      slug
      excerpt
      featuredImage {
        node {
          sourceUrl
          altText
        }
      }
      projectFields {
        clientName
        projectUrl
        technologies
        completionDate
        testimonial
      }
    }
  }
}

ACF-Daten in Next.js abrufen#

// lib/wordpress.ts
export interface Project {
  title: string;
  slug: string;
  excerpt: string;
  featuredImage: {
    node: {
      sourceUrl: string;
      altText: string;
    };
  } | null;
  projectFields: {
    clientName: string;
    projectUrl: string;
    technologies: string[];
    completionDate: string;
    testimonial: string;
  };
}

export async function getProjects(): Promise<Project[]> {
  const query = gql`
    query GetProjects {
      projects(first: 50) {
        nodes {
          title
          slug
          excerpt
          featuredImage {
            node {
              sourceUrl
              altText
            }
          }
          projectFields {
            clientName
            projectUrl
            technologies
            completionDate
            testimonial
          }
        }
      }
    }
  `;

  const data = await client.request<{
    projects: { nodes: Project[] };
  }>(query);

  return data.projects.nodes;
}

Bildoptimierung mit Next.js Image#

WordPress speichert Bilder auf dem eigenen Server, aber Next.js kann sie mithilfe der Image-Komponente im laufenden Betrieb optimieren.

next.config.js Konfiguration#

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cms.ihredomain.de',
        pathname: '/wp-content/uploads/**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

module.exports = nextConfig;

WordPress-Bildkomponente#

// components/WordPressImage.tsx
import Image from 'next/image';

interface WordPressImageProps {
  src: string;
  alt: string;
  width: number;
  height: number;
  priority?: boolean;
  className?: string;
  sizes?: string;
}

export function WordPressImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className,
  sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
}: WordPressImageProps) {
  return (
    <Image
      src={src}
      alt={alt}
      width={width}
      height={height}
      priority={priority}
      className={className}
      sizes={sizes}
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
    />
  );
}

Incremental Static Regeneration (ISR)#

ISR ist eine Schluesselfunktion von Next.js, die es ermoeglicht, statische Seiten ohne vollstaendigen Rebuild zu aktualisieren. Es ist die ideale Loesung fuer WordPress-Inhalte, die sich regelmaessig aendern.

Zeitbasierte Revalidierung#

// app/blog/page.tsx
export const revalidate = 3600; // Revalidierung jede Stunde

On-Demand-Revalidierung#

Ein besserer Ansatz ist die durch einen Webhook von WordPress ausgeloeste Revalidierung:

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
  }

  const body = await request.json();
  const { post_type, slug } = body;

  try {
    // Bestimmten Beitrag revalidieren
    if (slug) {
      revalidatePath(`/blog/${slug}`);
    }

    // Blog-Auflistung revalidieren
    revalidatePath('/blog');

    // Nach Tag revalidieren
    revalidateTag('wordpress-posts');

    return NextResponse.json({
      revalidated: true,
      date: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      { message: 'Error revalidating' },
      { status: 500 }
    );
  }
}

WordPress-Webhook#

// functions.php
add_action('save_post', function ($post_id, $post) {
    if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
        return;
    }

    if ($post->post_status !== 'publish') {
        return;
    }

    $webhook_url = 'https://ihredomain.de/api/revalidate';

    wp_remote_post($webhook_url, [
        'headers' => [
            'Content-Type' => 'application/json',
            'x-revalidation-secret' => defined('REVALIDATION_SECRET')
                ? REVALIDATION_SECRET
                : '',
        ],
        'body' => json_encode([
            'post_type' => $post->post_type,
            'slug'      => $post->post_name,
            'post_id'   => $post_id,
        ]),
        'timeout' => 10,
    ]);
}, 10, 2);

Authentifizierung und Vorschaumodus#

Der Vorschaumodus ermoeglicht es Redakteuren, unveroeffentlichte Inhaltsaenderungen direkt im Next.js-Frontend zu sehen.

Vorschau-Endpunkt in Next.js#

// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret');
  const slug = request.nextUrl.searchParams.get('slug');
  const postType = request.nextUrl.searchParams.get('post_type') || 'post';

  if (secret !== process.env.WORDPRESS_PREVIEW_SECRET || !slug) {
    return new Response('Invalid token', { status: 401 });
  }

  const draft = await draftMode();
  draft.enable();

  const path = postType === 'page' ? `/${slug}` : `/blog/${slug}`;
  redirect(path);
}

// app/api/exit-preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET() {
  const draft = await draftMode();
  draft.disable();
  redirect('/');
}

Entwuerfe aus WordPress abrufen#

// lib/wordpress.ts
export async function getPreviewPost(
  id: number,
  authToken: string
): Promise<WPPost | null> {
  const authenticatedClient = new GraphQLClient(
    process.env.WORDPRESS_GRAPHQL_URL!,
    {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${authToken}`,
      },
    }
  );

  const query = gql`
    query GetPreviewPost($id: ID!) {
      post(id: $id, idType: DATABASE_ID, asPreview: true) {
        title
        content
        slug
        date
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
      }
    }
  `;

  const data = await authenticatedClient.request<{ post: WPPost | null }>(
    query,
    { id }
  );

  return data.post;
}

Vorschau in WordPress konfigurieren#

// functions.php
add_filter('preview_post_link', function ($link, $post) {
    $frontend_url = 'https://ihredomain.de';
    $secret = defined('PREVIEW_SECRET') ? PREVIEW_SECRET : '';
    $slug = $post->post_name ?: $post->ID;

    return sprintf(
        '%s/api/preview?secret=%s&slug=%s&post_type=%s',
        $frontend_url,
        $secret,
        $slug,
        $post->post_type
    );
}, 10, 2);

SEO im Headless WordPress#

SEO ist einer der Hauptgruende, warum Unternehmen WordPress waehlen. In einer Headless-Architektur gehen diese Faehigkeiten nicht verloren -- sie werden sogar verbessert.

Yoast SEO Integration#

Das Plugin WPGraphQL Yoast SEO stellt Yoast-SEO-Daten ueber GraphQL bereit:

query GetPostSEO($slug: ID!) {
  post(id: $slug, idType: SLUG) {
    seo {
      title
      metaDesc
      canonical
      opengraphTitle
      opengraphDescription
      opengraphImage {
        sourceUrl
        mediaDetails {
          width
          height
        }
      }
      twitterTitle
      twitterDescription
      twitterImage {
        sourceUrl
      }
      schema {
        raw
      }
    }
  }
}

Metadaten in Next.js generieren#

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post?.seo) return {};

  return {
    title: post.seo.title,
    description: post.seo.metaDesc,
    alternates: {
      canonical: post.seo.canonical || `/blog/${slug}`,
    },
    openGraph: {
      title: post.seo.opengraphTitle || post.seo.title,
      description: post.seo.opengraphDescription || post.seo.metaDesc,
      type: 'article',
      publishedTime: post.date,
      modifiedTime: post.modified,
      images: post.seo.opengraphImage
        ? [{
            url: post.seo.opengraphImage.sourceUrl,
            width: post.seo.opengraphImage.mediaDetails?.width,
            height: post.seo.opengraphImage.mediaDetails?.height,
          }]
        : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.seo.twitterTitle || post.seo.title,
      description: post.seo.twitterDescription || post.seo.metaDesc,
    },
  };
}

Sitemap generieren#

// app/sitemap.ts
import { getAllPosts } from '@/lib/wordpress';

export default async function sitemap() {
  const posts = await getAllPosts(1000);

  const blogEntries = posts.map((post) => ({
    url: `https://ihredomain.de/blog/${post.slug}`,
    lastModified: new Date(post.modified || post.date),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  return [
    {
      url: 'https://ihredomain.de',
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 1,
    },
    {
      url: 'https://ihredomain.de/blog',
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 0.9,
    },
    ...blogEntries,
  ];
}

Leistungsvergleich: Headless vs Traditionell#

Tests an einem realen Projekt mit 500 Beitraegen zeigen signifikante Unterschiede:

| Metrik | Traditionelles WordPress | Headless (Next.js + WP) | |--------|--------------------------|------------------------| | TTFB | 800-1500 ms | 50-150 ms | | LCP | 2,5-4,0 s | 0,8-1,5 s | | FID / INP | 150-300 ms | 30-80 ms | | CLS | 0,1-0,25 | 0,01-0,05 | | Lighthouse Score | 55-75 | 90-100 | | Build-Zeit (500 Beitraege) | N/A | ~3-5 Min. | | Hosting-Kosten | 20-50 EUR/Monat (gutes Hosting) | 0-20 EUR/Monat (Vercel free/pro) |

Hauptfaktoren fuer die Leistungsverbesserung:

  • Statische Seiten -- HTML vorab generiert, ueber CDN bereitgestellt
  • Bildoptimierung -- automatische Konvertierung in WebP/AVIF, Lazy Loading
  • Code Splitting -- nur notwendiges JavaScript pro Seite geladen
  • Edge Caching -- Inhalte vom naechstgelegenen CDN-Knoten bereitgestellt

Deployment-Strategien#

Zielarchitektur#

[Redakteur] --> [WordPress CMS] --> [Webhook]
                      |                  |
                      v                  v
               [WPGraphQL API]    [On-Demand ISR]
                      |                  |
                      v                  v
               [Next.js Build]   [Vercel Edge CDN]
                      |                  |
                      v                  v
               [Statische Seiten] <------+
                      |
                      v
               [Endbenutzer]

WordPress-Hosting#

Empfohlene Hosting-Optionen fuer WordPress als Headless CMS:

  1. WordPress.com Business -- verwaltetes Hosting, automatische Updates
  2. Kinsta -- hohe Leistung, PHP 8.x-Unterstuetzung, Staging-Umgebungen
  3. WP Engine -- dediziertes WordPress-Hosting, erweiterte Tools
  4. DigitalOcean Droplet -- volle Kontrolle, niedrige Kosten

Next.js auf Vercel bereitstellen#

# Vercel CLI installieren
npm install -g vercel

# Bereitstellen
vercel --prod

# Umgebungsvariablen konfigurieren
vercel env add WORDPRESS_GRAPHQL_URL
vercel env add REVALIDATION_SECRET
vercel env add WORDPRESS_PREVIEW_SECRET

Docker Compose fuer lokale Entwicklung#

# docker-compose.yml
services:
  wordpress:
    image: wordpress:6.4-php8.2-apache
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
      - ./wp-plugins:/var/www/html/wp-content/plugins
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: rootsecret
    volumes:
      - db_data:/var/lib/mysql

  nextjs:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      WORDPRESS_GRAPHQL_URL: http://wordpress:80/graphql
    depends_on:
      - wordpress

volumes:
  wp_data:
  db_data:

Fazit#

WordPress als Headless CMS in Kombination mit Next.js ist eine leistungsstarke Kombination, die eine vertraute Inhaltsverwaltungsoberflaeche mit einer modernen, hochperformanten Praesentationsschicht vereint. Die wichtigsten Vorteile sind:

  • Performance -- statische Seiten ueber CDN bereitgestellt, Ladezeiten unter einer Sekunde
  • SEO -- volle Kontrolle ueber Metadaten, serverseitiges Rendering, automatische Sitemaps
  • Flexibilitaet -- beliebige Frontend-Technologie, mehrkanalige Inhaltsdistribution
  • Sicherheit -- Admin-Panel vom oeffentlichen Auftritt getrennt
  • Skalierbarkeit -- Inhalte im CDN, Backend nur zur Bearbeitung

Bei MDS Software Solutions Group sind wir auf den Aufbau moderner Webloesungen basierend auf Headless-Architektur spezialisiert. Wir helfen Unternehmen bei der Migration vom traditionellen WordPress zur Headless-Architektur, entwerfen und implementieren Next.js-Frontends und integrieren CMS-Systeme mit mobilen Anwendungen und anderen Distributionskanaelen.

Benoetigen Sie eine leistungsstarke Website mit WordPress und Next.js? Kontaktieren Sie uns -- unsere Experten helfen Ihnen, die beste Architektur auszuwaehlen und eine auf Ihre Beduerfnisse zugeschnittene Loesung zu liefern.

Autor
MDS Software Solutions Group

Team von Programmierexperten, die sich auf moderne Webtechnologien spezialisiert haben.

WordPress als Headless CMS mit Next.js - Vollstaendiger Leitfaden | MDS Software Solutions Group | MDS Software Solutions Group