
Module & Formulare im Frontend rendern

Der Content-Dispatcher (Das Factory-Pattern)
Wir haben in Teil 8 unserer Masterclass eine gewaltige Hürde genommen: Unser Next.js-Routing ist dynamisch, versteht jede Contao-URL und lädt über unseren typsicheren Fetcher die passenden Daten. Wenn wir die Seite jedoch aktuell im Browser aufrufen, sehen wir nur einen unschönen Platzhalter-Text.
Das Problem: Unsere Contao-API liefert ein Array von Inhaltselementen in reiner JSON-Form.
Ein typischer Payload für eine Startseite sieht in etwa so aus:
1{
2 "elements": [
3 { "id": 1, "type": "headline", "text": "Willkommen", "level": "h1" },
4 { "id": 2, "type": "text", "html": "<p>Das ist ein Text.</p>" },
5 { "id": 3, "type": "image", "src": "/files/hero.jpg", "alt": "Strand" }
6 ]
7}In der klassischen Contao-Welt würde das System nun über das Dateisystem iterieren, die passenden PHP-Templates (ce_headline.html5, ce_text.html5) laden und das HTML zusammenbauen. Im Next.js App Router müssen wir diesen Mechanismus mit React-Komponenten exakt nachbauen.
Hierfür nutzen wir ein bewährtes Software-Architekturmuster: Das Factory-Pattern (oft auch als Dispatcher oder Component Mapper bezeichnet).
Praxis & Code: Den Block-Renderer bauen
Wir schreiben eine smarte Server Component, die durch das Array der Inhaltselemente iteriert. Sie liest den Wert type aus und entscheidet dynamisch, welche unserer React-Komponenten geladen und mit den Daten gefüttert wird.
Schritt 1: Die Basis-Komponenten erstellen
Lass uns zunächst zwei sehr einfache React-Komponenten in unserem blocks-Ordner (den wir in Teil 7 angelegt haben) erstellen, die später das Rendering übernehmen.
Datei: frontend/src/components/blocks/HeadlineBlock.tsx
1import { ContaoContentElement } from '@/types/contao';
2
3export default function HeadlineBlock({ data }: { data: ContaoContentElement }) {
4 // Dynamisches Tag-Rendering (h1, h2, h3...)
5 const Tag = (data.level || 'h2') as keyof JSX.IntrinsicElements;
6
7 return (
8 <Tag className="text-3xl md:text-5xl font-bold text-brand-dark mb-6">
9 {data.text}
10 </Tag>
11 );
12}Datei: frontend/src/components/blocks/TextBlock.tsx
1import { ContaoContentElement } from '@/types/contao';
2
3export default function TextBlock({ data }: { data: ContaoContentElement }) {
4 return (
5 // Wir rendern HTML (Achtung: Dazu im nächsten Abschnitt mehr!)
6 <div
7 className="prose prose-slate max-w-none mb-8"
8 dangerouslySetInnerHTML={{ __html: data.html || '' }}
9 />
10 );
11}Schritt 2: Der Dispatcher (Component Mapper)
Nun bauen wir das Herzstück: Die Engine, die alles orchestriert.
Datei: frontend/src/components/layout/BlockRenderer.tsx
1import { ContaoContentElement } from '@/types/contao';
2import HeadlineBlock from '@/components/blocks/HeadlineBlock';
3import TextBlock from '@/components/blocks/TextBlock';
4
5// Ein Dictionary, das Contao-Typen auf React-Komponenten mappt
6const componentMap: Record<string, React.ComponentType<{ data: ContaoContentElement }>> = {
7 'headline': HeadlineBlock,
8 'text': TextBlock,
9 // Hier fügst du später z.B. 'accordion': AccordionBlock, 'gallery': GalleryBlock hinzu
10};
11
12export default function BlockRenderer({ elements }: { elements: ContaoContentElement[] }) {
13 if (!elements || elements.length === 0) return null;
14
15 return (
16 <>
17 {elements.map((element) => {
18 // Die passende Komponente anhand des Contao 'type' Felds finden
19 const Component = componentMap[element.type];
20
21 // Wenn der Redakteur ein Element nutzt, für das wir noch keine React-Komponente gebaut haben
22 if (!Component) {
23 // In der lokalen Entwicklung zeigen wir eine Warnung
24 if (process.env.NODE_ENV === 'development') {
25 return (
26 <div key={element.id} className="p-4 border-2 border-dashed border-red-400 bg-red-50 text-red-700 my-4 rounded">
27 ⚠️ Fehlende Komponente für den Typ: <strong>{element.type}</strong>
28 </div>
29 );
30 }
31 // In Produktion ignorieren wir unbekannte Blöcke stillschweigend, um das Layout nicht zu zerstören
32 return null;
33 }
34
35 // Die Komponente rendern und die CMS-Daten als Prop übergeben
36 return <Component key={element.id} data={element} />;
37 })}
38 </>
39 );
40}Schritt 3: Integration in unsere Catch-All Route
Gehe nun zurück zu der in Teil 8 erstellten Seite src/app/[[...slug]]/page.tsx und tausche den Platzhalter durch unseren neuen BlockRenderer aus:
1// Datei: frontend/src/app/[[...slug]]/page.tsx (Auszug)
2import BlockRenderer from '@/components/layout/BlockRenderer';
3
4// ... (vorheriger Fetching-Code)
5
6 return (
7 <main className="container mx-auto py-12">
8 <h1 className="sr-only">{pageData.routing.title}</h1>
9
10 {/* Das dynamische Rendering des Contao-Contents */}
11 {pageData.content.articles.map((article) => (
12 <article key={article.id}>
13 {/* Unser neuer Dispatcher übernimmt ab hier! */}
14 <BlockRenderer elements={article.elements} />
15 </article>
16 ))}
17 </main>
18 );Der geniale Vorteil dieses Setups: Dein Haupt-Router (page.tsx) muss ab sofort nie wieder angefasst werden. Wenn dein Kunde nächste Woche einen Slider auf der Website benötigt, baut der Backend-Entwickler das Element in Contao, du erstellst im Frontend einen SliderBlock.tsx, trägst ihn in die componentMap ein – und fertig. Die Architektur skaliert grenzenlos und bleibt gleichzeitig absolut aufgeräumt.

