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:
| Komponente | Technologie | Warum |
|---|---|---|
| Frontend Framework | React 18 | Größtes Ecosystem, ausgereift |
| Routing & SSR | React Router 7 | Type-safe Routes, Vite-basiert |
| Styling | Tailwind CSS 4 | Zero Runtime, Design Tokens |
| Data Layer | Apollo Client 4 | Normalized Cache, Split Links |
| Headless CMS | Directus | Open Source, native GraphQL |
| Type Safety | TypeScript + Codegen | Compile-Time-Garantien |
| Build Tool | Vite 6 | Blitzschnelle HMR |
| Container | Docker Multi-Stage | Klein, sicher, schnell |
| Hosting | Azure App Service | Serverless, Auto-Scaling |
| DNS | Azure DNS | Brauchen wir eh für den Exchange-Server |
| CI/CD | Azure DevOps | Native 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