TYPO3 headless mit Next.js

Schritt für Schritt zur entkoppelten Architektur — mit den Stolpersteinen, die in der Doku nicht stehen

Wann headless — und wann nicht

Klassisches TYPO3, serverseitig mit Fluid gerendert, ist für die meisten Projekte die richtige Wahl — auch bei mir ist das die Regel, nicht die Ausnahme. Headless lohnt sich, wenn das Frontend eigene Anforderungen mitbringt: App-artige Interaktivität mit React, ein Frontend-Team mit eigenem Stack, mehrere Abnehmer für denselben Content oder ein Backend, das aus dem öffentlichen Netz verschwinden soll.

Ein Argument gewinnt dabei an Gewicht: Moderne Bibliotheken erscheinen heute zuerst als React-Komponenten. Ein 3D-Viewer mit Three.js, ein Chat-Interface über das AG-UI-Protokoll, Karten, Charts, interaktive Konfiguratoren — im Next.js-Frontend ist das ein Import und eine Komponente mit gepflegter Dokumentation. In serverseitig generiertes Fluid-HTML lässt sich dasselbe zwar einbauen, aber als Eigenbau gegen die Bibliothek statt mit ihr: eigenes Bundling, eigene Initialisierung, kein Komponenten-Modell. Je mehr solcher Bausteine ein Projekt braucht, desto stärker zieht die Rechnung Richtung headless.

Dieser Guide zeigt den zweiten Fall: TYPO3 liefert JSON, Next.js rendert. Alles Folgende läuft produktiv auf dieser Site — die Codebeispiele sind daraus destilliert, nicht aus einem Tutorial-Projekt. Was headless konzeptionell bedeutet und wann es sich rechnet, steht ausführlicher unter Headless TYPO3.

Warum Next.js und nicht Nuxt?

Das offizielle Referenz-Frontend für EXT:headless ist nuxt-typo3 — Vue und Nuxt. Wer dort startet, bekommt fertige Komponenten für die Standard-Inhaltselemente. Ich arbeite trotzdem mit Next.js: Das React-Ökosystem ist größer, Server Components und App Router passen gut zum Muster „ein JSON-Dokument pro Seite“, und die Frontend-Kompetenz vieler Teams liegt heute bei React. Die Anbindung an TYPO3 ist in beiden Fällen dieselbe JSON-API — dieser Guide zeigt den Next.js-Weg, weil es dafür deutlich weniger dokumentierte Praxis gibt.

Die Zielarchitektur

Zwei getrennte Systeme, eine klare Schnittstelle: TYPO3 läuft auf einer eigenen API-Domain (etwa api.example.org) und liefert pro Seite genau ein JSON-Dokument — Seiteneigenschaften, Meta-Daten, Navigation und alle Inhaltselemente. Next.js holt dieses JSON serverseitig, rendert HTML daraus und liefert es an den Browser. Das Frontend ist ein eigenes Deployment mit eigenem Release-Zyklus.

Und die Redakteure?

Die häufigste Sorge bei Headless-Projekten zuerst: Für Redakteure ändert sich nichts. Seitenbaum, Inhaltselemente, Rechte, Workspaces, Mehrsprachigkeit — das Backend bleibt exakt das TYPO3, das sie kennen.

Die Schnittstelle ist bewusst schlicht: fertig aufbereitetes JSON über HTTP, ein Dokument pro Seite. Kein GraphQL-Layer, kein Query-Design, keine Schema-Pflege — das Frontend fragt eine URL an und bekommt alles, was die Seite braucht.

Wichtig: eine dedizierte Subdomain für die API, nicht ein Pfad wie example.org/api — Letzteres führt mit EXT:headless zu schwer diagnostizierbarem Verhalten.

Schritt 1: TYPO3 als JSON-API

Die Extension friendsoftypo3/headless übernimmt die komplette JSON-Ausgabe — Seitenbaum, Navigation, Inhaltselemente, Mehrsprachigkeit inklusive Fallbacks:

composer require friendsoftypo3/headless

Ab TYPO3 v13 wird die Extension als Site-Set eingebunden. Das eigene Site-Set deklariert headless als Abhängigkeit:

# Configuration/Sets/MySite/config.yaml
name: my/site
dependencies:
  - friendsoftypo3/headless

Schritt 2: Das JSON verstehen — und eigene Inhaltselemente

Jedes Inhaltselement kommt in einer einheitlichen Hülle an, die eigentlichen Felder liegen unter content:

{
  "id": 42,
  "type": "text",
  "colPos": 0,
  "appearance": { "layout": "default", "frameClass": "default" },
  "content": { "header": "…", "bodytext": "<p>…</p>" }
}

