Alle Beiträge

Hinter den Kulissen: Wie hello-eme.ch gebaut, getestet und ausgeliefert wird

Monorepo, CI-Pipeline, Deployment mit Coolify, eigene Payload-Plugins und ein MCP-Server, über den KI-Assistenten die ganze Website pflegen können – ein Blick unter die Haube.

· 4 Min. Lesezeit

KI-generiert
Illustration einer Deployment-Pipeline: commit, ci, preview, test und production als verbundene Stationen, darüber ein KI-Funke, der über MCP ins CMS schreibt.
Hinweis: Dieser Beitrag wurde mit Claude (KI-Assistent von Anthropic) erstellt und über den MCP-Server dieser Website direkt ins CMS geschrieben – Text, Übersetzung und Titelbild inklusive. Mehr dazu weiter unten.

Diese Website sieht schlicht aus – und das soll sie auch. Darunter steckt aber ein kleines, ziemlich sorgfältig gebautes System. In diesem Beitrag zeige ich, wie es aufgebaut ist: vom Setup über die Pipeline bis zum Deployment, dazu die Plugins und der MCP-Server, über den dieser Text entstanden ist.

Das Setup: ein Monorepo für alles

Alles lebt in einem Nx-Monorepo mit npm Workspaces:

  • Website: SvelteKit mit Svelte 5 (Runes), TypeScript und Tailwind CSS v4
  • CMS: Payload CMS 3 auf Next.js, mit SQLite als Datenbank
  • Design-System: ein gemeinsames Paket @hello-eme/ui mit Svelte-Komponenten und Design-Tokens
  • Plugins: eigene, wiederverwendbare Payload-Plugins als separate Pakete

Ein zweites Website-Paar (Seite + CMS) für ein weiteres Projekt teilt sich dasselbe Design-System und re-brandet nur über ein paar Farb-Tokens.

Ein paar Grundsätze ziehen sich durch alles:

  • Typsicher von vorne bis hinten: strict, kein any, keine Non-Null-Assertions. Die Typen der Inhalte werden direkt aus dem CMS generiert, nie von Hand geschrieben.
  • Nur Tokens: Farben, Grössen und Schatten gibt es ausschliesslich als Design-Tokens. Ein Skript prüft bei jedem Commit, dass im Markup keine Rohwerte oder willkürlichen Tailwind-Klassen landen.
  • Dark Mode in den Tokens: Farben sind als light-dark() definiert – die Seite folgt dem Betriebssystem, lässt sich aber auch umschalten.
  • Barrierefreiheit (WCAG 2.2 AA): semantisches HTML, sichtbarer Fokus, Kontrast von mindestens 4.5:1 in beiden Farbmodi.
  • Zweisprachig: Deutsch unter /, Englisch unter /en/, sauber mit hreflang verknüpft.

Inhalte: ein Seitenbaukasten statt fixer Templates

Jede Seite ist eine Liste von Blöcken – Hero, Text, Projektliste, Kontaktformular, Newsletter und so weiter. Jeder Block im CMS hat eine eigene Svelte-Komponente auf der Website; fehlt eine, ist das ein Typfehler und fällt beim Build auf.

Dazu kommen Blog-Beiträge, Projekte und der Bereich Kreativ für Gedichte, Texte und Fotos. Alles teilt sich eine Tag-Sammlung, es gibt Entwürfe, geplantes Veröffentlichen, eine Live-Vorschau direkt im Editor, automatische Weiterleitungen bei geänderten Slugs, eine Suche, Sitemap, RSS-Feeds und automatisch generierte Vorschaubilder für Social Media.

Die Pipeline: nichts geht ungeprüft raus

Git-Hooks fangen das Gröbste schon lokal ab: Vor jedem Commit laufen Prettier, ESLint und der Token-Check, vor jedem Push Lint, Typprüfung und Tests der betroffenen Projekte. Commit-Messages folgen Conventional Commits.

In GitHub Actions läuft dann bei jedem Pull Request und jedem Merge auf main:

  1. Formatierung und Design-Token-Check
  2. nx affected für Lint, Typprüfung, Unit-Tests und Build – nur für das, was sich tatsächlich geändert hat
  3. Ein Migrations-Check: Hat sich das CMS-Schema geändert, ohne dass eine Migration dazu existiert, schlägt der Build fehl. Produktion führt nämlich nur Migrationen aus – eine vergessene Migration würde sonst erst beim Deployment auffallen.
  4. End-to-End-Tests mit Playwright: Das CMS wird mit Beispielinhalten befüllt, dann prüft Playwright jede Route – inklusive axe-Accessibility-Checks im hellen und im dunklen Modus.

