
Der Draft- & Preview-Modus im Headless CMS

Das Headless Vorschau-Dilemma
Stell dir folgendes Szenario vor: Du hast deine Headless-Architektur perfektioniert. Dein Next.js-Frontend lädt Daten über eine sichere API aus Contao. Durch Incremental Static Regeneration (ISR) und generateStaticParams aus Teil 8 liefert dein Server die Seiten in atemberaubenden 50 Millisekunden aus. Du bist stolz auf deine 100/100 Google PageSpeed-Wertung.
Dann übergibst du das System an das Redaktionsteam.
Eine Redakteurin legt einen neuen Blog-Artikel an, setzt den Haken bei "Veröffentlichen" nicht (weil der Text noch in Arbeit ist) und klickt auf den altbekannten Contao-Button "Artikel in der Vorschau ansehen".
Das Ergebnis? Eine 404-Fehlerseite in Next.js.
Warum? Weil unsere hochoptimierte Architektur genau das tut, was sie soll:
Unsere API liefert aus Sicherheitsgründen keine unveröffentlichten Inhalte.
Selbst wenn sie es täte, schaut Next.js zuerst in seinen aggressiven HTML-Cache und baut die Seite nicht neu zusammen.
In einer monolithischen Welt (wo Backend und Frontend auf demselben PHP-Server laufen) ist das simpel: Contao erkennt den eingeloggten Redakteur über ein Session-Cookie und rendert den Entwurf. In der Headless-Welt sind Backend und Frontend jedoch physisch getrennt. Dein Next.js-Server weiß nicht, dass die Person vor dem Bildschirm gerade im Contao-Backend eingeloggt ist.
Die Lösung: Der Next.js Draft Mode
Um dieses Dilemma zu lösen, hat Vercel den Draft Mode (früher Preview Mode) in den App Router integriert.
Das Konzept ist genial und sicher:
Wir erstellen in Next.js einen geheimen, speziellen API-Endpunkt (einen sogenannten Route Handler).
Contao ruft diesen Endpunkt mit einem geheimen Token und der gewünschten URL auf.
Next.js validiert das Token. Wenn es korrekt ist, setzt Next.js ein verschlüsseltes Cookie im Browser des Redakteurs.
Sobald dieses Cookie existiert, schaltet Next.js für diesen spezifischen Browser in den "Draft Mode". Der gesamte Static Cache (SSG/ISR) wird für diesen Nutzer umgangen. Jede Seite wird plötzlich "On-Demand" auf dem Server gerendert.
In unseren React Server Components lesen wir den Draft-Status aus und rufen die Contao-API mit einem speziellen Parameter auf, um die unveröffentlichten Entwurfs-Daten anzufordern.
Das Beste daran: Reguläre Website-Besucher surfen weiterhin auf den pfeilschnellen, gecachten statischen Seiten. Nur der Redakteur mit dem Cookie durchbricht die Cache-Mauer.

Den sicheren Route Handler (/api/draft) bauen
Unser erstes Ziel ist es, den "Türsteher" für unseren Draft-Modus zu programmieren. Wir benötigen eine URL in unserem Frontend, die das Contao-Backend aufrufen kann, um den Vorschau-Modus zu aktivieren.
Diese Route muss extrem sicher sein. Wenn ein Angreifer sie aufrufen könnte, könnte er unser Frontend zwingen, den Cache für ihn zu umgehen und im schlimmsten Fall unsere API mit ungecachten Anfragen zu überlasten (ein klassischer DDoS-Vektor). Daher schützen wir die Route mit einem Secret Token.
Praxis & Code: Der Draft Route Handler
Füge zunächst einen neuen geheimen Token zu deiner .env.local Datei im Next.js-Projekt hinzu:
# frontend/.env.local
PREVIEW_SECRET=dein_super_geheimes_langes_passwort_123!Erstelle nun einen Route Handler im Next.js App Router. Lege dazu folgende Datei an:
Datei: frontend/src/app/api/draft/route.ts
1import { draftMode } from 'next/headers';
2import { redirect } from 'next/navigation';
3
4export async function GET(request: Request) {
5 // 1. Die Query-Parameter aus der URL auslesen
6 const { searchParams } = new URL(request.url);
7 const secret = searchParams.get('secret');
8 const slug = searchParams.get('slug');
9
10 // 2. Sicherheits-Check: Stimmt das Token überein?
11 if (secret !== process.env.PREVIEW_SECRET) {
12 return new Response('Invalid token', { status: 401 });
13 }
14
15 // 3. Existiert ein Slug, zu dem wir weiterleiten können?
16 if (!slug) {
17 return new Response('Missing slug parameter', { status: 400 });
18 }
19
20 // 4. Draft Mode aktivieren (ACHTUNG: In Next.js 15 ist draftMode() asynchron!)
21 const draft = await draftMode();
22 draft.enable();
23
24 // 5. Dem Redakteur ein sicheres Cookie setzen und ihn zur Zielseite weiterleiten.
25 // Das Cookie sorgt dafür, dass ab sofort bei jedem Seitenaufruf der Cache ignoriert wird.
26 redirect(`/${slug}`);
27}Wie funktioniert das in der Praxis?
Wenn dein Contao-Backend später die URL https://dein-frontend.de/api/draft?secret=dein_super_geheimes_langes_passwort_123!&slug=unternehmen/team aufruft, validiert Next.js das Secret, setzt ein verschlüsseltes Cookie (das nur für die aktuelle Browser-Sitzung gültig ist) und leitet den Redakteur sofort auf /unternehmen/team um.

