Von 0 auf Headless-CMS in 6 Wochen

Von Boris Sander22. Dezember 2025
Von 0 auf Headless-CMS in 6 Wochen

Von 0 auf Headless-CMS in 6 Wochen

Ein technisches Abenteuer in sechs Akten


Prolog: Die dunklen Jahre

Es war ein grauer Novembermorgen, als der Anruf kam. Ein alter Bekannter – nennen wir ihn Thomas – klang am Telefon wie jemand, der gerade sein Auto in einen See gefahren hatte.

„Die Website ist wieder down."

Wieder. Dieses Wort hing in der Leitung wie der Geruch von verbranntem Kaffee. Wieder bedeutete: zum dritten Mal in diesem Monat. Wieder bedeutete: WordPress.

Thomas betrieb seit acht Jahren eine mittelständische Beratungsfirma. Seine Website lief auf WordPress – wie gefühlt 43% des gesamten Internets. Und wie gefühlt 43% aller WordPress-Installationen hatte auch seine ihre Eigenheiten entwickelt.

„Der Hosting-Provider sagt, es liegt am Cache-Plugin. Das Cache-Plugin sagt, es liegt am Theme. Das Theme sagt, es liegt an einem der 23 anderen Plugins. Und ich sage: Ich wollte doch nur einen Blogpost veröffentlichen."

Ich kannte diese Geschichten. Jeder, der lange genug in dieser Branche arbeitet, kennt sie.


Akt I: Das WordPress-Trauma

Woche 0 – Die Erkenntnis

Verstehen Sie mich nicht falsch: WordPress ist ein Wunderwerk der Software-Geschichte. Ein System, das es jedem ermöglicht, Inhalte im Internet zu veröffentlichen. Demokratisierung des Webs. Ein echter Meilenstein.

Aber irgendwann – und dieser Moment kommt bei jedem – fragt man sich: Warum fühlt sich das an wie Chirurgie mit einem Buttermesser?

Die Anekdoten sammeln sich über die Jahre wie Staub auf einem ungepflegten Server:

Die Geschichte von der unsichtbaren Änderung: Ein Kunde, Inhaberin eines Architekturbüros, wollte eine Telefonnummer auf ihrer Kontaktseite ändern. Eine Ziffer. Eine einzige Ziffer. Vier Stunden später hatte sie versehentlich das komplette Menü gelöscht, weil der Visual Composer beschlossen hatte, dass heute ein guter Tag wäre, sich anders zu verhalten als gestern.

Die Nacht der lebenden Updates: Ein mittelständischer Maschinenbauer – 200 Mitarbeiter, millionenschwere Aufträge – wacht eines Morgens auf und seine Website zeigt nur noch weiße Leere. Das automatische WordPress-Update hatte ein Plugin aktualisiert. Das Plugin war inkompatibel mit einem anderen Plugin. Das andere Plugin war inkompatibel mit dem Theme. Das Theme war inkompatibel mit der PHP-Version. Die PHP-Version war inkompatibel mit der Hoffnung, heute noch produktiv zu arbeiten.

Der Plugin-Friedhof: Eine Marketingagentur erbte eine WordPress-Installation mit 67 installierten Plugins. Davon waren 12 aktiv, 34 deaktiviert, 18 „irgendwie kaputt" und 3 wurden seit 2019 nicht mehr gewartet – hatten aber trotzdem noch Zugriff auf die Datenbank. Die Ladezeit der Startseite: 14 Sekunden. Die Ladezeit der Geduld des Kunden: -3 Sekunden.

Diese Geschichten sind keine Ausnahmen. Sie sind der Alltag.

Und so stand ich an jenem Novembermorgen, den Hörer noch in der Hand, und dachte: Es muss einen anderen Weg geben.


Akt II: Die Vision

Woche 1 – Der Architekt zeichnet

Was wäre, wenn wir das Prinzip umkehren würden?

Was wäre, wenn das Backend nur eines täte: Inhalte verwalten? Keine Plugins für Kontaktformulare. Keine Themes, die sich gegenseitig das Leben schwer machen. Keine Abhängigkeiten, die nachts heimlich Updates durchführen.

