Zurück zum Blog
Next.js 16WordPressArchitekturTechnical Deep-DiveCache Components

Next.js 16 + WordPress: Unsere komplette Architektur erklärt

Wie wir Next.js 16 mit WordPress kombinieren: Von Cache Components über Partial Prerendering bis zur Deployment-Strategie. Code-Beispiele, Patterns und Lessons Learned.

1. Februar 2025
13 min Lesezeit
Von WP Headless Team

Letzte Woche haben wir erklärt, warum wir auf Headless WordPress setzen. Diese Woche zeigen wir wie: Unsere komplette Architektur mit Next.js 16 und WordPress.

Dies ist kein oberflächlicher Überblick. Wir gehen tief: Cache Components, Partial Prerendering, Server vs. Client Components, Data Fetching Patterns, und Deployment-Strategie.

Code-Beispiele stammen aus echten Projekten. Alle Patterns sind produktionserprobt.

Architektur-Überblick

Die zwei Welten

┌──────────────────────────────────────────────────────────────────┐
│                         FRONTEND (Next.js 16)                    │
│                                                                  │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────────┐    │
│  │ Static Pages   │  │ Dynamic Pages  │  │  API Routes      │    │
│  │ (SSG with ISR) │  │ (SSR/Streaming)│  │  (Server Funcs)  │    │
│  └────────────────┘  └────────────────┘  └──────────────────┘    │
│                                                                  │
│  Deployment: Vercel Edge Network (Global CDN)                    │
└──────────────────────────────────────────────────────────────────┘
                              ↕ HTTP/REST API
┌────────────────────────────────────────────────────────────────────┐
│                      BACKEND (WordPress)                           │
│                                                                    │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────────┐      │
│  │ WordPress Core │  │ ACF Pro        │  │  WPGraphQL       │      │
│  │ (CMS)          │  │ (Custom Fields)│  │  (Optional)      │      │
│  └────────────────┘  └────────────────┘  └──────────────────┘      │
│                                                                    │
│  Deployment: Managed WordPress Hosting (Private, nicht öffentlich) │
└────────────────────────────────────────────────────────────────────┘

Warum diese Architektur?

Frontend (Next.js 16):

  • Maximale Performance (Static Generation + Edge CDN)
  • Moderne Developer Experience (React 19, TypeScript)
  • Partial Prerendering (statische Shell + dynamische Inhalte)
  • Cache Components für granulare Caching-Kontrolle

Backend (WordPress):

  • Bewährte Content-Management-Oberfläche
  • Keine Redakteur-Umschulung nötig
  • Mature Plugin-Ecosystem (ACF, Yoast, WooCommerce)
  • Nicht öffentlich zugänglich (Security)

Next.js 16: Die neuen Features

Next.js 16 bringt drei Game-Changer für Headless WordPress:

1. Cache Components ('use cache')

Problem mit Next.js 15 und früher: Caching war auf Route-Ebene. Entweder die ganze Page ist cached oder nichts.

Next.js 16 Lösung: Granulare Component-Level Caching mit 'use cache' Directive.

