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.
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:
- Statische Shell erscheint sofort (< 100ms)
- Cached Content lädt (< 200ms)
- 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:
| Kriterium | REST API | GraphQL |
|---|---|---|
| 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:
- Build time: Alle Posts werden pre-rendered
- Request: Static HTML served from CDN
- Background: Nach 1h revalidiert Next.js den Content
- 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:
- Redakteur updated Post in WordPress
- WordPress sendet Webhook zu Next.js
- Next.js invalidiert Cache-Tag
- Nächster Request re-rendered die Page
- 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.
Bereit für Headless WordPress?
Kontaktieren Sie uns für eine kostenlose Erstberatung
Jetzt Kontakt aufnehmen