Diese Einheitlichkeit ist der Grund, warum das Frontend später mit einem einzigen Dispatcher auskommt. Eigene Inhaltselemente hängen sich per TypoScript in denselben Vertrag ein — lib.contentElementWithHeader liefert die Hülle samt Header-Feldern, darunter definieren Sie nur noch die eigenen Felder:

tt_content.myelement =< lib.contentElementWithHeader
tt_content.myelement {
  fields {
    content {
      fields {
        bodytext = TEXT
        bodytext {
          field = bodytext
          parseFunc =< lib.parseFunc_RTE
        }
      }
    }
  }
}

Das parseFunc ist kein Detail: Es löst interne t3://-Links in echte URLs auf. Ohne diese Zeile liefert die API RTE-Rohtext mit toten Links. Mehr passiert auf TYPO3-Seite mit Links nicht — im JSON steht danach ein reguläres <a href>. Wie daraus im Frontend Client-Navigation ohne Full-Page-Load wird, zeigt Schritt 3.

Schritt 3: Das Next.js-Frontend

Auf der Next.js-Seite (App Router) reichen zwei Routen: app/page.js für die Startseite und eine Catch-all-Route für alles andere. Die Route holt das JSON der angefragten Seite und rendert die Inhaltselemente:

// app/[...slug]/page.js
export default async function Page({ params }) {
  const { slug } = await params
  const res = await fetch(
    `${process.env.NEXT_PUBLIC_DOMAIN_HEADLESS}/${slug.join('/')}`,
    { cache: 'force-cache' }
  )
  const page = await res.json()
  return page.content.colPos0.map((ce) => <CE ce={ce} key={ce.id} />)
}

generateMetadata() holt dasselbe JSON für Title und Meta-Description — Next.js dedupliziert die beiden Fetches automatisch.

Das Herzstück ist ein einziger Dispatcher: eine Komponente, die über ce.type verzweigt. Ein neues Inhaltselement im Backend heißt im Frontend genau ein neuer case plus eine Komponente. Alles Unbekannte fällt auf eine sichtbare Fallback-Komponente zurück — so sehen Sie im Entwicklungsmodus sofort, welches Element noch keine Entsprechung hat.

Für RTE-Inhalte hat sich html-react-parser bewährt: Das HTML aus bodytext wird geparst, interne Links werden dabei zu Next.js-<Link>-Komponenten (Client-Navigation ohne Full-Page-Load), externe bekommen rel="noopener noreferrer".

Schritt 4: Container und mehrspaltige Layouts

Mehrspaltige Layouts über b13/container funktionieren headless ohne eigenen Adapter — mit einer Eigenheit: Die API liefert Container und Kind-Elemente als flache Liste, gruppiert nach colPos. Kinder erkennen Sie an colPos-Werten ab 200 und dem Feld tx_container_parent, das auf die ID des Containers zeigt. Das Frontend verschachtelt sie in einem Vorverarbeitungsschritt wieder unter ihren Container; die Container-Komponenten (zwei Spalten, drei Spalten) rendern dann rekursiv ihre Kinder durch denselben Dispatcher. Ein Nachmittag Arbeit, einmalig — danach ist jedes neue Container-Layout nur noch eine weitere Komponente.

Schritt 5: Bilder

Bilder liefert die API als fertige Objekte: öffentliche URL auf die verarbeitete Datei (Crops aus dem Backend bereits angewendet), Abmessungen, Alt-Text. Auf Wunsch erzeugt TYPO3 zusätzliche Varianten — WebP, Retina, Platzhalter in niedriger Auflösung. Im Frontend übernimmt next/image die Auslieferung; dafür muss die TYPO3-Domain in next.config.mjs unter images.remotePatterns für den /fileadmin/-Pfad freigeschaltet sein.

Ein Punkt, der in Headless-Projekten gern durchrutscht: Der Alt-Text steht im JSON unter properties.alternative — die Frontend-Komponente muss ihn auch verdrahten. Gepflegte Alt-Texte im Backend nützen nichts, wenn die Komponente alt="" rendert.

Schritt 6: Caching — der Teil, den Tutorials weglassen

Einfache Headless-Setups laden den Content bei jedem Seitenaufruf neu. Das offizielle nuxt-typo3 arbeitet genauso — sein API-Client ist ein dünner Fetch-Wrapper ohne Server-Cache —, und ein Next.js-Setup ohne Caching-Strategie ebenfalls: Jeder Aufruf fragt TYPO3 an, ausgeliefert wird mit no-store-Headern — kein Browser-Cache, kein Back-Forward-Cache, unnötige Last auf dem Backend. Für Content-Sites ist striktes Caching die bessere Antwort: statisch rendern.