Sauberes HTML & Rich-Text Sanitization im React-Kontext
In unserem ersten Wurf des TextBlocks haben wir Reacts dangerouslySetInnerHTML verwendet. Jeder erfahrene React-Entwickler zuckt bei diesem Attribut instinktiv zusammen – und das völlig zu Recht.
Wenn wir Daten aus dem TinyMCE-Editor von Contao (einem Rich-Text-Editor) erhalten, handelt es sich um formatierte HTML-Strings (z. B. <p>Hallo <strong>Welt</strong></p>). Da React standardmäßig alle Strings "escaped" (um Cross-Site Scripting, XSS, zu verhindern), würde ohne dangerouslySetInnerHTML genau dieser Text mit sichtbaren HTML-Tags auf der Seite stehen, anstatt fettgedruckt zu sein.
Das Risiko: Auch wenn unsere Redakteure im Contao-Backend vertrauenswürdig sind, ist das Vertrauen in Rohdaten aus einer API in Enterprise-Architekturen ein No-Go. Ein gehackter Redakteurs-Account oder ein fehlerhaftes Plugin könnte ein <script>-Tag in den HTML-String injizieren. Wenn unser Next.js-Frontend dieses Skript ungeprüft rendert, haben wir eine kritische XSS-Schwachstelle erschaffen.
Wir müssen die Daten daher vor dem Rendering sanitisieren (reinigen).
Praxis & Code: Isomorphic DOMPurify
Das Standard-Tool zur HTML-Reinigung in der JavaScript-Welt ist DOMPurify. Da wir uns jedoch im Next.js App Router befinden und Server Components auf einer Node.js-Umgebung laufen (die kein natives DOM wie der Browser besitzt), verwenden wir das Paket isomorphic-dompurify. Es funktioniert serverseitig genauso gut wie clientseitig.
Installiere das Paket über dein Terminal:
npm install isomorphic-dompurifyNun überarbeiten wir unseren TextBlock, um das HTML zu reinigen, bevor es gerendert wird.
Datei: frontend/src/components/blocks/TextBlock.tsx (Aktualisiert)
1import DOMPurify from 'isomorphic-dompurify';
2import { ContaoContentElement } from '@/types/contao';
3
4export default function TextBlock({ data }: { data: ContaoContentElement }) {
5 // 1. Wenn kein HTML vorhanden ist, rendern wir nichts
6 if (!data.html) return null;
7
8 // 2. Wir definieren strenge Regeln für DOMPurify
9 const cleanHtml = DOMPurify.sanitize(data.html, {
10 ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4'],
11 ALLOWED_ATTR: ['href', 'target', 'rel', 'title', 'class'],
12 });
13
14 // 3. Wir rendern ausschließlich das bereinigte HTML
15 return (
16 <div
17 className="prose prose-slate max-w-none mb-8"
18 dangerouslySetInnerHTML={{ __html: cleanHtml }}
19 />
20 );
21}Was haben wir erreicht?
Selbst wenn die API den String <p>Hallo <script>alert('Hacked!');</script></p> liefert, entfernt DOMPurify das <script>-Tag restlos, weil es nicht in unserer ALLOWED_TAGS-Liste (Whitelist) steht. Übrig bleibt nur das harmlose <p>Hallo </p>.
Mit diesem Ansatz machst du dein Next.js-Frontend kugelsicher gegen Injections, während du deinen Redakteuren weiterhin die volle Layout-Freiheit des TinyMCE-Editors gewährst.
Zusätzlich nutzen wir hier das @tailwindcss/typography Plugin (die Klasse prose), welches ungestyltes, rohes HTML wunderschön und responsiv formatiert.

