
Dynamisches Routing & Navigation in Next.js

Der Catch-All-Router – Das schwarze Loch für URLs
In Teil 7 haben wir das statische Fundament für unser Next.js Frontend gegossen. Wenn du aktuell jedoch http://localhost:3000/unternehmen/team aufrufst, begrüßt dich eine unschöne 404-Fehlerseite. Warum? Weil wir Next.js noch nicht beigebracht haben, wie es mit dynamischen URLs umgehen soll.
In einer klassischen monolithischen Contao-Installation ist das Routing tief in den Core eingebacken. Ein Redakteur legt im Backend eine Seite namens "Team" als Unterseite von "Unternehmen" an, und Contao generiert automatisch die Route /unternehmen/team.
In der Headless-Welt weiß unser Next.js-Frontend absolut nichts von diesem Seitenbaum. Es wäre architektonischer Wahnsinn (und für Redakteure ein Albtraum), wenn wir für jede neue Contao-Seite manuell einen neuen Ordner in Next.js anlegen müssten (z.B. app/unternehmen/team/page.tsx). Wir brauchen eine Lösung, die jede beliebige URL abfängt, an unsere Contao-API sendet und fragt: "Hey Contao, existiert eine Seite mit diesem Alias? Wenn ja, gib mir die Daten!"
Genau hier betritt die Optional Catch-All Route die Bühne.
Das Konzept der [[...slug]] Route
Der Next.js App Router hat ein geniales Feature für verschachtelte, dynamische Segmente. Wenn wir einen Ordner mit doppelten eckigen Klammern und drei Punkten benennen – also [[...slug]] –, erzeugen wir eine "Optionale Catch-All Route".
Warum Catch-All (
...slug)? Es fängt nicht nur eine einzelne Ebene ab (wie/unternehmen), sondern alle verschachtelten Ebenen (wie/unternehmen/standorte/berlin/kontakt). Next.js wandelt diese URL in ein Array um:['unternehmen', 'standorte', 'berlin', 'kontakt'].Warum Optional (doppelte Klammern
[[ ]])? Dadurch feuert die Route auch, wenn gar kein Parameter übergeben wird – also beim Aufruf der absoluten Startseite (/). Wir können somit unser gesamtes Seiten-Routing in einer einzigen Datei zentralisieren!
Praxis & Code: Die Next.js 15 Architektur
Lass uns diese Route bauen. Lösche als Erstes (falls vorhanden) die Datei app/page.tsx, die bei der Next.js-Installation generiert wurde. Sie würde unserer Catch-All Route im Weg stehen.
Erstelle nun folgende tiefe Ordnerstruktur in deinem src/app/ Verzeichnis:src/app/[[...slug]]/page.tsx
Achtung, Senior-Wissen für Next.js 15: Wenn du ältere Tutorials (Next.js 13 oder 14) liest, wirst du sehen, dass die params in der Page-Komponente direkt als Objekt ausgelesen wurden. Das ist in Next.js 15 ein Breaking Change! Die Parameter (params und searchParams) sind nun zwingend asynchron (Promises) und müssen mit await aufgelöst werden. Tust du das nicht, wirft dein Build einen TypeError.
Datei: frontend/src/app/[[...slug]]/page.tsx
1import { notFound } from 'next/navigation';
2
3// 1. Strikte Typisierung für die asynchronen Parameter in Next.js 15+
4interface PageProps {
5 params: Promise<{ slug?: string[] }>;
6}
7
8export default async function CatchAllPage({ params }: PageProps) {
9 // 2. Wir müssen die Params in Next.js 15 awaiten!
10 const resolvedParams = await params;
11
12 // 3. Den Array-Slug wieder zu einem String zusammenbauen (z.B. "unternehmen/team")
13 // Falls kein Slug existiert (Startseite), setzen wir einen leeren String oder "index"
14 const currentSlug = resolvedParams.slug ? resolvedParams.slug.join('/') : 'index';
15
16 // 4. (Simulation) Wir rufen unsere Contao-API auf
17 // Den echten Fetcher bauen wir in der nächsten Iteration dieses Artikels!
18 const contaoData = await fetch(`http://localhost:8080/api/v1/page/${currentSlug}`);
19
20 // 5. 404 Handling: Wenn Contao die Seite nicht kennt, werfen wir die Next.js 404-Seite
21 if (!contaoData.ok) {
22 notFound();
23 }
24
25 const page = await contaoData.json();
26
27 // 6. Das Frontend-Rendering (Vorübergehender Platzhalter)
28 return (
29 <main className="container mx-auto py-12">
30 <h1 className="text-4xl font-bold text-brand-primary">
31 {page.routing?.title || 'Seite gefunden!'}
32 </h1>
33 <p className="mt-4 text-slate-600">
34 Aktueller Slug: <code className="bg-slate-100 p-1 rounded">/{currentSlug}</code>
35 </p>
36 </main>
37 );
38}Mit dieser einen Datei haben wir das gesamte Seiten-Routing von Contao nachgebaut. Egal, wie tief der Redakteur die Seiten im Backend verschachtelt – dieser Next.js Server Component fängt die URL ab, formatiert sie sauber und delegiert sie an unseren Contao-Endpunkt (den wir in Teil 5 gebaut haben).