Deployment: einmal bauen, überall ausliefern

Website und CMS laufen als Docker-Container auf einem eigenen Server, verwaltet mit Coolify. Es gibt drei Umgebungen:

  • Preview: Jeder Pull Request bekommt seine eigene Umgebung unter einer eigenen Subdomain, inklusive einer Kopie der Test-Inhalte. Der Link landet automatisch als Kommentar im PR, und beim Schliessen wird alles wieder aufgeräumt.
  • Test: Jeder Merge auf main baut die Images, taggt sie mit dem Commit-Hash und rollt sie auf die Testumgebung aus.
  • Produktion: Ein Klick auf "Deploy production" – das geht auch vom Handy aus – erzeugt ein Release mit der nächsten Versionsnummer, wartet auf die Freigabe und rollt aus.

Der wichtigste Punkt dabei: Produktion baut nie neu. Sie bekommt exakt die Images, die vorher auf Test liefen, nur mit einem neuen Tag. Was getestet wurde, ist also auch das, was live geht. Nach jedem Deployment prüft ein Smoke-Test die Health-Endpoints und die Startseite. Die CMS-Datenbank wird täglich gesichert.

Eigene Plugins

Payload lässt sich gut über Plugins erweitern. Neben den offiziellen (Weiterleitungen, MCP, E-Mail) habe ich ein eigenes geschrieben: @hello-eme/payload-plugin-medium.

Es importiert Beiträge von Medium als Entwurf – per Post-URL (über den RSS-Feed oder die öffentliche Seite) oder über die HTML-Datei aus dem Medium-Export, die auch Entwürfe und Member-only-Beiträge abdeckt. Text, Überschriften, Listen, Links, Bilder, Video-Embeds und Code-Blöcke werden in Payloads Rich-Text-Format übersetzt, Bilder landen direkt in der Mediathek. Das Plugin ist bewusst generisch gehalten und kann in jedem Payload-Projekt verwendet werden.

MCP: die Website per KI-Assistent pflegen

Der spannendste Teil zum Schluss. Das CMS stellt einen MCP-Server bereit (Model Context Protocol). Damit können KI-Assistenten wie Claude oder ChatGPT alles bearbeiten, was die Website zeigt: Seiten mit allen Blöcken, Beiträge, Projekte, Kreativ-Werke, Medien, Tags, Weiterleitungen und Einstellungen.

Ein paar Details, die mir wichtig waren:

  • Standard-Anmeldung per OAuth 2.1 mit PKCE und dynamischer Client-Registrierung. In einer App reicht es, die Server-URL einzutragen; danach meldet man sich im CMS an und gibt den Zugriff frei.
  • Abgestufte Rechte: Lesen, Entwürfe, Veröffentlichen und Löschen werden einzeln freigegeben. Jede Verbindung erscheint im CMS als eigener Schlüssel und lässt sich jederzeit widerrufen.
  • Entwürfe zuerst: Neue Seiten, Beiträge und Werke landen immer als Entwurf. Live geht erst etwas, wenn es ausdrücklich veröffentlicht wird.
  • Markdown als Sprache: Rich Text wird als Markdown gelesen und geschrieben – das verstehen Sprachmodelle am besten. Was Markdown nicht abbilden kann, kommt als JSON und geht nicht verloren.
  • Privates bleibt privat: Kontaktnachrichten, Newsletter-Abonnenten und CMS-Benutzer sind über MCP bewusst nicht erreichbar.

Und ja: Genau dieser Beitrag ist so entstanden. Claude hat den Code gelesen, den Text auf Deutsch und Englisch geschrieben, das Titelbild gezeichnet und alles über den MCP-Server als Entwurf ins CMS gelegt. Durchgelesen und freigegeben habe ich ihn danach selbst.

Fazit

Für eine persönliche Website ist das alles vielleicht etwas viel. Aber genau das macht Spass: ein System, bei dem Qualität nicht von Disziplin abhängt, sondern von Checks, die automatisch laufen – und bei dem das Schreiben am Ende trotzdem so einfach ist wie eine Nachricht an einen Assistenten.