Nur Daten. Sauber strukturiert. Über eine API abrufbar.

Das Frontend? Das bauen wir selbst. Mit modernen Tools. Mit Type Safety. Mit der Kontrolle, die man haben sollte, wenn man Software schreibt, die länger als eine WordPress-Update-Zyklusdauer funktionieren soll.

Der Name für dieses Konzept existierte längst: Headless CMS.

Das CMS verliert seinen „Kopf" – das Frontend – und wird zu dem, was es sein sollte: ein leistungsfähiges Inhaltsverwaltungssystem. Nicht mehr, nicht weniger.

Aber welches? Der Markt ist voll von Optionen. Contentful, Strapi, Sanity, Prismic, Storyblok – die Liste liest sich wie das Namensverzeichnis einer Tech-Startup-Konferenz.

Und dann fiel mein Blick auf Directus.


Akt III: Die Entdeckung

Woche 1-2 – Directus: Der unerwartete Held

Directus kam unerwartet. Kein lautes Marketing, keine Milliarden-Bewertung, keine Keynotes mit Nebelmachinen und dramatischer Beleuchtung.

Stattdessen: Open Source. MIT-Lizenz. Self-hosted. Und – hier wurde ich hellhörig – native GraphQL-Unterstützung.

Nicht GraphQL als Afterthought. Nicht „Ja, wir haben auch eine GraphQL-API, irgendwo in den Einstellungen, aber eigentlich nutzt das niemand." Sondern GraphQL als First-Class Citizen.

Die erste Installation dauerte keine 15 Minuten:

docker-compose up -d

Mehr nicht. Kein PHP konfigurieren, keine Datenbank-Credentials in Dateien kopieren, die dann doch nicht die richtigen Berechtigungen haben, keine XML-Sitemap-Plugins installieren.

Ein Browser-Tab öffnen. localhost:8055. Einloggen. Fertig.

Die Admin-Oberfläche begrüßte mich wie ein gut organisiertes Arbeitszimmer: aufgeräumt, übersichtlich, ohne versteckte Ecken, in denen sich jahrelang nicht genutzte Einstellungen ansammeln.

Ich erstellte mein erstes Datenmodell: Eine Collection namens pages. Felder: title, permalink, content, published. Drag and Drop. Klick. Speichern.

Im GraphQL-Playground – ja, Directus hat einen eingebauten GraphQL-Playground, weil natürlich hat es einen – schrieb ich meine erste Query:

query {
  pages {
    title
    permalink
    content
  }
}

Die Antwort kam in Millisekunden. JSON. Sauber strukturiert. Exakt die Felder, die ich angefordert hatte. Nicht mehr, nicht weniger.

An diesem Punkt wusste ich: Das ist der Weg.


Akt IV: Der Stack

Woche 2-3 – Die Architektur nimmt Form an

Jede Reise braucht eine Karte. Jedes Gebäude einen Bauplan. Jedes Software-Projekt einen Stack.

Und dieser Stack sollte nicht nur funktionieren – er sollte elegant sein.

React Router 7: Der Dirigent

Warum nicht Next.js? Eine berechtigte Frage, die ich mir selbst stellte. Next.js ist der Platzhirsch, das Framework, das jeder kennt.

Aber React Router 7 hatte etwas, das mich sofort überzeugte: Type-safe Routes.

// routes/page.tsx
export async function loader({ params }: Route.LoaderArgs) {
  // params.permalink ist getypt. Automatisch. Ohne manuelles Interface.
}

Keine any-Types. Keine Runtime-Überraschungen. Der Compiler weiß genau, welche Parameter auf welcher Route existieren.

Dazu: Vite als Build-Tool. Hot Module Replacement, das so schnell ist, dass man manchmal glaubt, die Änderung hätte sich selbst vorgenommen. Kein Webpack-Konfigurationsalbtraum. Kein Bundle-Analysieren bis 3 Uhr morgens.

Apollo Client: Der Übersetzer

GraphQL ist elegant. Aber GraphQL ohne ordentlichen Client ist wie ein Orchester ohne Notenblätter.