Den API-Fetcher & Typsicherheit implementieren
Im ersten Abschnitt haben wir Next.js beigebracht, wie es dynamische URLs abfängt. Wir haben die Daten mit einem einfachen fetch()-Aufruf direkt in der Page-Komponente geladen. Für einen schnellen Prototyp ist das okay, für eine Enterprise-Anwendung jedoch ein absolutes No-Go.
Ein roher fetch-Aufruf mitten im Code bringt drei massive Probleme mit sich:
Keine Typsicherheit: Next.js hat keine Ahnung, wie das JSON von Contao aussieht. Du bekommst keine Autovervollständigung (IntelliSense) und Laufzeitfehler sind vorprogrammiert, wenn sich Datenstrukturen ändern.
Keine globale Authentifizierung: Du müsstest deinen geheimen API-Key bei jedem einzelnen Aufruf neu in die Header tippen.
Schlechtes Caching-Management: Ab Next.js 15 werden
fetch-Aufrufe nicht mehr standardmäßig aggressiv gecacht. Wir müssen die Caching-Strategie (z.B. für Incremental Static Regeneration - ISR) explizit definieren.
Als Senior-Entwickler bauen wir uns daher eine saubere Abstraktionsschicht: Wir definieren zuerst TypeScript-Interfaces (DTOs), die exakt den Normalizern entsprechen, die wir in Contao (Teil 5) gebaut haben. Danach erstellen wir einen zentralen Fetcher-Service.
Praxis & Code: TypeScript Interfaces und der zentrale API-Client
Lass uns in unserem src/-Verzeichnis für Ordnung sorgen.
Schritt 1: Die Typen definieren
Erstelle die Datei src/types/contao.ts:
1// Datei: frontend/src/types/contao.ts
2
3export interface ContaoMeta {
4 id: number;
5 type: string;
6 language: string;
7}
8
9export interface ContaoRouting {
10 alias: string;
11 title: string;
12}
13
14// Generisches Interface für alle Inhaltselemente
15export interface ContaoContentElement {
16 id: number;
17 type: string;
18 // Weitere spezifische Felder (text, image_url, etc.) werden hier abgeleitet
19 [key: string]: any;
20}
21
22export interface ContaoArticle {
23 id: number;
24 elements: ContaoContentElement[];
25}
26
27export interface ContaoPageData {
28 meta: ContaoMeta;
29 routing: ContaoRouting;
30 content: {
31 articles: ContaoArticle[];
32 };
33}Schritt 2: Den API-Fetcher bauen
Jetzt erstellen wir das eigentliche Werkzeug, das mit Contao spricht. Erstelle die Datei src/lib/api/contao.ts:
1// Datei: frontend/src/lib/api/contao.ts
2import { ContaoPageData } from '@/types/contao';
3
4const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
5const API_SECRET = process.env.CONTAO_API_KEY;
6
7export async function getContaoPageBySlug(slug: string): Promise<ContaoPageData | null> {
8 if (!API_SECRET) {
9 throw new Error('CONTAO_API_KEY ist in den Umgebungsvariablen nicht definiert.');
10 }
11
12 try {
13 // Next.js 15 Fetch-Konfiguration: Wir nutzen Tags für On-Demand Revalidation
14 const res = await fetch(`${API_BASE_URL}/api/v1/page/${slug}`, {
15 method: 'GET',
16 headers: {
17 'Accept': 'application/json',
18 'Authorization': `Bearer ${API_SECRET}`,
19 },
20 next: {
21 // Caching-Strategie (ISR): Der Cache wird nach 3600 Sekunden als stale markiert
22 // oder kann über den Tag 'contao-pages' manuell invalidiert werden.
23 revalidate: 3600,
24 tags: ['contao-pages', `page-${slug}`],
25 },
26 });
27
28 if (!res.ok) {
29 if (res.status === 404) return null; // Seite existiert nicht -> 404 im Frontend
30 throw new Error(`Contao API Fehler: ${res.statusText}`);
31 }
32
33 const data: ContaoPageData = await res.json();
34 return data;
35
36 } catch (error) {
37 console.error(`Fehler beim Abrufen der Seite /${slug}:`, error);
38 return null;
39 }
40}Schritt 3: Den Fetcher in der Page-Komponente nutzen
Gehe zurück zu deiner src/app/[[...slug]]/page.tsx und ersetze den alten Code durch unseren neuen, typsicheren Service:
1// Datei: frontend/src/app/[[...slug]]/page.tsx
2import { notFound } from 'next/navigation';
3import { getContaoPageBySlug } from '@/lib/api/contao';
4
5interface PageProps {
6 params: Promise<{ slug?: string[] }>;
7}
8
9export default async function CatchAllPage({ params }: PageProps) {
10 const resolvedParams = await params;
11 const currentSlug = resolvedParams.slug ? resolvedParams.slug.join('/') : 'index';
12
13 // 1. Daten über unseren typsicheren Service holen
14 const pageData = await getContaoPageBySlug(currentSlug);
15
16 // 2. Sauberes 404-Handling
17 if (!pageData) {
18 notFound();
19 }
20
21 // Ab hier hast du volle Autovervollständigung für "pageData"!
22 return (
23 <main className="container mx-auto py-12">
24 <h1 className="text-4xl font-bold mb-8">
25 {pageData.routing.title}
26 </h1>
27
28 {/* Artikel-Iteration (Platzhalter) */}
29 {pageData.content.articles.map((article) => (
30 <section key={article.id} className="mb-12 border-b pb-8">
31 <p className="text-slate-500">Artikel-ID: {article.id}</p>
32 <p>Hier rendern wir später die {article.elements.length} Inhaltselemente...</p>
33 </section>
34 ))}
35 </main>
36 );
37}Durch diese Architektur haben wir eine unsichtbare, fehlerresistente Brücke zwischen Contao und Next.js geschlagen. Ändert sich der Payload im Backend, passen wir das Interface in types/contao.ts an, und TypeScript zeigt uns sofort, welche Komponenten im Frontend aktualisiert werden müssen.