// src/components/blog/BlogPost.tsx
export async function BlogPost({ slug }: { slug: string }) {
  'use cache' // Diese Component wird gecached
  cacheLife('weekly') // Für 1 Woche
  cacheTag('blog-posts', `post-${slug}`) // Für Invalidierung

  const post = await fetchPostFromWordPress(slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Vorteile:

  • Granular: Nur statische Teile cachen, dynamische Teile live
  • Invalidierung: Spezifische Cache-Tags für gezieltes Purging
  • Performance: Weniger API-Calls zu WordPress

Beispiel: E-Commerce Produktseite

export default async function ProductPage({ params }) {
  return (
    <>
      {/* Static: Produktinfo (cached 1 Woche) */}
      <ProductInfo productId={params.id} />

      {/* Dynamic: Verfügbarkeit (live vom WooCommerce API) */}
      <ProductStock productId={params.id} />

      {/* Static: Reviews (cached 1 Tag) */}
      <ProductReviews productId={params.id} />
    </>
  )
}

async function ProductInfo({ productId }) {
  'use cache'
  cacheLife('weekly')
  // ... fetch von WordPress REST API
}

// ProductStock NICHT cached - immer live
async function ProductStock({ productId }) {
  await connection() // Signal für dynamic rendering
  // ... fetch live von WooCommerce API
}

2. Partial Prerendering (PPR)

Konzept: Statische Shell wird pre-rendered, dynamische Teile werden gestreamt.

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }) {
  return (
    <>
      {/* Static: Pre-rendered at build time */}
      <Navigation />
      <BlogPost slug={params.slug} /> {/* Cached Component */}

      {/* Dynamic: Streamed at request time */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={params.slug} /> {/* Live data */}
      </Suspense>

      {/* Static: Pre-rendered */}
      <Footer />
    </>
  )
}

User Experience:

  1. Statische Shell erscheint sofort (< 100ms)
  2. Cached Content lädt (< 200ms)
  3. Dynamischer Content streamt rein (< 500ms)

Resultat: Perceived Performance ist exzellent, auch wenn teile dynamisch sind.

3. React Compiler (Automatische Memoization)

Früher (React 18):

function ProductCard({ product }) {
  // Manuell memoizen für Performance
  const formattedPrice = useMemo(
    () => formatPrice(product.price),
    [product.price]
  )

  const handleClick = useCallback(() => {
    trackEvent('product_click', product.id)
  }, [product.id])

  return <div onClick={handleClick}>{formattedPrice}</div>
}

Mit React Compiler (React 19 + Next.js 16):

function ProductCard({ product }) {
  // Compiler memoized automatisch
  const formattedPrice = formatPrice(product.price)

  const handleClick = () => {
    trackEvent('product_click', product.id)
  }

  return <div onClick={handleClick}>{formattedPrice}</div>
}

Vorteil: Weniger Boilerplate, bessere Performance out-of-the-box.

WordPress Backend: Setup & Configuration

1. WordPress als Private API

Kritisch: WordPress sollte NICHT öffentlich erreichbar sein.

# Nginx Config: Block alle public requests
location / {
    # Erlaube nur von Vercel IPs (Frontend)
    allow 76.76.21.0/24;  # Vercel IP Range
    deny all;

    # Oder: IP-Whitelist für dev team
    allow 192.168.1.0/24; # Office IP
    deny all;
}

# Erlaube nur /wp-json/ endpoint für REST API
location /wp-json/ {
    allow all;
}

Alternative: Vercel Functions als Proxy

// app/api/wordpress/route.ts
export async function GET(request: Request) {
  const url = new URL(request.url)
  const endpoint = url.searchParams.get('endpoint')

  // Forward request to private WordPress
  const wpUrl = process.env.WORDPRESS_API_URL // Private URL
  const response = await fetch(`${wpUrl}/wp-json/wp/v2/${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${process.env.WP_API_TOKEN}`
    }
  })

  return response
}

2. REST API vs. GraphQL

Wir nutzen primär REST API. Hier ist warum:

REST API Vorteile:

  • Built-in (keine Plugins nötig)
  • Weniger Overhead
  • Einfachere Debugging
  • Caching auf HTTP-Level möglich
// Beispiel: Fetch Posts via REST API
async function fetchPosts() {
  'use cache'
  cacheLife('weekly')

  const response = await fetch(
    `${process.env.WP_API_URL}/wp-json/wp/v2/posts?_embed&per_page=10`
  )

  const posts = await response.json()
  return posts
}

GraphQL mit WPGraphQL:

Wir verwenden GraphQL nur für:

  • Komplexe Queries (verschachtelte Relationen)
  • Over-fetching Probleme (REST API gibt zu viel zurück)
  • Frontend-Developers (die GraphQL bevorzugen)
# WPGraphQL Query
query GetPost($slug: ID!) {
  post(id: $slug, idType: SLUG) {
    title
    excerpt
    content
    featuredImage {
      node {
        sourceUrl
        altText
      }
    }
    categories {
      nodes {
        name
        slug
      }
    }
  }
}