Den Fetcher für Draft-Daten anpassen
Jetzt, da unser Redakteur das Draft-Cookie besitzt, müssen wir unseren React-Komponenten beibringen, anders zu reagieren.
In Teil 8 haben wir unseren zentralen API-Fetcher (getContaoPageBySlug) gebaut. Dieser muss nun wissen: Befindet sich der aktuelle Nutzer im Draft Mode? Wenn ja, muss er die Caching-Strategie komplett ignorieren und der Contao-API mitteilen, dass auch unveröffentlichte Daten ausgeliefert werden dürfen.
Praxis & Code: Die Page Component und der Fetcher
Öffnen wir zuerst unseren Fetcher aus Teil 8 und erweitern ihn um einen isDraftMode Parameter.
Datei: frontend/src/lib/api/contao.ts (Aktualisiert)
1import { ContaoPageData } from '@/types/contao';
2
3const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
4const API_SECRET = process.env.CONTAO_API_KEY;
5
6export async function getContaoPageBySlug(slug: string, isDraftMode: boolean = false): Promise<ContaoPageData | null> {
7 if (!API_SECRET) throw new Error('API Key missing');
8
9 try {
10 // 1. Wenn wir im Draft-Mode sind, hängen wir einen Header an,
11 // der Contao signalisiert, auch unveröffentlichte Elemente zu laden.
12 const headers: HeadersInit = {
13 'Accept': 'application/json',
14 'Authorization': `Bearer ${API_SECRET}`,
15 };
16
17 if (isDraftMode) {
18 headers['X-Preview-Mode'] = 'true'; // Dein Contao-Backend muss diesen Header auswerten
19 }
20
21 const res = await fetch(`${API_BASE_URL}/api/v1/page/${slug}`, {
22 method: 'GET',
23 headers,
24 // 2. Entscheidend für den App Router:
25 // Wenn Draft-Mode aktiv ist, Cache rigoros abschalten ('no-store')
26 cache: isDraftMode ? 'no-store' : 'force-cache',
27 next: isDraftMode ? undefined : {
28 revalidate: 3600,
29 tags: ['contao-pages', `page-${slug}`]
30 },
31 });
32
33 if (!res.ok) return null;
34 return await res.json();
35
36 } catch (error) {
37 console.error(`Fehler bei /${slug}:`, error);
38 return null;
39 }
40}Jetzt passen wir unsere Catch-All Route an, um den Draft-Status aus Next.js auszulesen und an den Fetcher weiterzugeben.
Datei: frontend/src/app/[[...slug]]/page.tsx (Aktualisiert)
1import { notFound } from 'next/navigation';
2import { draftMode } from 'next/headers'; // Neu importiert
3import { getContaoPageBySlug } from '@/lib/api/contao';
4import BlockRenderer from '@/components/layout/BlockRenderer';
5
6interface PageProps {
7 params: Promise<{ slug?: string[] }>;
8}
9
10export default async function CatchAllPage({ params }: PageProps) {
11 // 1. Next.js 15: params und draftMode müssen awaited werden!
12 const resolvedParams = await params;
13 const currentSlug = resolvedParams.slug ? resolvedParams.slug.join('/') : 'index';
14
15 // 2. Draft Mode Status auslesen
16 const draft = await draftMode();
17 const isDraftMode = draft.isEnabled;
18
19 // 3. Den Status an unsere API-Schicht weiterleiten
20 const pageData = await getContaoPageBySlug(currentSlug, isDraftMode);
21
22 if (!pageData) notFound();
23
24 return (
25 <main className="container mx-auto py-12">
26 {/* Optionaler Hinweis für den Redakteur, dass er die Vorschau betrachtet */}
27 {isDraftMode && (
28 <div className="bg-amber-100 text-amber-900 px-4 py-2 text-sm text-center font-semibold mb-8 rounded">
29 ⚠️ Live-Vorschau aktiv (Unveröffentlichte Inhalte werden angezeigt)
30 </div>
31 )}
32
33 <h1 className="sr-only">{pageData.routing.title}</h1>
34
35 {pageData.content.articles.map((article) => (
36 <article key={article.id}>
37 <BlockRenderer elements={article.elements} />
38 </article>
39 ))}
40 </main>
41 );
42}Mit dieser Architektur ist die Trennung perfekt: 99,9 % deiner Besucher durchlaufen den hochperformanten Cache. Nur die Redakteure, die das Cookie aus Abschnitt 2 besitzen, laden die Seite "On-Demand" und sehen den Warnhinweis.

