
Next.js Setup & Grundstruktur für dein Headless CMS

Willkommen im Frontend – Die Macht des App Routers
Die Festung steht. In den ersten beiden Phasen unserer Masterclass haben wir Contao als sicheres, hochperformantes Headless CMS konfiguriert. Wir haben relationale Datenbankabfragen optimiert (N+1 Query-Killer), typsichere DTOs erstellt und unsere Endpunkte gegen CSRF- und Spam-Attacken abgeriegelt. Unser Backend liefert nun den perfekten, puren JSON-Datenstrom.
Jetzt wechseln wir die Seiten. Willkommen in Phase 3: Das Next.js Frontend. In diesem siebten Teil widmen wir uns dem perfekten Next.js Setup für ein Headless CMS. Wir werden die Applikation initialisieren, die Ordnerstruktur für ein skalierbares Enterprise-Projekt aufbauen und klären, warum der Next.js App Router in Kombination mit React Server Components (RSC) den absoluten Gamechanger für Headless-Architekturen darstellt.
Warum der Next.js App Router?
Als Senior Webentwickler habe ich über die Jahre viele Paradigmenwechsel miterlebt – von jQuery-Spaghetti-Code über Single Page Applications (SPAs) bis hin zu Static Site Generation (SSG). Der Next.js App Router (eingeführt in Version 13 und perfektioniert in 14/15) ist der wichtigste Architektur-Sprung seit der Erfindung von React.
In der alten Welt (dem Next.js Pages Router oder in klassischen React-SPAs) mussten wir CMS-Daten oft auf dem Client über useEffect und fetch nachladen. Die Folge: Der Nutzer sah einen Lade-Spinner, das JavaScript-Bundle war riesig (weil die gesamte API-Logik im Browser landete) und die SEO-Performance war katastrophal. Alternativ nutzten wir getServerSideProps, was jedoch an die gesamte Seite gekoppelt war und oft zu starren, langsamen Wasserfall-Abfragen führte.
Mit dem App Router und den React Server Components (RSC) ist standardmäßig alles ein Server Component. Das bedeutet:
Unsere React-Komponenten laufen auf dem Node.js-Server (oder am Edge).
Sie holen sich die Daten blitzschnell über interne Netzwerke direkt von unserer Contao-API.
Sie rendern fertiges HTML und senden null Kilobyte JavaScript an den Browser.
Wir können API-Keys (
API_SHARED_SECRET) sicher im Backend nutzen, ohne sie jemals dem Client preiszugeben.
Praxis & Code: Die Initialisierung des Projekts
Genug der Theorie. Lass uns unser neues Next.js-Projekt aufsetzen. Öffne dein Terminal in dem Verzeichnis, in dem dein Projekt leben soll (parallel zu deinem Contao-Backend-Ordner) und führe folgenden Befehl aus:
npx create-next-app@latest frontendNext.js wird dich nun durch einen interaktiven Setup-Prozess führen. Für ein professionelles, skalierbares Headless-Frontend antwortest du auf die Fragen wie folgt:
Would you like to use TypeScript? ->
Yes(Zwingend erforderlich! Wir wollen die JSON-Strukturen aus Contao strikt typisieren, um Laufzeitfehler zu vermeiden.)Would you like to use ESLint? ->
Yes(Für sauberen, konsistenten Code im Team.)Would you like to use Tailwind CSS? ->
Yes(Tailwind ist unser Utility-First-Framework der Wahl, um Designs rasend schnell und ohne tote CSS-Dateien umzusetzen.)Would you like to use
src/directory? ->Yes(Best Practice: So trennen wir Konfigurationsdateien wienext.config.tsoder.envsauber von unserem eigentlichen Quellcode.)Would you like to use App Router? (recommended) ->
Yes(Das Herzstück unserer Architektur.)Would you like to customize the default import alias (@/*)? ->
No(Der Standard@/für Importe wieimport Button from '@/components/Button'ist perfekt.)
Sobald die Installation abgeschlossen ist, navigierst du in den Ordner (cd frontend) und startest den Entwicklungsserver, optimalerweise direkt mit dem neuen, rasend schnellen Rust-basierten Compiler Turbopack:
npm run dev --turboDein Next.js-Frontend läuft nun unter http://localhost:3000. Du hast soeben das Fundament für eine Enterprise-Grade Headless-Architektur gelegt.

Die Enterprise Ordnerstruktur (Wie man Chaos vermeidet)
Ein frisch installiertes Next.js-Projekt sieht auf den ersten Blick sehr übersichtlich aus. Wenn du jedoch ein Headless CMS anbindest, wächst die Anwendung rasend schnell. Hunderte Inhaltselemente, verschiedene Layouts, Fetcher-Klassen und globale Typisierungen können das src/-Verzeichnis schnell in einen undurchdringlichen Dschungel verwandeln.
Aus Erfahrung in unzähligen Enterprise-Projekten hat sich eine strikte, semantische Trennung bewährt. Wir nutzen das App-Router-Paradigma (app/) ausschließlich für das Routing und das Laden von Daten. Die eigentliche visuelle Magie und Geschäftslogik lagern wir in spezialisierte Unterordner aus.
Praxis: Aufbau des src/-Verzeichnisses
Öffne deinen Code-Editor (z. B. VS Code) und lege innerhalb des src/-Verzeichnisses folgende Struktur an:
1src/
2├── app/ # (Wird von Next.js verwaltet: Routing, Layouts, Pages)
3├── components/ # Das Herzstück: Alle React-Komponenten
4│ ├── ui/ # "Dumme" Basis-Komponenten (Buttons, Inputs, Cards)
5│ ├── blocks/ # Komplexe CMS-Inhaltselemente (Hero, TextImage, TeamGrid)
6│ └── layout/ # Globale Struktur (Header, Footer, Navigation)
7├── lib/ # Geschäftslogik und API-Helfer
8│ ├── api/ # Contao Fetcher (die direkt mit unserem Backend sprechen)
9│ └── utils/ # Kleine Helfer-Funktionen (z.B. Datumsformatierung)
10├── types/ # TypeScript Interfaces (1:1 Abbild der Contao DTOs)
11└── styles/ # Globale CSS-Dateien (Tailwind globals.css)Warum diese strikte Trennung?
Das
components/ui/Verzeichnis: Hier leben Komponenten, die völlig ahnungslos vom CMS sind. Ein<Button />weiß nicht, ob sein Text aus Contao, Contentful oder einer statischen Datei kommt. Er nimmt nurPropsentgegen und rendert sich selbst. Diese Komponenten lassen sich perfekt in Storybook isolieren und testen.Das
components/blocks/Verzeichnis: Hier passiert die Magie. Wenn unser Next.js-Router später das JSON von Contao erhält (z. B.{"type": "text_image", "headline": "Hallo Welt"}), wird eine<TextImageBlock />Komponente aus diesem Ordner gerufen. Diese Block-Komponenten konsumieren die Contao-Daten, strukturieren sie und setzen sie dann mithilfe der kleinenui-Komponenten visuell zusammen.Das
lib/api/Verzeichnis: React-Komponenten sollten nicht selbstfetch()-Aufrufe schreiben. Das bläht den Code auf und macht ihn fehleranfällig. Wir lagern die Kommunikation mit der Contao-API komplett in dielibaus. Ein Aufruf im Server Component sieht dann später einfach so aus:const pageData = await getContaoPage(slug);.Das
types/Verzeichnis: Ein API-First-Ansatz ist ohne strikte Typisierung wertlos. Wenn du im Backend einen Key im JSON-Payload änderst, sollte dein Frontend beim nächsten Build rot aufleuchten. Hier definieren wir Interfaces wieexport interface ContaoPage { meta: {...}, content: {...} }, die exakt den Normalizern entsprechen, die wir in Teil 5 gebaut haben.
Indem wir diese Struktur von Tag 1 an konsequent durchziehen, garantieren wir, dass das Projekt auch in zwei Jahren noch von neuen Entwicklern verstanden und wartbar erweitert werden kann.

Tailwind CSS Integration & Das Headless Theming
Bei der Initialisierung unseres Projekts haben wir Tailwind CSS bereits installiert. Tailwind ist in der Headless-Welt der unangefochtene Platzhirsch. Warum? Weil wir uns in einer komponenten-basierten Architektur (React) keine Sorgen mehr um globale CSS-Kollisionen, BEM-Namenskonventionen oder riesige, ungenutzte style.css Dateien machen wollen.
Im Kontext eines Headless CMS müssen wir Tailwind jedoch strategisch aufstellen. Contao liefert uns oft Design-Entscheidungen der Redakteure mit – zum Beispiel einen Hintergrund-Farbwähler für ein Hero-Element ("backgroundColor": "primary"). Wir müssen Tailwind beibringen, diese CMS-Daten nahtlos in unser Frontend-Design-System (Design Tokens) zu übersetzen.
Praxis & Code: Die Tailwind-Konfiguration für Enterprise-Projekte
Standardmäßig scannt Tailwind nur die Dateien im app/-Ordner. Da wir unsere Architektur in Abschnitt 2 auf ein dediziertes components/-Verzeichnis umgebaut haben, müssen wir den Compiler anweisen, auch dort nach Klassen zu suchen. Zudem definieren wir unsere zentralen Markenfarben (Design Tokens).
Öffne die Datei tailwind.config.ts im Root-Verzeichnis deines Projekts und passe sie wie folgt an:
Datei: frontend/tailwind.config.ts
1import type { Config } from "tailwindcss";
2
3const config: Config = {
4 // 1. Dem Compiler sagen, WO er nach Tailwind-Klassen suchen soll
5 content: [
6 "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 "./src/components/**/*.{js,ts,jsx,tsx,mdx}", // WICHTIG: Unser neues Komponenten-Verzeichnis!
8 "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 ],
10 theme: {
11 // 2. Das Design-System (Design Tokens) erweitern
12 extend: {
13 colors: {
14 // Wir mappen diese Farben später auf die Auswahlen im Contao-Backend
15 brand: {
16 primary: "#0ea5e9", // Ein leuchtendes Cyan
17 secondary: "#f59e0b", // Ein kräftiges Amber
18 dark: "#0f172a", // Tiefes Schieferblau für Dark-Modes/Footer
19 light: "#f8fafc",
20 }
21 },
22 fontFamily: {
23 // Optionale Konfiguration für Next/Font (Google Fonts lokal gehostet)
24 sans: ['var(--font-inter)', 'sans-serif'],
25 },
26 // Wir definieren Container-Breiten zentral, damit alle CMS-Blöcke bündig sind
27 container: {
28 center: true,
29 padding: '1.5rem',
30 screens: {
31 '2xl': '1280px',
32 },
33 }
34 },
35 },
36 plugins: [
37 // Beliebte Plugins für Formulare und Typografie (bei Bedarf via npm installieren)
38 require('@tailwindcss/typography'),
39 ],
40};
41
42export default config;Die CMS-Verknüpfung: Dynamisches Styling
Wie verbinden wir das nun mit Contao? Stell dir vor, der Redakteur wählt im Contao-Backend für ein "Text-Bild-Element" die Hintergrundfarbe "Primary" aus. Deine API (die wir in Teil 5 gebaut haben) liefert folgendes JSON:
{
"type": "text_image",
"theme": "primary",
"headline": "Unsere Mission"
}In deiner React-Komponente (z. B. src/components/blocks/TextImageBlock.tsx) kannst du diesen Wert nun elegant auf dein Tailwind-Design-System mappen:
1// Ein simples Mapping-Objekt, das CMS-Strings in Tailwind-Klassen übersetzt
2const themeClasses = {
3 primary: "bg-brand-primary text-white",
4 secondary: "bg-brand-secondary text-black",
5 dark: "bg-brand-dark text-white",
6 light: "bg-brand-light text-slate-900",
7};
8
9export default function TextImageBlock({ data }: { data: any }) {
10 // Fallback auf 'light', falls der Redakteur nichts ausgewählt hat
11 const backgroundClass = themeClasses[data.theme as keyof typeof themeClasses] || themeClasses.light;
12
13 return (
14 <section className={`py-16 ${backgroundClass}`}>
15 <div className="container">
16 <h2>{data.headline}</h2>
17 {/* ... Rest der Komponente ... */}
18 </div>
19 </section>
20 );
21}Der Senior-Tipp: Nutze niemals String-Interpolation direkt für Tailwind-Klassen (wie bg-brand-${data.theme}). Tailwind scannt den Quellcode während des Build-Prozesses nach exakten, vollständigen Strings. Dynamisch zusammengebaute Strings werden nicht erkannt, und die Farbe fehlt im fertigen CSS! Nutze immer ein Mapping-Objekt (Dictionary) wie im obigen Beispiel.