Entscheidungskriterien:

KriteriumREST APIGraphQL
Einfachheit✅ Sehr einfach⚠️ Zusätzliche Komplexität
Performance✅ HTTP Caching⚠️ Schwieriger zu cachen
Over-fetching❌ Kann passieren✅ Exakte Fields
Verschachtelte Daten⚠️ Multiple Requests✅ Single Request
Debugging✅ Browser DevTools⚠️ GraphQL Tools nötig

Unsere Empfehlung: Start mit REST API. Migrate zu GraphQL nur wenn nötig.

3. Custom Fields mit ACF Pro

Advanced Custom Fields (ACF) ist unser Go-To für flexible Content-Strukturen.

// WordPress: ACF Field Group für "Service" Post Type
[
  'title' => 'Service Details',
  'fields' => [
    [
      'key' => 'service_icon',
      'name' => 'icon',
      'type' => 'image',
    ],
    [
      'key' => 'service_features',
      'name' => 'features',
      'type' => 'repeater',
      'sub_fields' => [
        ['name' => 'feature_title', 'type' => 'text'],
        ['name' => 'feature_description', 'type' => 'textarea'],
      ]
    ]
  ]
]
// Next.js: Fetch ACF Fields via REST API
async function fetchService(slug: string) {
  'use cache'
  cacheLife('weekly')

  const response = await fetch(
    `${process.env.WP_API_URL}/wp-json/wp/v2/service?slug=${slug}`
  )
  const [service] = await response.json()

  // ACF fields are in "acf" key
  return {
    title: service.title.rendered,
    icon: service.acf.icon,
    features: service.acf.features,
  }
}

Tipp: ACF to REST API Plugin aktivieren für automatische REST API Exposition.

Data Fetching Patterns

Pattern 1: Static Pages mit ISR

Use Case: Blog Posts, Dokumentation, Marketing Pages

// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ISR: Revalidate every hour

export async function generateStaticParams() {
  const posts = await fetchAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

export default async function BlogPostPage({ params }) {
  const post = await fetchPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

async function fetchPost(slug: string) {
  'use cache'
  cacheLife('weekly')
  cacheTag('blog-posts', `post-${slug}`)

  // Fetch from WordPress
  const response = await fetch(
    `${process.env.WP_API_URL}/wp-json/wp/v2/posts?slug=${slug}`
  )
  return response.json()
}

Flow:

  1. Build time: Alle Posts werden pre-rendered
  2. Request: Static HTML served from CDN
  3. Background: Nach 1h revalidiert Next.js den Content
  4. Result: Immer schnell, Content max. 1h alt

Pattern 2: Dynamic Pages mit On-Demand Revalidation

Use Case: E-Commerce, Dashboards, User-Generated Content

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

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret')
  const tag = request.nextUrl.searchParams.get('tag')

  // Verify webhook secret
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }

  // Revalidate cache tag
  revalidateTag(tag)

  return Response.json({ revalidated: true })
}

WordPress Webhook:

// WordPress: Trigger revalidation on post update
add_action('save_post', function($post_id) {
  $post = get_post($post_id);

  // Call Next.js revalidation API
  wp_remote_post('https://your-nextjs-app.vercel.app/api/revalidate', [
    'body' => [
      'secret' => getenv('REVALIDATE_SECRET'),
      'tag' => 'post-' . $post->post_name,
    ]
  ]);
});

Flow:

  1. Redakteur updated Post in WordPress
  2. WordPress sendet Webhook zu Next.js
  3. Next.js invalidiert Cache-Tag
  4. Nächster Request re-rendered die Page
  5. Result: Content ist sofort aktuell

Pattern 3: Hybrid (Static Shell + Dynamic Content)

Use Case: Produktseiten mit live Inventory

// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  return (
    <div>
      {/* Static: Produktinfo (cached) */}
      <ProductInfo productId={params.id} />

      {/* Dynamic: Stock (live) */}
      <Suspense fallback={<StockSkeleton />}>
        <ProductStock productId={params.id} />
      </Suspense>

      {/* Static: Reviews (cached) */}
      <ProductReviews productId={params.id} />
    </div>
  )
}