Die Contao-Backend Integration (Iframe Preview)
Der Next.js-Server ist bereit. Jetzt müssen wir Contao beibringen, wie es unseren neuen Route Handler aufruft.
In einem monolithischen Setup öffnet Contao die Seite preview.php in einem Iframe innerhalb des Backends, sobald du auf "Artikel in der Vorschau ansehen" klickst. In unserem Headless-Setup wollen wir, dass Contao stattdessen unser Next.js Frontend im Iframe lädt.
Konfiguration des Headless Bundles
Wenn du ein professionelles Headless-Bundle in Contao nutzt (wie z.B. das formatz/contao-headless-cms-bundle oder eine eigene Implementierung), bieten diese in der Regel eine Konfigurationsmöglichkeit in der config.yaml, um die Preview-URL umzuschreiben.
Öffne deine Symfony-Konfiguration im Contao Backend-Projekt:
Datei: workspace/contao-backend/config/config.yaml
(Hinweis: Die exakte Syntax hängt von deinem verwendeten Headless-Bundle ab. Hier ein allgemeingültiges Beispiel.)
1# Beispielhafte Konfiguration für das Überschreiben der Preview-URL
2contao_headless:
3 preview:
4 # Die Basis-URL deines Next.js Frontends
5 base_url: "[https://dein-frontend-domain.de](https://dein-frontend-domain.de)"
6
7 # Der Pfad zu unserem Route Handler aus Abschnitt 2
8 # Contao ersetzt Platzhalter wie {page_alias} automatisch
9 preview_path: "/api/draft?secret=dein_super_geheimes_langes_passwort_123!&slug={page_alias}"Alternative ohne spezielles Bundle:
Wenn du Contao komplett nativ nutzt und die Headless-Architektur selbst gebaut hast, kannst du den URL-Aufbau der Vorschau über einen Symfony Event Listener oder Contao Hook anpassen (z. B. den contao.preview_url_create Event, der seit Contao 4.13/5.0 existiert).
Den Backend-Modus meistern
Sobald die URL in Contao konfiguriert ist, ergibt sich ein magischer Workflow für deine Redakteure:
Der Redakteur klickt in der Contao-Seitenstruktur auf das Auge-Symbol (Vorschau).
Contao lädt das Iframe. Anstatt der internen
preview.phpruft der Iframe die externe Next.js-URL auf:https://.../api/draft?secret=...&slug=team.Next.js validiert das Secret im Hintergrund, setzt das Cookie im Browser des Redakteurs und leitet innerhalb des Iframes auf die finale React-Seite
/teamweiter.Der Redakteur sieht das Next.js Frontend mitsamt allen unveröffentlichten Inhalten direkt eingebettet in seinem Contao-Arbeitsbereich!

