
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
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.


