WordPress zu Static mit Claude Code: Wie ich diese Seite an einem Nachmittag neu gebaut habe
- Kategorie
- KI & lokale LLMs
- Veröffentlicht
- 11 Juli 2026
- Von
- Jacob Lloyd — mit KI-Unterstützung geschrieben, im Nachhinein
- Lesezeit
- 8 Min. Lesezeit
Kurz gesagt: Die Geschichte, wie genau diese Website an einem einzigen Nachmittag von einem KI-Coding-Assistenten neu gebaut wurde — von WordPress zu einem Ordner mit einfachen Dateien. Die neue Seite ist schneller, kostenlos im Betrieb und hat keine Login-Seite mehr, die Hacker angreifen könnten. Am Ende steht das Schritt-für-Schritt-Rezept, um dasselbe mit der eigenen Seite zu machen.
Diese Seite lief früher auf WordPress. Jetzt ist sie ein Ordner voller Markdown-Dateien und ein 183 Zeilen langes Python-Skript, gebaut aus einem WordPress-Export von einem KI-Coding-Agenten an einem Nachmittag. Hier ist, wie das ablief — und warum genau dieser Workflow den Laden bis heute am Laufen hält.
- Was es ist: ein von Grund auf gebauter statischer Site-Generator (Python + Jinja2), der WordPress ersetzt hat, plus ein einmaliger Importer für den alten Content
- Was es kostet: nichts. Keine Plugins, keine Theme-Lizenz, dasselbe günstige Shared Hosting wie vorher
- Was du brauchst: deinen WordPress-Export (eine WXR-Datei), deinen Uploads-Ordner und einen KI-Coding-Agenten — ich habe Claude Code verwendet
- Was am Ende dabei rauskommt: flache HTML-Dateien, keine Datenbank, keine Login-Seite, die per Brute-Force angegriffen werden kann
Was am Ende dabei rauskommt
Zuerst die Belege — die echte Terminal-Ausgabe von dem Tag, an dem der Neubau live ging:
$ python3 _build/build.py
Built 15 files (7 projects, 3 pages, 4 categories).
Fünfzehn HTML-Seiten waren zum Zeitpunkt der Migration die gesamte Seite: Startseite, vier Kategorieseiten, sieben Projekt-Beiträge, drei Info-Seiten. Alles vorgerendert, alles reine Dateien, null serverseitiger Code, der pro Request läuft.
Der WordPress-Export, aus dem das entstand, war 572 KB groß mit 123 Einträgen — Beiträge, Seiten, Bild-Anhänge und ein Haufen aufgegebener Entwürfe. Sieben echte Beiträge haben die Reise überstanden. Der Rest war Müll, den der Importer absichtlich übersprungen hat.
Und die Maschine, die das alles baut, passt in eine Tabelle:
| Teil | Zeilen |
|---|---|
| build.py (der Generator) | 183 |
| migrate.py (der einmalige Importer) | 166 |
| Templates (5 Seiten-Layouts) | 139 |
| Partials (Nav, Footer, Karte) | 51 |
| site.yaml (die gesamte Site-Konfiguration) | 36 |
| serve.sh (lokale Vorschau) | 12 |
| .htaccess (Serverregeln) | 57 |
| Gesamt | 644 |
Rechnet man das CSS (322 Zeilen) und die eine JavaScript-Datei (31 Zeilen — ein mobiles Nav-Toggle, das war's) dazu, liegt die ganze Seite unter 1.000 Zeilen. Kein node_modules, kein Build-Framework, kein Lockfile, mit dem man sich streiten muss.
Teil 1: Weg von WordPress
Ich habe davon nichts von Hand geschrieben. Ich habe Claude Code beschrieben, was ich wollte, und es hat den Importer geschrieben, dann den Generator, dann die Templates, während mein Job darin bestand, die Ausgabe zu prüfen und zu sagen: "Nein, mach das lieber so." Die Datei-Zeitstempel belegen das: vom Export in der Hand bis zu einer gebauten, durchklickbaren Seite in unter zwei Stunden. Ein Nachmittag, kein Wochenende.
Die Pipeline, bei der es gelandet ist:
Der Export und der Medien-Spiegel
WordPress exportiert alles über Werkzeuge → Exportieren → Alle Inhalte. Man bekommt eine WXR-Datei — XML mit Beiträgen, Seiten, Kategorien und Anhang-Einträgen darin. Das ist die einzige Eingabe für den Importer.
Die eine Sache, die der Export nicht enthält, sind die tatsächlichen Bilddateien, nur ihre URLs. Der Uploads-Ordner wird also separat geholt — per SFTP, Dateimanager oder einem einfachen wget-Spiegel — nach assets/uploads/, wobei dieselbe Jahr/Monat-Struktur beibehalten wird, die WordPress verwendet hat, damit die umgeschriebenen URLs auch aufgelöst werden können.
Was migrate.py tatsächlich macht
Der Importer läuft einmalig, ist aber gefahrlos wiederholbar. Keine XML-Bibliothek — nur Regex gegen den WXR-Text, was fragil klingt, bis man merkt, dass WordPress-Export-XML extrem regelmäßig aufgebaut ist und Grep-artiges Abgreifen deutlich weniger Code braucht als einen vollständigen Parser aufzusetzen. Für jeden Beitrag zieht er Titel, Slug, Datum, Body, Kategorie und Beitragsbild heraus und dann:
- Überspringt Müll — leere Beiträge und alles mit einem Titel wie "(draft)…" landet nie in content/
- Mappt Kategorien auf einen kurzen, sauberen Satz von Schlüsseln statt auf WordPress' unordentliche Nicenames
- Löst Beitragsbilder auf über die Anhang-ID, mit Fallback auf das erste hochgeladene Bild im Body
- Schreibt Medien-URLs um von
wp-content/uploads/…zu/assets/uploads/… - Schreibt interne Links um — alte Permalinks werden zu den neuen sprechenden URLs, anhand einer Slug-Map, die aus jedem Beitrag und jeder Seite gebaut wird
- Entfernt Gutenberg-Block-Kommentare und löscht dynamische Listing-Blöcke komplett — die Build-Stufe generiert Kategorielisten jetzt selbst, also würden die alten fest eingebackenen nur veraltet herumliegen
- Generiert Auszüge automatisch — Tags entfernen, auf 160 Zeichen kürzen, als Kartentext und Meta-Description verwenden
Der interessante Teil: Beiträge kommen auf der anderen Seite als bereinigtes WordPress-HTML in einer Markdown-Datei heraus, nicht in Markdown-Syntax konvertiert. Der Renderer lässt rohes HTML einfach durch, sodass mit dem alten Content nichts verloren geht — und neue Beiträge werden in reinem Markdown geschrieben. Beides baut problemlos aus demselben Ordner. Als ich zum ersten Mal eine ".md"-Datei öffnete, die zu 90 % aus HTML-Tags bestand, dachte ich, etwas sei kaputtgegangen. War es nicht.
Was build.py macht
Das ist der Teil, der für immer läuft. Es liest site.yaml für Navigation, Kategorien und Links, liest alles in content/ und rendert jede Datei über ein Jinja2-Template in einen echten Ordner mit einer index.html darin — saubere URLs ohne serverseitige Rewrite-Regeln nötig. Über die Seiten selbst hinaus erzeugt es:
- Eine Navigationsleiste und einen Footer, gebaut aus Daten — eine Kategorie oder Seite hinzufügen, und sie taucht von selbst auf
- Pro-Seite-Titel, Meta-Description, kanonische URL, Open-Graph- und Twitter-Tags
- sitemap.xml, robots.txt und ein Web-Manifest, bei jedem Lauf neu erzeugt
- Einen Aufräum-Durchgang, der zuerst veraltete Ausgabe löscht, damit ein umbenannter Beitrag keine tote Seite hinterlässt
- Null Laufzeit-Abhängigkeiten: selbst gehostete Fonts, inline SVG-Icons, die eine kleine JS-Datei, nichts von einem CDN
Einen Beitrag hinzuzufügen bedeutet: eine Markdown-Datei in content/projects/ ablegen, Build ausführen. Das ist der ganze Workflow.
Erst Vorschau, dann Deployment
Die lokale Vorschau ist ein einziges Skript — bauen, dann ausliefern. Jede Seite anklicken, bevor irgendetwas in die Nähe des echten Hosts kommt.
./serve.sh
# builds, then serves the whole site at http://localhost:8099
Das Deployment ist ein rsync-Push, geführt von einem Manifest, das genau auflistet, was ausgeliefert wird (HTML, CSS, JS, Bilder, Fonts, .htaccess) und was verweigert wird (die Python-Skripte, die Markdown-Quelle, der WordPress-Export, die Templates). Die Serverkonfiguration ergänzt Kompression, Cache-Header, Sicherheits-Header und eine harte Sperre gegen den direkten Abruf jeder Quelldatei — selbst wenn jemand weiß, dass der Generator existiert, gibt es nichts herunterzuladen.
Teil 2: Wie diese Seite jetzt gepflegt wird
Hier ist, was den Neubau erst richtig lohnenswert gemacht hat: Auch die Pflege der Seite läuft nicht mehr von Hand. Derselbe Agenten-Workflow, der sie gebaut hat, hält sie aktuell — einschließlich der Aktualisierung, die du gerade liest.
Diese Überarbeitung begann mit einem Audit-Schwarm. Eine Crew hat jede veröffentlichte Seite faktengeprüft und jeden ausgehenden Link angeklickt; eine andere hat Screenshots und harte technische Fakten darüber gesammelt, wie die Seite funktioniert. Alles floss in eine geschriebene Spezifikation: das hier bauen, keine Zahlen erfinden, niemals ein Geheimnis veröffentlichen.
Von dort aus fächerte sich die Arbeit auf — ein Plattform-Agent (Kategoriestruktur, eine zweite Sprache, Suchmaschinen-Sanitär), ein Designer und je ein Schreib-Agent pro Artikel, dieser hier eingeschlossen. Ein schnelles Modell entwirft jede Seite aus ihrer Faktendatei. Ein stärkeres Modell redigiert für Stimme und Genauigkeit. Dann versucht ein feindseliger Durchgang, alles zu zerlegen — Zahlen gegen die Quellfakten gegenprüfen, Links erneut anklicken, alles jagen, was halluziniert klingt. Oder, ehrlich gesagt, alles, was klingt, als hätte es eine KI geschrieben. Was in diesem Fall auch stimmt.
Gib deinem eigenen Agenten diesen Auftrag
Sitzt du auf einer alten WordPress-Seite, die du nicht ewig weiter flicken willst? Das ist das Rezept, das ich meinem gegeben habe. Beschreibe deinem Agenten jeden Schritt und lass ihn den Code schreiben.
- Exportieren. WordPress-Admin → Werkzeuge → Exportieren → Alle Inhalte. Leg die WXR-Datei dort ab, wo der Agent sie lesen kann, z. B.
_build/source.xml. - Die Medien spiegeln. Kopiere
wp-content/uploads/vom alten Host nachassets/uploads/, unter Beibehaltung der Jahr/Monat-Struktur. - Nach einem Migrations-Skript fragen. Die WXR-Einträge parsen, Entwürfe und Leerbeiträge überspringen, Kategorien auf eine kurze saubere Liste mappen, Beitragsbilder auflösen (Fallback auf das erste Inline-Bild), Upload-URLs und interne Links umschreiben, Gutenberg-Block-Kommentare entfernen, Auszüge automatisch generieren, Frontmatter-Markdown-Dateien schreiben.
- Nach einem Generator fragen. Eine Site-Konfiguration plus den Content-Ordner lesen, über Templates rendern (Base, Home, Kategorie, Beitrag, Seite, gemeinsame Nav/Footer/Karten-Partials), Ordner mit sprechenden URLs ausgeben, sitemap.xml, robots.txt und ein Web-Manifest erzeugen.
- Das Design von Grund auf neu bauen. Das CSS des alten Themes nicht portieren — ein sauberes Stylesheet, selbst gehostete freie Fonts, inline SVG-Icons, null externe Requests.
- Alles in der Vorschau prüfen. Ein einzeiliges Skript, das baut und dann lokal ausliefert. Jede Seite anklicken.
- Die Serverkonfiguration schreiben. Kompression, Cache-Header, Sicherheits-Header, Deny-Regeln für Quelldateien, ein Fallback für erweiterungslose URLs.
- Mit einem Manifest deployen. Die Ausgabe per rsync übertragen, dabei Quellordner und Skripte explizit ausschließen, dann prüfen, dass die Live-URL 200 zurückgibt.
- Den Export aufbewahren. Die WXR-Datei bleibt im Projekt — ein erneuter Lauf des Migrations-Skripts regeneriert den Content-Ordner sauber, falls du je einen frischen Import brauchst.
Die komplette Liste der Laufzeit-Abhängigkeiten, vollständig:
pip install jinja2 pyyaml markdown
Das war's.
Fallstricke
- Vergisst man, den Uploads-Ordner zu spiegeln, warnt einen nichts. Der Build läuft erfolgreich durch, während jedes Thumbnail einen 404 wirft. Auf die harte Tour gelernt.
- Ausgabe und Quelle teilen sich einen Ordner. Das gebaute HTML landet im selben Projekt-Root wie die Python- und Markdown-Quelle — die Deployment-Trennung besteht komplett aus der Allow/Deny-Liste. Fühlte sich anfangs verkehrt an; genau das hält das Deployment aber bei einem einfachen rsync.
- Eine Kategorie umzubenennen kann einen toten Ordner hinterlassen. Der Aufräumschritt hat die Kategorienamen direkt eingetippt. Benennt man eine in der Konfiguration um und vergisst diese Liste, bleibt der alte Ordner veröffentlicht und verwaist liegen.
- Daten sortieren als Strings, nicht als Daten. Kein Problem, solange jedes Datum als
YYYY-MM-DDgeschrieben ist. Vertippt man sich einmal, sinkt der neueste Beitrag still nach unten.