Dafür braucht es zwei Dinge. Erstens cache: 'force-cache' an jedem serverseitigen CMS-Fetch — ein einziger ungecachter Fetch irgendwo im Baum (auch im Footer) kippt die ganze Route zurück in dynamisches Rendering. Zweitens export const dynamic = 'force-static' in der Catch-all-Route, weil Next.js sie sonst trotzdem pro Request rendert. Unbekannte URLs werden beim ersten Aufruf generiert und dann aus dem Cache bedient.

Hartes Caching hat aber eine Konsequenz: Es braucht einen gezielten Weg, den Cache wieder zu leeren — sonst zeigt die Site nach jeder Redaktionsänderung den alten Stand.

Publish invalidiert gezielt

Dieser Weg ist On-demand-Revalidation: TYPO3 meldet beim Publish per Webhook die betroffenen Seiten an das Frontend, Next.js invalidiert gezielt deren Cache und ruft die Seite einmal selbst auf — der Redakteur sieht die Änderung beim nächsten Reload, der Rest der Site bleibt unberührt im Cache. Genau so läuft es auf dieser Site: ein DataHandler-Hook in TYPO3, ein Route Handler in Next.js, ein gemeinsames Secret. Zwei Details entscheiden über die Zuverlässigkeit: revalidateTag markiert nur als veraltet und liefert kein Ergebnis — erst der kontrollierte Selbst-Aufruf macht daraus ein überprüfbares Resultat — und die Invalidierung pro Seite läuft über den Pfad, weil die Catch-all-Route ihre Seiten-ID zum Fetch-Zeitpunkt nicht kennt. Die komplette Umsetzung — Hook, Route Handler, Verifikation — steht im Guide On-demand revalidation: TYPO3 + Next.js.

Eine Eigenheit gehört trotzdem ins Deploy-Skript: Next.js persistiert gecachte API-Antworten im Build-Verzeichnis, und ein neuer Build räumt sie nicht automatisch weg. Vor dem Build das Verzeichnis löschen — dann startet jedes Deployment mit frischem Content, und der Rebuild bleibt das Sicherheitsnetz für alles Site-Weite.

Vier Stolpersteine aus der Praxis

Redirects gehören an eine Stelle

In einer entkoppelten Architektur konkurrieren drei Orte um Redirects: TYPO3, der Webserver, das Frontend. Entscheiden Sie sich für einen. Bei mir liegen SEO-Redirects zentral in der Next.js-Konfiguration — versioniert im Git des Systems, das die Anfrage zuerst sieht.

Vorschau für Redakteure

Die Vorschau versteckter Seiten funktioniert über Backend-Cookies — und Cookies brauchen eine gemeinsame Root-Domain. api.example.org neben example.org funktioniert mit passender cookieDomain-Konfiguration; völlig getrennte Domains für Backend und Frontend funktionieren nicht ohne Zusatzaufwand. Das ist ein Argument bei der Domain-Planung, nicht erst beim Go-live.

Sitemap-URLs prüfen

Die XML-Sitemap generiert TYPO3 — das Backend kennt aber erstmal nur die eigene API-Domain. Ohne Konfiguration der Frontend-Basis stehen API-URLs in der Sitemap. Nach dem Go-live als Erstes prüfen.

Formulare funktionieren — anders als gewohnt

EXT:form läuft headless weiter, nur die Arbeitsteilung verschiebt sich: TYPO3 liefert die Formular-Definition als JSON — Felder, Typen, Validierungsregeln —, das Frontend baut daraus das Markup und schickt die Eingaben per POST zurück. Validierungsfehler kommen strukturiert als JSON zurück, nicht als fertige HTML-Seite; die Fehlerdarstellung ist damit Frontend-Aufgabe. Das ist der Bereich mit den meisten Community-Fragen zur Extension.

Das Ganze in echt

Diese Site läuft exakt so: TYPO3 auf einer eigenen API-Domain, EXT:headless, Next.js mit App Router, ein Dispatcher, statisches Rendering mit Rebuild pro Deploy. Wie das Projekt insgesamt aufgebaut ist — inklusive KI-Anbindung per RAG — steht unter dieser Referenz; die Technologie-Seiten zu TYPO3 und Next.js vertiefen die beiden Enden der Architektur. Wer nach dem statischen Rendern noch dynamische Antworten streamen will, findet den Weg dahin im Guide HTTP-Streaming in TYPO3.

Kontakt

Klingt nach Ihrem Projekt?

Sprechen wir unverbindlich darüber.

Antwort innerhalb 24h — in der Regel deutlich schneller.