Apollo Client übernahm diese Rolle. Nicht, weil es das populärste war – was es ist –, sondern weil es etwas konnte, das mir schlaflose Nächte ersparen würde: Normalisierte Cache-Verwaltung.

Einmal geladene Daten? Im Cache. Automatisch dedupliziert. Automatisch aktualisiert, wenn sich etwas ändert. Keine manuellen State-Updates, keine vergessenen Invalidierungen.

Und dann: Split Links.

const splitLink = ApolloLink.split(
  (operation) => isInternalOperation(operation),
  internalLink,
  externalLink  // → Directus
);

Der Apollo Client weiß, welche Anfragen wohin gehen. Interne API? Andere Richtung. Directus? Direkt zum CMS. Ein Router für GraphQL-Queries, elegant und unsichtbar.

Tailwind CSS 4: Der Stilist

CSS-in-JS hatte ich versucht. Styled Components, Emotion, die ganze Palette. Alles hat seinen Platz.

Aber Tailwind hatte etwas, das die anderen nicht hatten: Zero Runtime Overhead.

<div className="bg-primary-600 text-white p-4 rounded-lg shadow-md">

Kein JavaScript, das zur Laufzeit CSS generiert. Keine hydration mismatches. Keine Bundle-Size-Explosionen.

Version 4 brachte native CSS-Variablen. Das Design-Token-System wurde damit zur Selbstverständlichkeit:

:root {
  --primary-600: oklch(0.55 0.20 265);
}

OKLCH-Farbraum – perceptual uniform, zukunftssicher, und ehrlich gesagt einfach schöner als die Hex-Codes unserer Vorväter.

TypeScript: Das Sicherheitsnetz

An dieser Stelle muss ich nicht predigen. TypeScript ist längst der Standard für professionelle Frontend-Entwicklung.

Aber in Kombination mit GraphQL entfaltet es seine wahre Kraft.


Akt V: Die Magie der Fragmente

Woche 3-4 – Typsicherheit als Kunstform

Hier wird es technisch. Hier wird es schön.

GraphQL hat ein Feature, das unterschätzt wird: Fragmente.

fragment PageFields on pages {
  id
  title
  permalink
  content
  hero {
    headline
    subheadline
    backgroundImage {
      id
      filename_download
    }
  }
}

query GetPage($permalink: String!) {
  pages(filter: { permalink: { _eq: $permalink } }) {
    ...PageFields
  }
}

Ein Fragment ist ein wiederverwendbarer Baustein. Einmal definiert, überall verwendbar. Die Felder, die ein Component braucht, an einer Stelle beschrieben.

Aber das ist nur der Anfang.

Der GraphQL Code Generator nimmt diese Fragmente und erstellt daraus TypeScript-Types. Automatisch. Bei jedem Speichern.

pnpm run codegen

Und plötzlich hat Ihr Component typsichere Props:

import { PageFieldsFragment } from '~/directus/graphql';

interface HeroSectionProps {
  page: PageFieldsFragment;
}

export function HeroSection({ page }: HeroSectionProps) {
  // page.hero.headline ist getypt
  // page.hero.backgroundImage.filename_download ist getypt
  // Autocomplete funktioniert
  // Der Compiler beschwert sich, wenn Sie tippen "page.haro" statt "page.hero"
}

Die Bindung zwischen Backend und Frontend ist nicht mehr eine Hoffnung – sie ist ein Vertrag. Ein Vertrag, den der Compiler überwacht.

Ändern Sie ein Feld in Directus? Der nächste Codegen-Lauf zeigt Ihnen exakt, welche Components betroffen sind. Nicht zur Laufzeit, wenn der Kunde bereits anruft. Zur Entwicklungszeit, wenn Sie noch Kaffee haben.

Das ist keine Magie. Das ist Engineering.


Akt VI: First Light

Woche 5-6 – Der Weg in die Produktion

Der Code war geschrieben. Die Components standen. Die Inhalte waren in Directus gepflegt – mit einer Leichtigkeit, die selbst den skeptischsten Stakeholder überzeugt hätte.

Jetzt kam der Moment, der jedes Entwicklerherz schneller schlagen lässt: Deployment.

Das Dockerfile

# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run typecheck
RUN pnpm run build

# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
RUN adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/build ./build
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