Die Hauptnavigation – Global, dynamisch und rasend schnell
Das Routing funktioniert. Aber eine Website ohne Navigation ist wie ein Labyrinth ohne Karte. In der klassischen Welt platzieren wir einfach ein Navigations-Modul im Seitenlayout von Contao. Im Headless-Szenario müssen wir das Menü dynamisch über einen separaten API-Endpunkt abfragen und im Next.js-Frontend global bereitstellen.
Das wirft eine wichtige Architektur-Frage auf: Wo bauen wir die Navigation ein?
Wir rufen die Navigation nicht in der [[...slug]]/page.tsx ab. Warum? Weil die Navigation global ist. Sie soll sich nicht bei jedem Seitenwechsel neu rendern. Der App Router von Next.js bietet dafür die perfekte Datei: app/layout.tsx.
Alles, was in der layout.tsx (als Server Component) gefetcht und gerendert wird, bildet die dauerhafte "Schale" deiner Applikation. Wenn der User von der Startseite zur "Über uns"-Seite navigiert, wird die Navigation nicht neu geladen – das spart API-Requests und macht die User-Experience rasend schnell.
Praxis & Code: Der Navigations-Fetcher und die Layout-Integration
Damit die Navigation aus der API nahtlos in Next.js übergeht, definieren wir zunächst ihren Typ, schreiben einen schnellen Fetcher und bauen eine rekursive React-Komponente.
Schritt 1: Typen erweitern
Ergänze deine src/types/contao.ts um die Navigations-Struktur:
1// Datei: frontend/src/types/contao.ts (Ergänzung)
2
3export interface ContaoNavItem {
4 id: number;
5 title: string;
6 href: string; // z.B. "/unternehmen/team"
7 isActive?: boolean;
8 subitems?: ContaoNavItem[]; // Rekursive Struktur für Dropdowns!
9}Schritt 2: Fetcher hinzufügen
Öffne deine src/lib/api/contao.ts und füge den Navigations-Call hinzu. (Wir gehen davon aus, dass du in Contao einen Endpunkt /api/v1/navigation/main gebaut hast, der den Menübaum liefert).
1// Datei: frontend/src/lib/api/contao.ts (Ergänzung)
2
3export async function getContaoNavigation(menuId: string = 'main'): Promise<ContaoNavItem[]> {
4 if (!API_SECRET) return [];
5
6 try {
7 const res = await fetch(`${API_BASE_URL}/api/v1/navigation/${menuId}`, {
8 headers: { 'Authorization': `Bearer ${API_SECRET}` },
9 next: {
10 revalidate: 3600, // Caching für 1 Stunde
11 tags: ['contao-navigation'] // Erlaubt sofortige Invalidierung bei Menü-Änderungen
12 }
13 });
14
15 if (!res.ok) throw new Error('Navigation API Error');
16 return res.json();
17 } catch (error) {
18 console.error('Fehler beim Abrufen der Navigation:', error);
19 return [];
20 }
21}Schritt 3: Die rekursive Menü-Komponente
Jetzt bauen wir die UI-Komponente für den Header. Da ein Dropdown-Menü oft Interaktivität benötigt (z.B. Aufklappen am Handy), deklarieren wir den interaktiven Teil als Client Component, während der Datenabruf auf dem Server bleibt.
Erstelle die Datei src/components/layout/MainNavigation.tsx:
1// Datei: frontend/src/components/layout/MainNavigation.tsx
2'use client'; // Client Component für Interaktivität (Hover/Click states)
3
4import Link from 'next/link';
5import { usePathname } from 'next/navigation';
6import { ContaoNavItem } from '@/types/contao';
7
8interface Props {
9 items: ContaoNavItem[];
10}
11
12export default function MainNavigation({ items }: Props) {
13 // Next.js Hook, um die aktive Route auszulesen
14 const pathname = usePathname();
15
16 // Eine rekursive Render-Funktion für unendlich tiefe Menüs
17 const renderNavTree = (navItems: ContaoNavItem[], level = 0) => {
18 return (
19 <ul className={`flex ${level === 0 ? 'gap-6' : 'flex-col gap-2 pl-4 mt-2'}`}>
20 {navItems.map((item) => {
21 // Prüfen, ob wir uns auf der aktuellen Route befinden
22 const isActive = pathname === item.href || pathname?.startsWith(`${item.href}/`);
23
24 return (
25 <li key={item.id} className="relative group">
26 <Link
27 href={item.href}
28 className={`text-sm font-medium transition-colors hover:text-brand-primary ${
29 isActive ? 'text-brand-primary' : 'text-slate-700'
30 }`}
31 >
32 {item.title}
33 </Link>
34
35 {/* Dropdown rendern, falls Sub-Items existieren */}
36 {item.subitems && item.subitems.length > 0 && (
37 <div className={level === 0 ? "absolute left-0 top-full hidden group-hover:block bg-white shadow-lg p-4 rounded-md min-w-[200px]" : "block"}>
38 {renderNavTree(item.subitems, level + 1)}
39 </div>
40 )}
41 </li>
42 );
43 })}
44 </ul>
45 );
46 };
47
48 return (
49 <nav aria-label="Hauptnavigation">
50 {renderNavTree(items)}
51 </nav>
52 );
53}Schritt 4: Die Integration in das Root Layout
Zum Schluss verheiraten wir alles in der app/layout.tsx. Hier läuft die Server Component, die zur Build-Zeit oder Request-Zeit die API anfragt und die fertigen Daten an unsere MainNavigation weiterreicht.
1// Datei: frontend/src/app/layout.tsx
2import type { Metadata } from "next";
3import { getContaoNavigation } from "@/lib/api/contao";
4import MainNavigation from "@/components/layout/MainNavigation";
5import "@/styles/globals.css";
6
7export const metadata: Metadata = {
8 title: {
9 template: '%s | Headless Masterclass',
10 default: 'Contao Next.js Headless Masterclass',
11 }
12};
13
14export default async function RootLayout({
15 children,
16}: {
17 children: React.ReactNode;
18}) {
19 // 1. Navigation auf dem Server laden (Profitiert vom Next.js Cache!)
20 const navItems = await getContaoNavigation('main');
21
22 return (
23 <html lang="de">
24 <body className="bg-slate-50 text-slate-900 font-sans antialiased min-h-screen flex flex-col">
25
26 {/* Globaler Header */}
27 <header className="bg-white border-b border-slate-200 sticky top-0 z-50">
28 <div className="container mx-auto px-4 h-16 flex items-center justify-between">
29 <div className="text-xl font-bold text-brand-primary">LOGO</div>
30 {/* Wir reichen die Server-Daten an die Client Component weiter */}
31 <MainNavigation items={navItems} />
32 </div>
33 </header>
34
35 {/* Hier injiziert Next.js die dynamischen [[...slug]] Pages */}
36 <div className="flex-grow">
37 {children}
38 </div>
39
40 {/* Globaler Footer */}
41 <footer className="bg-slate-900 text-slate-400 py-8 mt-12">
42 <div className="container mx-auto px-4 text-center">
43 © {new Date().getFullYear()} Contao Headless Masterclass
44 </div>
45 </footer>
46
47 </body>
48 </html>
49 );
50}Mit dieser Architektur schlägst du zwei Fliegen mit einer Klappe: Der Server kümmert sich um den sicheren API-Aufruf und das Caching. Das JSON wird zur Build-Zeit in HTML verwandelt. Erst auf dem Client übernimmt React die Kontrolle über die Navigation, um isActive-States zu berechnen oder Dropdowns bei Mouse-Hover elegant einzublenden.