Fazit – Das Beste aus zwei Welten
Die Implementierung des Draft-Modus ist oft der Moment in einem Headless-Projekt, in dem das Redaktionsteam endlich aufatmet. Die Sorge, durch die moderne Technologie die Kontrolle über das Endprodukt zu verlieren, löst sich in Luft auf.
Wir haben mit minimalem Code-Aufwand eine Enterprise-Lösung geschaffen:
Sicherheit: Der Draft-Modus ist durch einen geheimen Token geschützt. Ohne dieses Token gibt es keinen Cache-Bypass.
Performance: 100 % deiner regulären Website-Besucher profitieren weiterhin von der maximalen Geschwindigkeit der Next.js Server Components und dem statischen Caching.
Redakteurs-Erlebnis (UX): Der Workflow für das Marketing-Team bleibt identisch. Sie arbeiten im vertrauten Contao-Backend, klicken auf Vorschau und sehen das fertige Next.js-Design.
Du hast in den Phasen 1 bis 3 nun ein vollständiges, sicheres und ausgabefähiges Headless-System aufgebaut. Dein Backend liefert JSON, dein Frontend rendert React-Komponenten, verarbeitet Formulare und bietet eine Live-Vorschau.
Contao Headless Masterclass: High-Performance mit Next.js Pillar
Contao Headless Architektur: Das Konzept im Detail verstehen
Projektstruktur & Entwicklungsumgebung für Contao und Next.js
Contao Headless Basis-Setup: Installation & Bildkonfiguration
Contao Headless Datenstruktur & Redakteurs-Erlebnis
Die API From Scratch entwickeln: Symfony Routing in Contao 5
Contao Headless API-Sicherheit & Formularverarbeitung
Next.js Setup & Grundstruktur für dein Headless CMS
Dynamisches Routing & Navigation in Next.js
Module & Formulare im Frontend rendern
Der Draft- & Preview-Modus im Headless CMS
Häufig gestellte Fragen (FAQ)
Das Draft-Cookie ist ein Session-Cookie. Es wird automatisch gelöscht, wenn der Redakteur den Browser schließt. Alternativ kannst du im Frontend (z.B. im Warn-Banner) einen Button einbauen, der einen weiteren Route Handler (z.B. /api/disable-draft) aufruft. Dort führst du in Next.js einfach (await draftMode()).disable() aus.
Ja, solange der Redakteur das korrekte Secret kennt! Du könntest in Contao einen Button einbauen, der einen QR-Code generiert, welcher die URL /api/draft?secret=...&slug=... enthält. Scannt der Redakteur diesen mit dem Handy, aktiviert sich der Draft-Modus auch auf dem mobilen Gerät.
Jein. Wenn dein Frontend (z.B. www.meine-seite.de) und dein Contao-Backend (z.B. admin.meine-seite.de) auf unterschiedlichen Subdomains liegen, weigern sich einige moderne Browser (aus Sicherheitsgründen bezüglich Third-Party-Cookies), das Next.js-Cookie innerhalb des Iframes zu speichern. Die eleganteste Lösung: Nutze einen Reverse Proxy (wie Nginx oder aaPanel), um Backend und Frontend auf dieselbe Hauptdomain zu routen (z.B. Frontend über / und Contao-Backend über /contao).
Dein nächster Schritt: Phase 4 – Performance-Tuning & Caching
Dein Headless-Setup steht, aber es ist noch nicht "Enterprise-Ready". In der Architektur-Welt unterscheidet man zwischen einer Seite, die funktioniert, und einer Seite, die auch bei 10.000 gleichzeitigen Besuchern nicht in die Knie geht.
Mit dem Beginn von Phase 4: Performance-Tuning und SEO-Meisterschaft bringen wir unser System auf das nächste Level.
Im kommenden Teil 11: High-Performance & Caching-Strategien werden wir das Next.js Caching-Modell tiefgreifend meistern. Du wirst lernen:
Die 4 Ebenen des Next.js Caches: Request Memoization, Data Cache, Full Route Cache und Router Cache verstehen.
On-Demand Revalidation: Wie wir Contao beibringen, nach dem Speichern eines Artikels einen Webhook an Next.js zu senden, um den Cache für diese eine spezifische Seite in Millisekunden zu leeren (ohne einen vollständigen Re-Build zu erzwingen).
Tag-basiertes Caching: Wie wir hunderte Seiten gleichzeitig invalidieren können, wenn z.B. die globale Navigation geändert wird.
Jetzt starten: Teil 11 – High-Performance & Caching-Strategien meistern

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