// Cached component
async function ProductInfo({ productId }) {
  'use cache'
  cacheLife('weekly')

  const product = await fetchProductInfo(productId)
  return <div>{/* ... */}</div>
}

// Live component (NOT cached)
async function ProductStock({ productId }) {
  await connection() // Signal: dynamic

  const stock = await fetchLiveStock(productId)
  return <div>Verfügbar: {stock} Stück</div>
}

Benefit: Best of both worlds - schnelle statische Teile + aktuelle dynamische Daten.

Deployment & Hosting

Frontend: Vercel

Warum Vercel:

  • Native Next.js Support (von den Next.js Creators)
  • Edge Network (Global CDN)
  • Automatische Preview Deployments
  • Zero-Config (meist)
# Deployment
vercel --prod

# Environment Variables (über Vercel Dashboard)
WORDPRESS_API_URL=https://wp-backend.your-domain.com
WP_API_TOKEN=***
REVALIDATE_SECRET=***

Vercel Config (vercel.json):

{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "regions": ["fra1", "iad1"],
  "crons": [
    {
      "path": "/api/cron/cache-warm",
      "schedule": "0 2 * * *"
    }
  ]
}

Backend: Managed WordPress Hosting

Anforderungen:

  • SSH Access (für WP-CLI)
  • Modern PHP (8.1+)
  • Gute Performance (nicht kritisch, da nicht öffentlich)
  • Sicherheit (Private Network oder IP-Whitelist)

Empfohlene Hoster (Schweiz):

  • Cyon: Schweizer Hosting, guter Support
  • Hostpoint: Managed WordPress, SSD
  • WP Engine: Premium (international, aber DACH-Präsenz)

Nicht empfohlen:

  • Shared Hosting ohne SSH
  • Hosting ohne HTTP/2
  • Alte PHP-Versionen (< 7.4)

Security Best Practices

1. WordPress ist nicht öffentlich

# Option 1: IP-Whitelist
location / {
  allow 76.76.21.0/24;  # Vercel
  allow 192.168.1.100;  # Office
  deny all;
}

# Option 2: Vercel Proxy
# Alle requests gehen durch Vercel Functions
# WordPress ist nur per private URL erreichbar

2. API Authentication

// Next.js: Authenticated requests
async function fetchFromWordPress(endpoint: string) {
  const response = await fetch(
    `${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/${endpoint}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.WP_API_TOKEN}`,
      }
    }
  )
  return response.json()
}
// WordPress: JWT Authentication Plugin
// wp-config.php
define('JWT_AUTH_SECRET_KEY', getenv('JWT_SECRET'));
define('JWT_AUTH_CORS_ENABLE', true);

3. Rate Limiting

// app/api/wordpress/route.ts
import { rateLimit } from '@/lib/rate-limit'

const limiter = rateLimit({
  interval: 60 * 1000, // 1 minute
  uniqueTokenPerInterval: 500,
})

export async function GET(request: Request) {
  try {
    await limiter.check(10, 'WORDPRESS_API') // 10 requests per minute
  } catch {
    return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
  }

  // ... forward to WordPress
}

Performance Optimization

1. Image Optimization

// Next.js Image Component mit WordPress Media
import Image from 'next/image'

export function FeaturedImage({ post }) {
  const image = post._embedded?.['wp:featuredmedia']?.[0]

  if (!image) return null

  return (
    <Image
      src={image.source_url}
      alt={image.alt_text || post.title.rendered}
      width={1200}
      height={630}
      priority={true} // LCP optimization
    />
  )
}

WordPress Plugin: Install "Enable Media Replace" für WebP-Support.

2. Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

export default function RootLayout({ children }) {
  return (
    <html lang="de" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

Result: Fonts werden über Next.js optimiert (self-hosted, preloaded).

3. Bundle Optimization

// next.config.ts
const nextConfig = {
  experimental: {
    optimizePackageImports: ['@heroicons/react'],
  },

  // Webpack config für weitere Optimierungen
  webpack(config, { isServer }) {
    if (!isServer) {
      // Client bundle optimization
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          default: false,
          vendors: false,
          framework: {
            name: 'framework',
            chunks: 'all',
            test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types|use-subscription)[\\/]/,
            priority: 40,
            enforce: true,
          },
        },
      }
    }
    return config
  },
}