Headless Formulare bauen (React Hook Form + Zod)
Erinnerst du dich an Teil 6 unserer Masterclass? Dort haben wir in Symfony einen kugelsicheren, API-Key-geschützten Endpunkt (/api/v1/form/contact) gebaut, der eingehende Formulardaten validiert und säubert.
Jetzt stehen wir im Frontend vor der Herausforderung, genau dieses Formular in React zu rendern. Wenn ein Redakteur in Contao ein Formular-Inhaltselement anlegt, liefert uns die API vielleicht nur das Signal: {"type": "form", "formId": "contact"}.
In der Headless-Welt hast du zwei Möglichkeiten, Formulare abzuwickeln:
Voll-dynamisch: Du renderst anhand des Contao-JSON-Payloads (der alle Felder, Labels und Typen enthält) das Formular Stück für Stück. Das ist extrem aufwendig und fehleranfällig.
Bespoke Components: Du baust für jedes spezifische Formular (z. B. Kontaktformular, Newsletter) eine eigene, stark optimierte React-Komponente und verknüpfst sie in deinem Dispatcher.
Für Enterprise-Projekte ist Option 2 der Goldstandard. Sie erlaubt uns, die absolut beste User Experience (UX) durch Client-seitige Validierung zu gewährleisten, noch bevor überhaupt ein Request an Contao gesendet wird.
Die Werkzeuge der Wahl: React Hook Form & Zod
Um Formulare in React performant und sicher zu bauen, nutzen wir zwei Bibliotheken:
React Hook Form (RHF): Verwaltet den Formular-State über unkontrollierte Komponenten. Das verhindert, dass deine Seite bei jedem Tastendruck im Input-Feld komplett neu gerendert wird.
Zod: Ein TypeScript-First Schema-Validator. Zod ist unser "Frontend-Türsteher" und das perfekte Gegenstück zum Symfony Validator in unserem Backend.
Installiere die benötigten Pakete:
npm install react-hook-form zod @hookform/resolversLass uns nun unseren ContactFormBlock vorbereiten. Wir schreiben die UI-Komponente und binden Zod ein. (Die echte API-Anbindung an Contao folgt im Abschnitt 4).
Datei: frontend/src/components/blocks/ContactFormBlock.tsx
1'use client'; // Formulare benötigen Interaktivität (State, onChange, onSubmit)
2
3import { useState } from 'react';
4import { useForm } from 'react-hook-form';
5import { zodResolver } from '@hookform/resolvers/zod';
6import * as z from 'zod';
7
8// Wir importieren die Server Action (die wir in Abschnitt 4 bauen werden)
9import { submitContactForm } from '@/app/actions/contact';
10
11// 1. Zod Schema definieren: Wir spiegeln EXAKT die Regeln aus unserem Contao-Backend (Teil 6)
12export const contactSchema = z.object({
13 email: z.string().min(1, 'E-Mail ist ein Pflichtfeld.').email('Bitte eine gültige E-Mail-Adresse eingeben.'),
14 message: z.string().min(10, 'Ihre Nachricht muss mindestens 10 Zeichen lang sein.').max(5000, 'Nachricht ist zu lang.'),
15});
16
17// TypeScript Typ automatisch aus dem Zod-Schema extrahieren
18export type ContactFormData = z.infer<typeof contactSchema>;
19
20export default function ContactFormBlock() {
21 const [serverStatus, setServerStatus] = useState<'idle' | 'success' | 'error'>('idle');
22 const [errorMessage, setErrorMessage] = useState('');
23
24 // 2. React Hook Form mit dem Zod-Resolver initialisieren
25 const {
26 register,
27 handleSubmit,
28 formState: { errors, isSubmitting },
29 reset
30 } = useForm<ContactFormData>({
31 resolver: zodResolver(contactSchema),
32 });
33
34 // 3. Submit-Handler (wird nur aufgerufen, wenn Zod grünes Licht gibt!)
35 const onSubmit = async (data: ContactFormData) => {
36 // Die Server Action ausführen (Next.js leitet die Daten ans Backend weiter)
37 const result = await submitContactForm(data);
38
39 if (result.success) {
40 setServerStatus('success');
41 reset(); // Formular leeren
42 } else {
43 setServerStatus('error');
44 setErrorMessage(result.error || 'Es ist ein Fehler aufgetreten.');
45 }
46 };
47
48 return (
49 <div className="bg-white p-6 rounded-lg shadow-sm border border-slate-100 max-w-lg mb-8">
50 <h3 className="text-2xl font-bold mb-4">Kontaktieren Sie uns</h3>
51
52 {serverStatus === 'success' && (
53 <div className="p-4 mb-4 bg-green-50 text-green-700 border border-green-200 rounded">
54 Vielen Dank! Ihre Nachricht wurde sicher verschickt.
55 </div>
56 )}
57
58 {serverStatus === 'error' && (
59 <div className="p-4 mb-4 bg-red-50 text-red-700 border border-red-200 rounded">
60 {errorMessage}
61 </div>
62 )}
63
64 {/* Das handleSubmit von RHF blockiert den echten Submit, bis Zod validiert hat */}
65 <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
66
67 {/* Feld: E-Mail */}
68 <div>
69 <label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-1">E-Mail</label>
70 <input
71 id="email"
72 type="email"
73 {...register('email')}
74 className={`w-full p-2 border rounded focus:ring-2 focus:ring-brand-primary outline-none transition-all ${
75 errors.email ? 'border-red-500 bg-red-50' : 'border-slate-300'
76 }`}
77 />
78 {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
79 </div>
80
81 {/* Feld: Nachricht */}
82 <div>
83 <label htmlFor="message" className="block text-sm font-medium text-slate-700 mb-1">Ihre Nachricht</label>
84 <textarea
85 id="message"
86 rows={4}
87 {...register('message')}
88 className={`w-full p-2 border rounded focus:ring-2 focus:ring-brand-primary outline-none transition-all ${
89 errors.message ? 'border-red-500 bg-red-50' : 'border-slate-300'
90 }`}
91 />
92 {errors.message && <p className="text-red-500 text-sm mt-1">{errors.message.message}</p>}
93 </div>
94
95 {/* Submit Button */}
96 <button
97 type="submit"
98 disabled={isSubmitting}
99 className="bg-brand-primary text-white font-semibold py-2 px-6 rounded hover:bg-sky-600 transition-colors disabled:opacity-50 flex items-center justify-center min-w-[150px]"
100 >
101 {isSubmitting ? 'Wird gesendet...' : 'Nachricht senden'}
102 </button>
103 </form>
104 </div>
105 );
106}Mit diesem Code prüft das Frontend in Echtzeit, ob die E-Mail ein @ enthält und die Nachricht mindestens 10 Zeichen lang ist. Erst wenn Zod grünes Licht gibt, dürfen die Daten an den Server geschickt werden.

Server Actions & Die sichere Contao-Anbindung
Die Daten sind im Frontend gesammelt und von Zod validiert. Wie bekommen wir sie nun sicher in unsere Contao-Datenbank?
In Teil 6 haben wir gelernt, dass wir das Backend-For-Frontend (BFF) Pattern nutzen müssen, um das CSRF-Dilemma zu lösen und unseren API_SHARED_SECRET geheim zu halten. Der Browser des Nutzers darf niemals direkt mit Contao kommunizieren. Er muss mit dem Next.js-Node-Server sprechen, der die Anfrage signiert und weiterleitet.
Im Next.js App Router gibt es dafür ein revolutionäres Feature: Server Actions.
Server Actions erlauben es dir, asynchrone Funktionen auf dem Server zu schreiben, die du direkt aus einer Client-Komponente (wie unserem Formular) aufrufen kannst. Next.js generiert im Hintergrund automatisch einen sicheren API-Endpunkt dafür.
Praxis & Code: Die Server Action implementieren
Erstelle einen neuen Ordner actions in deinem src/app/ Verzeichnis und lege die Datei contact.ts an:
Datei: frontend/src/app/actions/contact.ts
1'use server'; // Diese Direktive ist zwingend! Sie markiert die Datei als Server Action.
2
3import { contactSchema, ContactFormData } from '@/components/blocks/ContactFormBlock';
4
5export async function submitContactForm(data: ContactFormData) {
6 // 1. Zod-Validierung auf dem SERVER wiederholen (Never trust the client!)
7 // Das schützt uns vor Angreifern, die den Browser umgehen und cURL/Postman nutzen
8 const parsedData = contactSchema.safeParse(data);
9
10 if (!parsedData.success) {
11 return { success: false, error: 'Ungültige Formulardaten.' };
12 }
13
14 try {
15 // 2. Den sicheren POST-Request an unsere Contao API senden (siehe Teil 6)
16 const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
17 const API_SECRET = process.env.CONTAO_API_KEY;
18
19 const res = await fetch(`${API_URL}/api/v1/form/contact`, {
20 method: 'POST',
21 headers: {
22 'Content-Type': 'application/json',
23 // Der geheime Key verlässt den Next.js-Server nicht!
24 'Authorization': `Bearer ${API_SECRET}`,
25 },
26 body: JSON.stringify(parsedData.data),
27 });
28
29 if (!res.ok) {
30 const errorData = await res.json();
31 return { success: false, error: errorData.message || 'Contao hat die Anfrage abgelehnt.' };
32 }
33
34 return { success: true, message: 'Nachricht erfolgreich an Contao übermittelt.' };
35
36 } catch (error) {
37 console.error('BFF Error:', error);
38 return { success: false, error: 'Serverfehler bei der Übermittlung.' };
39 }
40}Das Ergebnis: Du hast soeben ein hochprofessionelles, entkoppeltes Enterprise-Formular gebaut. Die Nutzererfahrung ist sofort, schnell und reibungslos (Dank React Hook Form und Zod). Die Sicherheit ist wasserdicht: Der Browser kennt das Contao-Backend nicht einmal, es gibt keine CSRF-Probleme, und Next.js fungiert als gepanzerter Mittelsmann (Proxy), der den API-Key anhängt und die Daten in Contao abliefert.

Fazit – Der Content wird lebendig
Mit dem Abschluss dieses 9. Teils haben wir die größte Lücke in der Headless-Entwicklung geschlossen: Die Verwandlung von totem JSON-Code in lebendige, interaktive Websites.
Lass uns rekapitulieren, welche Meilensteine wir erreicht haben:
Das Factory-Pattern (Dispatcher): Anstatt für jede Seite starre Layouts zu programmieren, haben wir ein intelligentes Mapping-System gebaut. Contao liefert ein Array aus Modulen, und Next.js rendert exakt die dazu passenden React-Komponenten.
XSS-Sicherheit: Wir haben verstanden, dass
dangerouslySetInnerHTMLseinen Namen zu Recht trägt. Durchisomorphic-dompurifykönnen Redakteure frei texten, ohne dass wir Sicherheitslücken im Frontend riskieren.Enterprise-Formulare: Wir haben das klassische Formular-Paradigma revolutioniert. Mit React Hook Form und Zod fangen wir Fehler sofort beim Tippen ab. Die Server Actions von Next.js agieren als unsichtbarer Proxy, der die Daten mit unserem Contao-Backend-Schlüssel signiert und sicher überträgt.
Unsere Plattform ist nun fähig, Inhalte auszugeben und Nutzer-Interaktionen (POST-Requests) zu verarbeiten.
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
High-Performance & Caching-Strategien
Häufig gestellte Fragen (FAQ)
Contao liefert ja den Formulargenerator mit.
In der Theorie klingt es verlockend, einen FormRenderer zu schreiben, der über das Array der Formularfelder loopt und dynamisch <input>-Tags generiert. In der Praxis (und in Enterprise-Projekten) stößt du damit jedoch sehr schnell an Grenzen. Komplexe mehrstufige Formulare (Multi-Step), asynchrone Validierungen (z.B. "Ist die E-Mail schon vergeben?") oder hochspezifische UX-Patterns lassen sich mit dynamischen Generatoren kaum abbilden. Der "Bespoke"-Ansatz (spezifische React-Komponenten für spezifische Formulare) ist wartbarer, typsicherer und bietet eine drastisch bessere User Experience.
Reicht nicht eines?
Nein. Die Backend-Validierung (Symfony) ist für die Sicherheit zuständig (um bösartige Payloads abzuwehren, die die UI umgehen). Die Frontend-Validierung (Zod) ist für die User Experience zuständig (um dem Nutzer sofort Feedback zu geben, ohne auf eine Server-Antwort warten zu müssen). Das ist das Prinzip der Defense in Depth.
Ja. Wenn du in Contao ein völlig neuartiges Element baust (z. B. einen interaktiven 3D-Viewer), musst du im Next.js-Frontend die entsprechende React-Komponente (ViewerBlock.tsx) schreiben und sie im BlockRenderer (der componentMap) registrieren. Das ist der Architektur-Kompromiss, den man bei entkoppelten Systemen eingeht.
Dateiuploads erfordern, dass du in deiner Next.js Server Action die FormData API verwendest (anstatt application/json). Du nimmst das File-Objekt aus dem Client entgegen und sendest es als multipart/form-data an den Contao-Controller, den wir in Teil 6 genau für diesen Zweck (mit Dbafs::addResource()) präpariert haben.
Dein nächster Schritt: Der Draft- und Preview-Modus
Unsere Architektur steht, die Seite lädt in Millisekunden und die Formulare
sind sicher. Doch eine entscheidende Frage aus der Redakteurs-Perspektive ist
noch offen:
"Wie kann ich meine Änderungen sehen, bevor ich sie
veröffentliche?"
In der monolithischen Welt klickt der Redakteur in Contao einfach auf das kleine "Auge"-Icon und sieht die unveröffentlichte Seite. Im Headless-Setup beißt sich dieser Wunsch massiv mit unserer Next.js Caching-Architektur (ISR). Wenn Next.js die Seiten statisch generiert, wie können wir dem Redakteur im Contao-Backend eine Live-Vorschau seiner Änderungen bieten, ohne den Cache für reguläre Besucher zu zerstören?
Im kommenden Teil 10: Der Draft-/Preview-Modus lösen wir dieses komplexe Problem. Du wirst lernen:
-
Das Next.js Draft Mode API: Wie wir den statischen Cache gezielt und sicher für einzelne User (Redakteure) umgehen.
-
Die Contao Backend-Integration: Wie wir das Next.js-Frontend als iframe-Vorschau direkt in das Contao Backend injizieren.
-
Token-basierte Authentifizierung: Wie das Frontend erkennt, dass gerade ein berechtigter Contao-Redakteur auf die Seite zugreift.
Jetzt starten: Teil 10 – Der Draft-/Preview-Modus für Redakteure einrichten

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.