Performance & Static Generation (generateStaticParams)
Bisher fangen wir mit unserer Catch-All-Route [[...slug]] jede Anfrage ab und rufen die Contao-API auf. Das ist großartig, aber wenn hunderte Nutzer gleichzeitig auf die Website zugreifen, würde unser Server jedes Mal auf die API warten müssen.
Um eine Google PageSpeed-Wertung von 100/100 zu erreichen, wollen wir, dass Next.js alle Seiten bereits beim Build-Prozess (Static Site Generation - SSG) vorrendert. Das fertige HTML liegt dann auf einem CDN und lädt in Millisekunden.
Im Next.js App Router gibt es dafür eine magische Funktion: generateStaticParams.
Wir bringen Next.js bei, vorher bei Contao anzufragen: "Gib mir bitte eine Liste aller existierenden URLs, damit ich sie alle auf einmal rendern kann."
Praxis & Code: Die Seiten vorkompilieren
Zuerst erweitern wir unseren API-Fetcher um eine Route, die nur ein flaches Array aller URLs (Aliase) aus Contao zurückgibt (z.B. ['index', 'unternehmen', 'unternehmen/team']).
1// Datei: frontend/src/lib/api/contao.ts (Ergänzung)
2
3export async function getContaoAllAliases(): Promise<string[]> {
4 if (!API_SECRET) return [];
5
6 try {
7 const res = await fetch(`${API_BASE_URL}/api/v1/routing/aliases`, {
8 headers: { 'Authorization': `Bearer ${API_SECRET}` }
9 });
10 if (!res.ok) return [];
11 return res.json();
12 } catch (error) {
13 return [];
14 }
15}Nun öffnen wir wieder unsere Catch-All Route und fügen die generateStaticParams-Funktion ganz oben hinzu:
Datei: frontend/src/app/[[...slug]]/page.tsx (Erweiterung)
1import { notFound } from 'next/navigation';
2import { getContaoPageBySlug, getContaoAllAliases } from '@/lib/api/contao';
3
4interface PageProps {
5 params: Promise<{ slug?: string[] }>;
6}
7
8// NEU: Diese Funktion wird nur während des Builds (npm run build) ausgeführt!
9export async function generateStaticParams() {
10 const aliases = await getContaoAllAliases();
11
12 // Next.js erwartet ein Array von Objekten, die den Ordner-Namen ('slug') matchen
13 return aliases.map((alias) => ({
14 // 'index' wird zum leeren Array (Root /), ansonsten splitten wir den String
15 slug: alias === 'index' ? [] : alias.split('/'),
16 }));
17}
18
19export default async function CatchAllPage({ params }: PageProps) {
20 const resolvedParams = await params;
21 const currentSlug = resolvedParams.slug ? resolvedParams.slug.join('/') : 'index';
22
23 const pageData = await getContaoPageBySlug(currentSlug);
24
25 if (!pageData) notFound();
26
27 return (
28 <main className="container mx-auto py-12">
29 <h1 className="text-4xl font-bold mb-8">
30 {pageData.routing.title}
31 </h1>
32 {/* ... Artikel-Rendering ... */}
33 </main>
34 );
35}Was passiert hier Magisches?
Wenn du nun npm run build aufrufst, feuert Next.js generateStaticParams ab. Es erhält vielleicht 50 Aliase aus Contao. Next.js führt daraufhin die CatchAllPage-Funktion 50 Mal im Hintergrund aus, lädt alle Daten, rendert das HTML und legt es statisch auf der Festplatte ab. Dein Frontend ist nun physisch vom Backend entkoppelt und unendlich skalierbar!
(Dank des Parameters revalidate: 3600 aus Abschnitt 2 greift zudem das ISR (Incremental Static Regeneration): Auch wenn die Seite statisch ist, prüft Next.js im Hintergrund stündlich, ob der Redakteur in Contao den Text geändert hat, und aktualisiert den Cache lautlos).