Server Components vs. Client Components im Headless-Kontext
Bevor wir in den nächsten Tutorials die echten Contao-Daten abfragen, müssen wir das wichtigste mentale Modell des App Routers verinnerlichen. Wer Next.js 14 oder 15 meistert, denkt nicht mehr in "Frontend vs. Backend", sondern in "Server Components vs. Client Components".
Im Headless CMS Betrieb ist diese Trennung der Schlüssel zu extremer Performance und wasserdichter Sicherheit.
Die React Server Component (Das Standardverhalten)
In Next.js ist standardmäßig jede Datei im app/-Ordner eine Server Component (RSC).
Was passiert hier? Die Komponente wird ausschließlich auf dem Server (in einer Node.js Umgebung) ausgeführt.
Der Vorteil: Sie kann direkt Datenbanken abfragen oder über
fetchunsere Contao-API kontaktieren – inklusive geheimer API-Keys (process.env.CONTAO_API_KEY). Nichts davon landet jemals im Browser des Nutzers. Das JS-Bundle für den Client bleibt bei exakt 0 Byte für diese Komponente.
Die Client Component (Inseln der Interaktivität)
Sobald du Interaktivität benötigst – z. B. einen onClick-Listener für ein Accordion, React-Hooks wie useState oder Browser-APIs wie window.localStorage –, reicht eine Server Component nicht mehr aus. Hier fügst du in der ersten Zeile der Datei die Direktive 'use client'; hinzu.
Was passiert hier? Diese Komponente wird zwar serverseitig vorgerendert (für SEO), schickt aber ihr JavaScript an den Browser, wo sie "hydriert" wird und interaktiv wird.
Praxis & Code: Das Architektur-Muster
Das Erfolgsgeheimnis für Headless-Projekte: Halte deine Server Components an der Spitze des Baumes (Top-Level) und nutze Client Components nur so weit unten wie nötig (als "Blätter" am Baum).
Wir simulieren das: Eine Server Component (unsere Hauptseite) holt die strukturierten CMS-Daten und reicht sie als Props an eine kleine, interaktive Client Component (ein FAQ-Accordion) weiter.
1. Die Server Component (Datenbeschaffung):
Datei: frontend/src/app/page.tsx
1// Kein 'use client' - das ist eine Server Component!
2import FaqAccordion from '@/components/ui/FaqAccordion';
3
4// Unser API-Aufruf läuft sicher auf dem Server
5async function getContaoPageData() {
6 const res = await fetch('[https://api.deine-contao-domain.de/api/v1/page/home](https://api.deine-contao-domain.de/api/v1/page/home)', {
7 headers: {
8 // Der Key verlässt den Server nie!
9 'Authorization': `Bearer ${process.env.CONTAO_API_KEY}`
10 }
11 });
12
13 if (!res.ok) throw new Error('Fehler beim Laden der Contao-Daten');
14 return res.json();
15}
16
17export default async function HomePage() {
18 const data = await getContaoPageData();
19
20 return (
21 <main className="container py-20">
22 <h1 className="text-4xl font-bold mb-8">{data.routing.title}</h1>
23
24 {/* Wir übergeben nur das nötige CMS-Array an die Client Component */}
25 <FaqAccordion items={data.content.faqs} />
26 </main>
27 );
28}2. Die Client Component (Interaktivität):
Datei: frontend/src/components/ui/FaqAccordion.tsx
1'use client'; // WICHTIG: Macht dies zu einer Client Component
2
3import { useState } from 'react';
4
5// Wir definieren sauber die Typen, die das CMS liefert
6interface FaqItem {
7 id: number;
8 question: string;
9 answer: string; // HTML-String aus dem CMS
10}
11
12export default function FaqAccordion({ items }: { items: FaqItem[] }) {
13 // useState ist nur im Client verfügbar
14 const [openId, setOpenId] = useState<number | null>(null);
15
16 const toggle = (id: number) => {
17 setOpenId(openId === id ? null : id);
18 };
19
20 return (
21 <div className="space-y-4">
22 {items.map((item) => (
23 <div key={item.id} className="border border-slate-200 rounded-lg">
24 <button
25 onClick={() => toggle(item.id)}
26 className="w-full text-left p-4 font-semibold text-brand-dark flex justify-between"
27 >
28 {item.question}
29 <span>{openId === item.id ? '-' : '+'}</span>
30 </button>
31
32 {openId === item.id && (
33 <div
34 className="p-4 pt-0 text-slate-600 prose"
35 // Wir vertrauen hier auf Contao, da wir den String in Teil 6 sanitisiert haben!
36 dangerouslySetInnerHTML={{ __html: item.answer }}
37 />
38 )}
39 </div>
40 ))}
41 </div>
42 );
43}Die Genialität dieses Musters: Das schwere Paket an API-Logik, Fetch-Aufrufen und JSON-Parsing verbleibt vollständig auf deinem Next.js Server. Nur der absolute Minimum-Code – der Zustand für das Aufklappen des Accordions (useState) – wird in den Browser des Nutzers geladen. Das Ergebnis ist eine rasend schnelle Ladezeit (LCP) und maximale Sicherheit.