Common Pitfalls & Solutions

Pitfall 1: Over-fetching von WordPress

Problem:

// ❌ Bad: Fetch all posts with ALL fields
const posts = await fetch('/wp-json/wp/v2/posts?per_page=100')

Solution:

// ✅ Good: Nur nötige Fields
const posts = await fetch(
  '/wp-json/wp/v2/posts?' +
  'per_page=10&' +
  '_fields=id,title,excerpt,slug,date'
)

Pitfall 2: Missing Error Handling

Problem:

// ❌ Bad: Keine error handling
const post = await fetch(`/wp-json/wp/v2/posts/${id}`).then(r => r.json())

Solution:

// ✅ Good: Mit error handling
async function fetchPost(id: string) {
  try {
    const response = await fetch(`/wp-json/wp/v2/posts/${id}`)

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }

    return await response.json()
  } catch (error) {
    console.error('Failed to fetch post:', error)
    return null // Graceful degradation
  }
}

Pitfall 3: Hardcoded URLs

Problem:

// ❌ Bad: Hardcoded
const API_URL = 'https://wp-backend.example.com'

Solution:

// ✅ Good: Environment variables
const API_URL = process.env.WORDPRESS_API_URL

if (!API_URL) {
  throw new Error('WORDPRESS_API_URL is not defined')
}

Monitoring & Debugging

1. WordPress Query Monitor

Install "Query Monitor" Plugin für:

  • API Response Times
  • Database Queries
  • PHP Errors
  • Cache Hit Rates

2. Vercel Analytics

// app/layout.tsx
import { Analytics } from '@vercel/analytics/next'
import { SpeedInsights } from '@vercel/speed-insights/next'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Metrics:

  • Real User Monitoring (RUM)
  • Core Web Vitals
  • API Response Times

3. Error Logging

// lib/logger.ts
export function logError(error: Error, context: Record<string, any>) {
  if (process.env.NODE_ENV === 'production') {
    // Send to error tracking (z.B. Sentry)
    console.error('[ERROR]', {
      message: error.message,
      stack: error.stack,
      ...context,
    })
  } else {
    console.error(error)
  }
}

Lessons Learned

1. Start Simple

Wir haben mit REST API angefangen. GraphQL erst später hinzugefügt (und nur wo nötig).

Recommendation: Nicht zu früh over-engineer. REST API reicht für 90% der Use Cases.

2. Cache Granularly

Mit Next.js 16 Cache Components können wir jetzt granular cachen. Game changer.

Recommendation: Nutze 'use cache' auf Component-Ebene statt auf Route-Ebene.

3. Monitor Real Performance

Lighthouse Scores sind nice-to-have. Real User Monitoring (RUM) ist kritisch.

Recommendation: Vercel Analytics oder Google Analytics 4 (mit Web Vitals) einbauen.

Nächste Schritte

Nächste Woche: "68% schneller – So haben wir diese Website gemessen"

  • Vollständige Performance-Methodik
  • Tools, Tests, Raw Data
  • Vergleich: Traditional WordPress vs. Headless

Folgende Woche: "Launch Partner Programm: Details & Anmeldung"

  • Package-Details (Pilot, Standard, Premium)
  • Anwendungsprozess
  • FAQ und Erwartungen

Fragen zu dieser Architektur? Diskutieren Sie mit uns auf GitHub oder LinkedIn.

Für Entwickler: Wir planen ein Open-Source Next.js + WordPress Starter Template. Folgen Sie unserem GitHub für Updates.

W

WP Headless Team

Teil des WP Headless Teams - WordPress Experten auf der Reise zur Headless-Architektur.

Bereit für Headless WordPress?

Kontaktieren Sie uns für eine kostenlose Erstberatung

Jetzt Kontakt aufnehmen