Fazit – Der Seitenbaum lebt
Wir haben in diesem 8. Teil der Masterclass das Herzstück unserer Headless-Architektur zum Schlagen gebracht. Anstatt statische Landingpages hart in React zu programmieren, haben wir Next.js beigebracht, auf die Struktur von Contao zu reagieren.
Die Catch-All Route (
[[...slug]]) agiert als intelligenter Türsteher für alle URLs.Der typsichere Fetcher garantiert, dass wir fehlerfrei und sicher (via API-Key) mit Contao kommunizieren.
Die dynamische Navigation in der
layout.tsxmacht das Menü auf jeder Unterseite global verfügbar, ohne Performance-Einbußen.Und mit
generateStaticParamshaben wir Next.js in einen High-Speed-Rendermotor verwandelt, der die Contao-Daten zur Build-Zeit in statisches HTML gießt.
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
Häufig gestellte Fragen (FAQ)
Dank ISR (Incremental Static Regeneration) nicht zwingend! Wenn Next.js eine URL aufruft, die beim Build noch nicht existierte, versucht der App Router standardmäßig, die Seite "On-Demand" (also in Echtzeit) zu generieren. Wenn Contao die Seite liefert, wird sie gerendert, im Cache gespeichert und verhält sich fortan wie eine statisch generierte Seite.
Eine Route mit einfachen eckigen Klammern ([...slug]/page.tsx) verlangt zwingend mindestens einen URL-Parameter (wie /unternehmen). Die Startseite deiner Domain (/) würde also nicht gematcht werden und einen 404-Fehler werfen. Die doppelten Klammern bedeuten "Optional" – sie matchen also sowohl /unternehmen als auch die absolute Root-URL /.
In einer mehrsprachigen Headless-Architektur fügst du dem App Router oft noch eine Ebene hinzu: app/[locale]/[[...slug]]/page.tsx. Der URL-Pfad beginnt dann z.B. immer mit /de/ oder /en/. Im Fetcher greifst du den locale-Parameter ab und sendest ihn an Contao, damit die API weiß, aus welchem Seitenbaum sie die Daten laden muss.
Dein nächster Schritt: Der Component-Dispatcher (Content-Rendering)
Unsere Architektur kann nun URLs auflösen, Navigationen rendern und weiß, auf welcher Seite sich der Nutzer befindet. Doch aktuell geben wir als Inhalt nur eine langweilige Platzhalter-Liste aus (<p>Artikel-ID: {article.id}</p>).
Das wahre Potenzial eines Headless CMS entfaltet sich erst, wenn wir die Datenblöcke aus Contao – Texte, Galerien, Akkordeons, Slider – in hochgradig interaktive React-Komponenten verwandeln.
Im kommenden Teil 9: Komponenten-Mapping & Content-Rendering widmen wir uns dem Herzstück der Darstellung. Du wirst lernen:
Das Factory-Pattern (Der Dispatcher): Wie wir eine smarte Komponente bauen, die das JSON ausliest (
{"type": "text_image"}) und automatisch die passende React-Komponente (<TextImageBlock />) lädt.Sauberes HTML rendern: Wie wir TinyMCE-Richtext aus Contao sicher im Frontend ausgeben, ohne uns Reacts
dangerouslySetInnerHTML-Fallstricken hinzugeben.Verschachtelte Elemente: Wie wir komplexe Strukturen wie Spaltensets oder Wrapper-Elemente Headless abbilden.
Jetzt starten: Teil 9 – Komponenten-Mapping & Content-Rendering in Next.js

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.