Fazit – Das Fundament steht
Mit diesem Setup haben wir ein hochgradig skalierbares, performantes Fundament für unser Headless CMS-Projekt gelegt. Wir haben Next.js mit dem mächtigen App Router initialisiert, uns für eine Enterprise-taugliche Ordnerstruktur entschieden, die uns auch in den nächsten Jahren nicht über den Kopf wächst, und Tailwind CSS so konfiguriert, dass es perfekt mit unseren CMS-Design-Tokens harmoniert.
Die Trennung zwischen Server Components und Client Components gibt uns die Kontrolle darüber, wo Daten geladen, aufbereitet und ausgeführt werden. Dank der Server Components wird unsere Architektur nicht nur blitzschnell, sondern wir schützen auch sensible Informationen wie den API_SHARED_SECRET, die niemals den Server verlassen.
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
Häufig gestellte Fragen (FAQ)
Technisch gesehen: Ja. Architektonisch gesehen: Bitte nicht. Der Pages Router (/pages) gilt mittlerweile als veraltet. Wenn du heute ein neues Enterprise-Projekt aufsetzt, ist der App Router (/app) der De-facto-Standard. Nur mit dem App Router profitierst du vollumfänglich von React Server Components, Streaming, verschachtelten Layouts und dem drastisch reduzierten JavaScript-Bundle für den Browser.
Warum legen wir unsere React-Komponenten nicht einfach mit in den app/ Ordner?
Next.js erlaubt das durch "Colocation" (Dateien, die nicht page.tsx heißen, werden nicht als Route interpretiert). In großen Headless-Projekten wird der app/-Ordner dadurch aber extrem unübersichtlich. Ein strikt getrennter src/components/-Ordner zwingt dich dazu, Routing-Logik (URLs, Parameter, API-Fetches) strikt von der UI-Logik (Buttons, Grids, Typografie) zu trennen. Das erhöht die Testbarkeit und Wartbarkeit massiv.
Das ist ein häufiger Kritikpunkt an Tailwind. In einer komponenten-basierten Architektur wie React ist das jedoch kein Problem. Anstatt 15 Tailwind-Klassen auf jeder Seite neu zu tippen, kapselst du sie einmal in deiner <Button /> Komponente. Dein eigentlicher Code bleibt sauber (<Button variant="primary">Klick mich</Button>). Alternativen wie CSS-in-JS (Styled Components) oder klassische SCSS-Module funktionieren ebenfalls, stoßen aber im App Router (da sie oft Laufzeit-JavaScript benötigen) schnell an Kompatibilitätsgrenzen mit Server Components. Tailwind ist hier der sicherste Weg.
Die Faustregel lautet: Nutze 'use client' ausschließlich, wenn deine Komponente eine der folgenden Interaktionen zwingend benötigt:
Browser-APIs (wie
window,document,localStorage,navigator)React State-Hooks (
useState,useReducer)React Lifecycle-Hooks (
useEffect,useLayoutEffect)Event-Listener (
onClick,onChange,onSubmit)
Datenabfragen an das CMS sollten im Idealfall immer ohne'use client'in einer Server Component stattfinden.
Dein nächster Schritt: Dynamisches Routing & Navigation
Das Frontend-Fundament ist gegossen. Server Components und Tailwind stehen bereit. Doch aktuell haben wir nur eine starre, fest programmierte Startseite (app/page.tsx).
Ein echtes CMS lebt von seiner dynamischen Seitenstruktur. Der Redakteur legt in Contao beliebig viele neue Seiten an ("Über uns", "Team", "Karriere"), verschachtelt sie und erwartet, dass das Frontend diese URLs automatisch verarbeitet und in einer globalen Navigation darstellt.
Im kommenden Teil 8: Dynamisches Routing & Navigation erwecken wir unseren Next.js Router zum Leben. Du wirst lernen:
Die Catch-All Route: Wie wir mit dem Konstrukt
app/[...slug]/page.tsxjede erdenkliche URL (z.B./unternehmen/standorte/berlin) abfangen und an Contao weiterleiten.Den Seitenbaum nachbauen: Wie wir den hierarchischen Contao-Seitenbaum 1:1 im Next.js-Frontend abbilden.
Dynamische Navigationen: Wir generieren aus dem JSON-Payload des CMS eine performante, verschachtelte Hauptnavigation, die serverseitig vorgerendert wird.
404 Handling: Wie wir elegant mit Seiten umgehen, die in der API nicht existieren oder offline genommen wurden.
Jetzt starten: Teil 8 – Dynamisches Routing & Navigation 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.