Multi-Stage Build. Der Builder-Container installiert, kompiliert, optimiert. Der Production-Container enthält nur das Nötigste. Kein node_modules-Dschungel. Kein Dev-Dependencies-Gepäck.

Image-Größe: 180 MB. Startzeit: unter 2 Sekunden.

Azure Container Apps

Die Cloud sollte es sein. Nicht weil es modern klingt, sondern weil es praktisch ist.

Azure App Service bot, was wir brauchten:

  • Serverless Container: Kein Server-Management
  • Auto-Scaling: Mehr Traffic? Mehr Instanzen. Automatisch.
  • Blue-Green Deployment: Neue Version? Erst 10% Traffic. Dann 50%. Dann 100%. Kein Risiko.

Die Pipeline – Azure DevOps, YAML-basiert, so unsexy wie effektiv – nahm ihren Dienst auf:

trigger:
  - develop  # → DEV Environment
  - main     # → PROD mit Manual Approval

stages:
  - Build
  - Deploy DEV
  - Deploy PROD (mit Approval Gate)

Ein Push auf develop: Automatisches Deployment auf die Dev-Umgebung. Ein Merge auf main: Die Pipeline wartet geduldig, bis ein Mensch auf „Approve" klickt.

Der Moment

Es war ein Freitagabend – die traditionelle Zeit für Deployments, wie jeder erfahrene Entwickler weiß (nicht wirklich, aber manchmal muss man leben).

Die Pipeline lief durch. Grün. Grün. Grün.

Der Browser öffnete sich.

https://asora.systems

Die Seite lud. In 1,3 Sekunden. Ohne Plugin-Konflikte. Ohne weiße Bildschirme. Ohne mysteriöse PHP-Fehler.

First Light.

Der Health-Check-Endpoint antwortete:

{
  "status": "OK",
  "timestamp": "2025-12-20T19:42:00Z",
  "uptime": 123.45
}

Ich lehnte mich zurück.

Sechs Wochen. Von der Idee zur Production. Von 0 auf eine Architektur, die nicht nur funktioniert, sondern die man versteht. Die man warten kann. Die nicht nachts um 3 Uhr anruft, weil ein Plugin beschlossen hat, aufzuhören.


Epilog: Der Anruf

Ich weiß, er wird kommen, der Anruf. In spätestens drei Monaten klingelt mein Telefon. Thomas.

„Hey, ich wollte nur fragen... diese Headless-CMS-Sache, von der du erzählt hast..."

„Ja?"

„Mein WordPress ist letzte Woche wieder abgestürzt. Der Hoster will jetzt extra für PHP 8.2-Support."

Ich werde lächeln. Ganz subtil nur, aber innerlich platze ich vor Schadenfreude.

„Hast du nächste Woche Zeit? Ich zeig dir was."


Technisches Nachwort

Für die, die es genau wissen wollen – hier der finale Stack:

KomponenteTechnologieWarum
Frontend FrameworkReact 18Größtes Ecosystem, ausgereift
Routing & SSRReact Router 7Type-safe Routes, Vite-basiert
StylingTailwind CSS 4Zero Runtime, Design Tokens
Data LayerApollo Client 4Normalized Cache, Split Links
Headless CMSDirectusOpen Source, native GraphQL
Type SafetyTypeScript + CodegenCompile-Time-Garantien
Build ToolVite 6Blitzschnelle HMR
ContainerDocker Multi-StageKlein, sicher, schnell
HostingAzure App ServiceServerless, Auto-Scaling
DNSAzure DNSBrauchen wir eh für den Exchange-Server
CI/CDAzure DevOpsNative Azure-Integration

Ergebnis:

  • ✅ Lighthouse Score: 98
  • ✅ First Contentful Paint: 1,2 Sekunden
  • ✅ Build-Zeit: 45 Sekunden
  • ✅ Deployment-Zeit: 3 Minuten
  • ✅ Nächtliche Support-Anrufe: 0

Dieses Abenteuer ist keine Fiktion. Es ist Dokumentation.

Und das Beste daran: Sie können es nachbauen.


Boris Sander
Cloud Solution Architect
asora.systems

Dezember 2025