zum Blättern · Enter öffnet · Esc schließt

Kontakt aufnehmen.

Projekte

Migration: 20 Jahre Joomla nach Astro + Directus.

Eigenprojekt30. Juli 2026

Bereich
Tourismus
Umsetzung
Juni bis Juli 2026 bis zum Cutover, danach laufend
Kompetenz
Webentwicklung · SEO
Stack
Astro · Directus · PostgreSQL · Cloudflare · Python · Docker · n8n · Git

Ergebnis

  • 11.660 statische Seiten in vier Sprachen aus einem EOL-CMS von 2005
  • Cutover ohne Rankingverlust: legacy-URLs 1:1 auf neue IDs abgebildet
  • Bildauslieferung 31 bis 57 Prozent leichter durch Format-Verhandlung im Browser
  • Maschinenlesbare Tour-API und llms.txt für KI-Suche — vor der Konkurrenz
  • HTML-Fehler über den ganzen Katalog von 1.285 auf 21 gesenkt

In vier Sätzen: Ein Reiseveranstalter betreibt seit 2005 ein Portal auf Joomla 1.0 — einem CMS, dessen letzter Sicherheitspatch rund fünfzehn Jahre zurückliegt, mit 2.094 Artikeln, 11.247 Übersetzungszeilen und vier Sprachen in gemischten Zeichenkodierungen. Daraus ist eine Headless-Architektur geworden: Inhalte in Directus, Ausgabe als statisches HTML über Astro, ausgeliefert von Cloudflare — 11.660 Seiten, die ohne Datenbankabfrage im Auslieferungsweg entstehen. Der schwierige Teil war nicht der neue Stack, sondern die Daten: In zwanzig Jahren sammelt ein CMS Eigenheiten, die kein Import-Skript erwartet, und ein falsch verstandenes Bildfeld oder eine stillschweigend abgeschnittene Redirect-Datei kostet mehr Sichtbarkeit als jeder Relaunch-Fehler im Design. Dieser Text beschreibt die Architektur, die Datenfallen und die Automatisierung drumherum — ausführlich, weil genau diese Details den Unterschied machen.

Hinweis: Dies ist ein eigenes Projekt, das ich baue und betreibe — kein Kundenauftrag. Alle Zahlen stammen aus dem laufenden System, nicht aus einer Planung. Wo Wissensbasis und Code auseinanderliefen, steht der Code.

Die Ausgangslage: ein CMS von 2005

Das alte Portal lief auf Joomla 1.0.12. Diese Version stammt aus 2006, ist seit Jahren End-of-Life und bekommt keine Sicherheitspatches mehr. Sie lief auf einem klassischen Shared-Hosting mit PHP und MySQL, alle Inhalte in Tabellen mit dem Präfix jos_.

Was in zwanzig Jahren zusammenkam:

Tabelle Zeilen Was drin ist
jos_content 2.094 Artikel: Programme, Berichte, Ratgeber, Nachrichten
jos_jf_content 11.247 Übersetzungen (JoomFish), teils Maschinenübersetzung von ~2010
jos_eventlist_locate 138 die Touren — nicht als Artikel, sondern als Datensätze
jos_eventlist_dates 317 Termine mit Preis und Guide
jos_eventlist_categories 23 Reiseziele
jos_content2rez 2.344 Altbestand eines Buchungssystems — nicht importieren

Dazu ein Zoo aus Erweiterungen, von denen jede ihr eigenes Datenformat mitbringt: EventList für den Tourkalender, JoomFish für Mehrsprachigkeit, ein selbstgebauter SEF-Patch für die URLs, FacileForms für Anfragen, dazu JCE, JComments und JoomMap. Im Text stecken Shortcodes wie {mosimage} oder {events_list/get_dates/locid=X}, die ein Import-Skript verstehen muss, sonst landen sie als sichtbarer Müll auf der neuen Seite.

Alt: ein Server, alles darauf Shared Hosting (PHP + MySQL) Joomla 1.0.12 · EOL seit ~2011 jos_content · 2.094 Artikel als eine Textwand (Monolith) jos_jf_content · 11.247 JoomFish: Übersetzung als Zeile jos_eventlist_* · 138 / 317 / 23 Touren, Termine, Reiseziele /images/ im Dateibaum · cp1251 + utf8 gemischt Erweiterungen EventList · JoomFish SEF-Patch · FacileForms JCE · JComments · JoomMap jede mit eigenem Datenformat Shortcodes im Text {mosimage} {events_list/get_dates/…} {anketa/get_form} Seitenaufruf PHP rendert live DB-Abfragen pro Besuch Browser HTML ein Ausfall des Servers = Seite offline Sicherheitslücken ohne Patch
Das alte System: Inhalte, Erweiterungen und Auslieferung auf einer Maschine. Jeder Seitenaufruf löst Datenbankabfragen aus, jede Lücke im CMS ist direkt erreichbar.

Das eigentliche Problem war nicht die Technik, sondern was sie unmöglich machte. Ein Artikel war eine Textwand: Programm, Leistungen, Preise, Termine und Bilder als ein HTML-Feld. Für vier Sprachen heißt das vier Textwände, die auseinanderlaufen — jemand korrigiert den Preis im ukrainischen Text und vergisst den deutschen. Und die Frage «welche Touren haben keinen gültigen Preis?» ist gegen eine Textwand nicht beantwortbar.

Warum Astro und Directus

Die naheliegende Option wäre WordPress mit ACF und WPML gewesen. Drei der vier regionalen Wettbewerber fahren genau das. Genau deshalb nicht: Ein WordPress-Stack hätte dieselbe Kopplung von Inhalt und Ausgabe reproduziert, nur in moderner Verpackung, und die Ladezeiten wären im Feld der Konkurrenz geblieben.

Die Entscheidung fiel auf Headless: Inhalte in einem CMS, das nichts über die Darstellung weiß, Ausgabe als vorab generiertes HTML.

  • Directus als CMS auf eigenem Server, PostgreSQL darunter. Es erzeugt keine Seiten, es hält strukturierte Daten und liefert sie über eine API.
  • Astro als Generator: Er zieht die Inhalte beim Build und schreibt fertige HTML-Dateien. Im Auslieferungsweg gibt es danach kein PHP und keine Datenbank.
  • Cloudflare Pages als Auslieferung: statische Dateien vom Content-Delivery-Netz.

Der Nebeneffekt ist der wichtigste: Wenn der CMS-Server ausfällt, bleibt die Seite online. Sie ist ja nur eine Sammlung von Dateien. Ein Angriff auf das CMS erreicht die öffentliche Seite gar nicht, weil dort nichts läuft, was man angreifen könnte.

Die Zielarchitektur

Vier Bausteine, die bewusst getrennt sind, damit jeder für sich ausfallen oder umgezogen werden kann.

Neu: Inhalte, Build und Auslieferung getrennt Hetzner VPS (CX22, Helsinki) Docker · 2 vCPU · 4 GB RAM Directus 11 Inhalte, Rechte, Assets-API PostgreSQL + PostGIS Geodaten für Wegpunkte Caddy: TLS automatisch Backup = ein Postgres-Dump Cloudflare R2 Originalbilder, 4,3 GB und wachsend S3-kompatibel, EU-Region GitHub Code + Actions Build · Linkcheck · HTML-Prüfung Qualitätstore vor dem Deploy Astro-Build (SSG) holt JSON aus Directus rendert 11.660 HTML-Dateien 4 Sprachen × 2.913 Seiten erzeugt Sitemaps, API, llms.txt Bilder werden NICHT mitgebaut Cloudflare Pages statische Dateien am Edge _redirects: legacy-Pfade Bulk Redirects: ID-Dedups kein PHP, keine DB im Weg Besucher HTML sofort Bilder aus R2 JSON Deploy Bilder direkt an den Browser, über CDN-Cache
Die Zielarchitektur. Der Build ist die einzige Stelle, an der CMS und Website sich berühren — danach ist die Seite von der Datenbank unabhängig.

Ein Detail, das sich erst im Betrieb zeigte: Der Build holt aus Directus nur JSON. Bilder lädt der Browser des Besuchers direkt, aus R2 über den CDN-Cache. Das klingt banal, korrigierte aber eine falsche Annahme aus der Planungsphase — nämlich dass R2 den Datenverkehr beim Bauen spare. Tut es nicht, es gibt beim Bauen keinen Bildverkehr. Der echte Grund für R2 ist ein anderer und besserer: Der VPS bleibt praktisch zustandslos, das Backup ist ein Datenbank-Dump, und das Fotoarchiv hängt nicht an der Festplatte einer Maschine, die man neu aufsetzen können muss.

Das Datenmodell und ein Fund, der alles einfacher machte

Vor dem ersten Import stand ein Audit des Datenbank-Dumps. Der Fund dort war der wertvollste Moment des ganzen Projekts.

Die Annahme war: Touren sind Artikel, also Textwände, die geparst werden müssen. Falsch. Touren lagen in jos_eventlist_locate als strukturierte Datensätze — mit getrennten Feldern für Name je Sprache, Beschreibung, Preis, Schwierigkeit, Dauer, Kilometer, Sammelort und vier Bildslots. Das heißt: Der frühere Betreiber hatte vor fünfzehn Jahren eine sauber modellierte Struktur angelegt, die fast 1:1 in ein modernes Schema passt.

Das änderte den Aufwand des Kernstücks von «alles aus Prosa rekonstruieren» auf «Felder abbilden und nur das Programm parsen». Die Verbindung zwischen beidem war unscheinbar und entscheidend: Das Feld url im Tour-Datensatz enthält keine Adresse, sondern die ID des Artikels mit dem Tagesprogramm.

Die Abbildung ins neue Schema:

Directus-Sammlung Quelle in Joomla
destination (Reiseziel) jos_eventlist_categories
tour jos_eventlist_locate
tour_day (Tagesetappe) jos_content + Übersetzungen, geparst nach «Tag N»
tour_departure (Termin) jos_eventlist_dates
report (Reisebericht) jos_content nach Kategorie
guide Kategorie «Instruktoren», verknüpft über die Termine

Für die Verknüpfung gilt eine Regel, die ich inzwischen in jedem Migrationsprojekt anwende: keine harten Zuordnungen im Code, sondern Herkunfts-IDs in den Daten. Jeder Datensatz trägt seine alte ID mit — tour.legacy_locid, guide.legacy_id, report.public_id. Damit ist jeder Import wiederholbar, jede Prüfung gegen die Quelle möglich, und die URL-Umleitung später trivial.

Der ETL-Pfad

Extraktion, Umformung, Laden — der Teil, in dem die eigentliche Arbeit steckt. 85 Python-Skripte, gewachsen über die Migration, mit gemeinsamen Bibliotheken für die Dinge, die alle Inhaltstypen brauchen.

Vom Dump in die neue Datenbank MySQL-Dump jos_* Tabellen lokale Joomla-Kopie im Docker, zum Nachsehen 1. Kodierung cp1251 → unescape → UTF-8 pro Tabelle unterschiedlich 2. Shortcodes {mosimage} → Bildblock {events_list} → Termine {anketa} → Formular 3. Tagesparser «Tag N» → tour_day km, Auf-/Abstieg, Stunden Leistungen / nicht enthalten 4. Sprachdetektor welcher Text ist Original? ru-Primär bei 95 % Altbestand Ergebnis wird materialisiert 5. Bilder ~10.600 Fotos nach R2 Pfad → Platzhalter im Text Dateinamen nur ASCII 6. Laden Directus REST, idempotent legacy_id bleibt am Datensatz Wiederholung ohne Dubletten 7. Prüfen: Zeilen zählen, Stichproben, Verknüpfungsquote, HTML validieren Ohne diesen Schritt merkt niemand, dass 145 von 299 Berichten keine Tour gefunden haben Directus 135 Touren 980 Etappen 1.258 Artikel
Der ETL-Pfad. Schritt 7 ist der, den man am ehesten weglässt und am bittersten vermisst: ohne Prüfung bleibt jeder Importfehler unentdeckt, bis ihn ein Besucher findet.

Am Ende standen in der neuen Datenbank: 135 Touren mit 980 Tagesetappen, 299 Reiseberichte, 744 Journalbeiträge, 191 Ratgeber, 520 Sehenswürdigkeiten, 25 Reiseziele, 11 Guides — und rund 10.600 Fotos.

Kodierung: die Hölle der ersten Woche

Der erste Import produzierte Text wie рук statt Wörtern. Die Ursache war ein Dreifach-Problem, das in dieser Kombination typisch für alte CMS ist:

  1. Die Tabellen hatten gemischte Zeichenkodierung — ein Teil in cp1251 (Windows-Kyrillisch), ein Teil in UTF-8. Nicht pro Datenbank, sondern pro Tabelle, teils pro Spalte.
  2. Die Übersetzungstabelle enthielt Text zusätzlich als HTML-Entities, weil JoomFish so gespeichert hat.
  3. Der Dump selbst behauptete eine Kodierung, die nicht für alle Tabellen stimmte.

Die Lösung ist eine feste Reihenfolge, und die Reihenfolge ist nicht beliebig: erst aus cp1251 dekodieren, dann HTML-Entities auflösen, dann als UTF-8 schreiben. Wer die ersten zwei Schritte tauscht, bekommt Text, der aussieht wie Text, aber an den Rändern kaputt ist — und das fällt bei 11.247 Übersetzungszeilen erst auf, wenn ein Leser sich beschwert.

Daraus wurde eine Regel für alle folgenden Importe: Rohwerte immer als Text lesen, nie als Zahl. Sonst wird aus der Artikelnummer 001234 die Zahl 1234, und die Verknüpfung zur alten Seite ist weg.

Bilder: der Slot, der beinahe verloren ging

Jede Tour hatte in Joomla vier Bildfelder: img_s, img_m, img_b, img_v. Die naheliegende Lesart: small, medium, big, very big — also dieselbe Aufnahme in vier Größen. Dann importiert man die größte und lässt den Rest weg.

Ein Blick in die Daten zeigte etwas anderes. s, m und b sind tatsächlich Größen desselben Querformats. img_v ist kein Format, sondern ein anderes Bild — ein bewusst hochkant beschnittenes Titelbild für schmale Displays, mit eigener Komposition. Vorhanden bei 64 von 138 Touren.

Wer der naheliegenden Lesart folgt, verliert 64 handgemachte mobile Titelbilder und merkt es nie, weil «es gibt ja ein Titelbild». Im neuen Schema wurde daraus ein eigenes Feld cover_mobile, und die Tourseite entscheidet per Art-Direction, welches Bild sie zeigt.

Die Auslieferung selbst läuft nicht über den Build. Directus hat eine Assets-API, die Bilder auf Anfrage skaliert und das Format aushandelt: Der Browser sagt im Accept-Header, ob er AVIF versteht, und bekommt AVIF, sonst WebP, sonst JPEG. Gemessen an derselben Aufnahme: −31 Prozent bei Vorschaubildern, −40 Prozent bei 1.280 Pixeln, −57 Prozent bei Großansichten.

Die Alternative wäre gewesen, alle Formate beim Bauen zu erzeugen. Bei rund 10.600 Fotos × mehreren Breiten × drei Formaten wären das über 80.000 zusätzliche Dateien im Build und eine Bauzeit von Minuten auf Stunden — für exakt dasselbe Ergebnis in Bytes. Diese Entscheidung ist der Grund, warum der Build heute knapp zwei Minuten dauert und nicht eine halbe Stunde.

Eine harte Regel kam noch dazu: Dateinamen nur in ASCII. Kyrillische oder umlautbehaftete Dateinamen überleben den Weg durch S3-Signaturen, CDN-Caches und Content-Disposition-Header nicht zuverlässig.

URLs: wo Sichtbarkeit wirklich verloren geht

Bei einem Relaunch entscheidet nicht das Design über den Traffic danach, sondern ob die alten Adressen ankommen. Das alte Portal hatte drei Muster, die Google seit Jahren indexiert hatte:

  • /{sprache}/a/{id}/ — Artikel, die real indexierte Seite
  • /{sprache}/c/{id}/ — Kategorie
  • /{sprache}/s/{id}/ — Sektion

Der erste Glücksfall: Weil im neuen Schema public_id gleich der alten Artikel-ID ist, ist die Umleitung für Artikel trivial — /uk/a/1655 wird /uk/1655. Gleiche Nummer, nur das /a/ fällt weg.

Der erste Stolperstein steckt direkt daneben: /c/16 und /a/16 sind verschiedene Seiten. Kategorien und Artikel haben getrennte Nummernkreise, die sich überschneiden. Eine Umleitung «nach Nummer» hätte Besucher auf inhaltlich fremde Seiten geschickt — mit korrektem HTTP-Status, also unbemerkt. Kategorien brauchen eine explizite Zuordnungstabelle.

Ein weiterer Fehler war ein Ein-Zeichen-Problem mit großer Wirkung: Die erste Regelversion leitete /ua/a/{id} auf /ua/{id}. Nur existiert /ua/ auf der neuen Seite nicht — die ukrainische Sprache heißt dort uk. Jede Umleitung landete im Nichts. Solche Fehler findet man nicht durch Nachdenken, sondern indem man jedes Ziel gegen den echten Build prüft.

Genau das deckte den nächsten Fall auf: Die Kategorie-Umleitungen zeigten auf Themen-Hubs, die im ursprünglichen Plan standen, aber nie gebaut wurden. Eine Validierung aller Ziele gegen die generierten Dateien ergab 32 von 44 Zielen im 404. Nach der Korrektur: 661 Ziele, null Fehler.

Der teuerste Fehler des Projekts

Cloudflare Pages liest Umleitungen aus einer Datei _redirects. Die Dokumentation nennt großzügige Grenzen. Die Realität: ab etwa Regel 120 wird der Rest stillschweigend verworfen. Kein Fehler beim Deploy, keine Warnung — die Regeln existieren einfach nicht.

Über Monate wuchs die Datei, weil auch Umleitungen von zusammengeführten Duplikaten dort landeten: 59 Paare mal vier Sprachen sind 236 Zeilen. Insgesamt rund 240 Regeln. Alles über der Grenze war unwirksam — und dort standen die legacy-Joomla-Regeln, also genau die Adressen mit dem realen Suchmaschinen-Traffic. Sie lieferten 404, bei laufendem Besucherstrom.

Aufgefallen ist es durch einen Zufall: In einem Session-Recording-Werkzeug tauchte ein Fehlerbericht für die Adresse /ru/a/2032 auf. Ein Besucher war über eine alte Adresse gekommen und ins Leere gelaufen. Die Grenze selbst wurde danach durch Bisektion gemessen — Regel 124 wirkt, Regel 125 ist weg.

Vorher: alles in eine Datei — der Hälfte fehlte die Wirkung _redirects (eine Datei, ~240 Regeln) Zeile 1–120: Dedup-Umleitungen · wirksam Zeile 121–240: legacy /a/ /c/ /s/ · still verworfen Ergebnis: 404 auf allen Adressen mit echtem Such-Traffic Nachher: zwei Mechanismen, jeder mit passender Grenze _redirects · 55 Regeln nur legacy-Joomla-Pfade /a/ /c/ /s/ /ua/ /index.php Grenze ~120, Puffer bleibt frei wird mit dem Build ausgeliefert Bulk Redirects · 81 Paare ID auf ID nach Dublettenmerge Grenze: Tausende wirkt am Edge, sofort kein Rebuild der Seite nötig Regeln, die daraus folgen · keine Ketten: ein Ziel ist nie Quelle · ein Skript als einzige Quelle der Wahrheit · Regeln zählen ist Teil des Deploys · jedes Ziel gegen den Build prüfen · Live-Verifikation nach dem Sync
Getrennt nach Grenze statt nach Bequemlichkeit: was knapp ist, bekommt nur das Nötigste; was praktisch unbegrenzt ist, nimmt den Rest — und braucht dafür keinen neuen Build.

Die Lehre ist nicht «Cloudflare ist schuld». Sie ist: Wenn ein System eine Grenze hat und beim Überschreiten schweigt, muss die Grenze im eigenen Prozess sichtbar werden. Heute ist das Zählen der Regeln Teil des Deploys, und Dedup-Umleitungen stehen in einem Skript, das sie am Edge synchronisiert und danach gegen das Live-System verifiziert.

Vier Sprachen, und was «übersetzt» wirklich heißt

Das alte System hatte Englisch und Deutsch mit beeindruckenden Zahlen: 807 englische und 1.052 deutsche Artikel. Ein Blick in die Inhalte beendete die Freude — es waren eingefrorene Maschinenübersetzungen aus den 2010er Jahren. Die Zahlen hatten sich seit einem Dump von 2019 kaum verändert.

Die Entscheidung: nicht übernehmen, neu übersetzen. Mit einer Ausnahme, die Aufmerksamkeit brauchte — bei einigen sehr alten Artikeln war die englische Version kein Übersetzungstext, sondern anderes Material. Solche Fälle erkennt man an Länge und Ähnlichkeit und legt sie einem Menschen vor, statt sie zu überschreiben.

Der Sprachstand steht heute in den Daten selbst: Der Detektor im Import hat für jeden Datensatz festgehalten, welche Sprache das Original ist. Damit muss keine späte Pipeline mehr die alte Seite befragen — die Antwort liegt in der Datenbank.

Was Mehrsprachigkeit in einem strukturierten Modell wirklich bedeutet, zeigt ein einzelnes Feld: Visa- und Einreisehinweise sind keine Übersetzung. Für ukrainische und deutsche Gäste gelten unterschiedliche Regeln — das ist zielgruppenspezifischer Inhalt, der zufällig in verschiedenen Sprachen steht. Ein normales Übersetzungsfeld wäre hier die falsche Struktur und würde über Jahre falsche Auskunft geben.

Bei der Korrektur half ein Sprachmodell, aber nicht als Übersetzer, sondern als Korrektor: Durchlauf über bestehende ukrainische Tourtexte, Suche nach Russizismen, Grammatik- und Zeichensetzungsfehlern, Vorschläge zur Prüfung. Ergebnis: 107 Touren, 2.363 angenommene Korrekturen. Der Weg ist auf andere Sprachen und Inhaltstypen übertragbar, weil er auf Feldern arbeitet und nicht auf Textwänden.

Datenqualität: was zwanzig Jahre anrichten

Drei Fälle, die zeigen, warum eine Prüfschicht kein Luxus ist.

Der falsche Krim-Bestand. Ein früherer Import hatte Kopien ausländischer Objekte angelegt und ihnen die Region «Krim» zugewiesen — Orte in Norwegen, der Türkei, Georgien. Rund 42 Duplikate wurden zu Kanons zusammengeführt und den richtigen Regionen zugeordnet. Solche Fehler sind aus dem CMS heraus unsichtbar, weil jede einzelne Seite plausibel aussieht. Sichtbar werden sie erst in einer Auswertung über den gesamten Bestand.

Die Verknüpfungsquote. Von 299 Reiseberichten ließen sich 154 automatisch einer Tour zuordnen — über ein Signal im Text, nämlich einen Link auf den Tourartikel. Die restlichen 145 blieben ohne Zuordnung. Wichtig ist die Haltung dahinter: nicht raten. Ein Bericht ohne Tour ist ein sauberer Zustand; ein Bericht an der falschen Tour ist eine Falschaussage im Verkauf.

Der Validierungsverlauf. Der HTML-Prüfer läuft über den kompletten Katalog. Sein Verlauf ist die ehrlichste Kurve des Projekts:

Stand HTML-Fehler Was passierte
Ausgangsmessung 1.285 Joomla-Altlast: <font>-Tags, offene Listen, Word-Reste
erste Bereinigung 80.231 ein Ausdruck für Attribut-Abstände zerstörte alle Attributwerte
nach dem Rückbau 678 Fehler behoben, dazu FAQ-Bereinigung
Serie kleiner Fixes 398 <font> entfernt, Slider-Reste entfernt
weiter 49, dann 21 Zeichensetzung in Style-Attributen, Inline-Tag-Balance
nach Massenübersetzung 37 neue Übersetzungen brachten neue offene Tags mit
aktuell 21 Rest sind tiefe Kaskaden aus dem Alt-HTML

Der Sprung auf 80.231 ist die Stelle, die ich in jedem Rückblick zeige. Eine einzelne Ersetzungsregel, gedacht für Kosmetik, hat in einem Durchlauf jedes HTML-Attribut im Katalog beschädigt. Sichtbar wurde das nur, weil die Validierung bei jedem Commit automatisch läuft und eine Zahl ausgibt. Ohne diese Zahl wäre der Schaden mit dem nächsten Deploy live gegangen.

Ein zweiter Fall aus derselben Serie ist lehrreicher, als er klingt: Eine Bereinigungsregel sollte fehlplatzierte Listenelemente aus Absätzen entfernen. Ihr Muster griff über den Absatz hinaus, verschluckte eine komplette verschachtelte Liste und entfernte deren Listenelemente mit. Ein nachgelagerter Schritt fügte danach neue, falsche Elemente hinzu. Die Korrektur bestand darin, verschachtelte Blöcke vor dem Bereinigen durch Platzhalter zu schützen — und nichts zu ändern, wenn danach kein Ziel mehr übrig ist.

Automatisierung: was ohne Zutun läuft

Ein Projekt dieser Größe ist mit Handarbeit nicht zu halten. Was heute von selbst passiert:

  • Bei jedem Commit baut GitHub Actions die komplette Seite, crawlt alle internen Links auf Bruchstellen und validiert das HTML. Der Linkcheck läuft gegen die gebaute Seite, nicht gegen eine Vorschau — er sieht also genau das, was Besucher sehen.
  • Nachts holt ein Skript die Zahlen aus der Google Search Console in die Projektdatenbank. Daraus entsteht das Dashboard, das ich in einem eigenen Projekt beschrieben habe.
  • Ein Monitor prüft die Container auf dem Server, ein zweites Skript legt täglich einen Datenbank-Dump ab. Weil die Bilder in R2 liegen und nicht auf der Festplatte, ist dieser Dump das vollständige Backup.
  • Anfragen aus dem Formular gehen in die Datenbank und parallel als Benachrichtigung heraus — ohne Umweg über ein Postfach, in dem sie liegenbleiben könnten.
  • n8n trägt die Abläufe, die zwischen Systemen vermitteln: Übersetzungs- und Anreicherungsläufe, geplante Rebuilds.

Ein Punkt, der über Zeitersparnis hinausgeht: Der Rebuild ist vom Inhalt entkoppelt. Wer im CMS etwas ändert, ändert nicht sofort die Website — es braucht einen Build. Das klingt nach einem Nachteil und ist ein Sicherheitsnetz: Zwischen Redaktion und Öffentlichkeit liegen Prüfschritte, die kaputtes HTML und tote Links abfangen, bevor sie jemand sieht.

Maschinenlesbar: die Seite für KI-Suche vorbereiten

Dieser Teil ist der jüngste und aus meiner Sicht der unterschätzteste. Immer mehr Anfragen erreichen Websites nicht über eine Ergebnisliste, sondern über ein Sprachmodell, das im Auftrag eines Nutzers liest. Solche Zugriffe führen kein JavaScript aus, klicken keine Filter und interpretieren kein Layout.

Drei Dinge folgen daraus, und alle drei sind umgesetzt:

Erstens: der Inhalt steht im HTML. Weil die Seiten statisch generiert sind, ist jeder Text ohne Skriptausführung vorhanden. Was für Geschwindigkeit gebaut wurde, zahlt sich hier ein zweites Mal aus.

Zweitens: eine llms.txt. Eine kurze, maschinenlesbare Beschreibung, was die Seite ist, welche Sprachen es gibt, wie die Adressen aufgebaut sind und wo die Hauptbereiche liegen — mit Links. Das ist kein Ranking-Trick, sondern eine Landkarte für Leser, die keine Navigation sehen.

Drittens: eine echte API. Die Touren gibt es zusätzlich als saubere JSON-Endpunkte, pro Sprache eine Liste und pro Tour ein Detaildokument. Bewusste Entscheidungen darin:

  • Nur buchbare Touren. Archivierte Routen bleiben auf der Website, aber außerhalb der API — ein Agent soll nur sehen, was man tatsächlich buchen kann.
  • Drei Sprachen statt vier. Russisch ist Archivbestand, also nicht in der maschinenlesbaren Ausgabe.
  • Ein einziger Abruf beim Bauen. Alle Endpunkte teilen sich denselben Datensatz, gecacht über den Build hinweg, statt pro Datei erneut abzufragen.
  • Textfelder ohne HTML. Was ein Modell liest, soll Prosa sein, nicht Markup.

Das Ergebnis ist eine Website, die dreifach lesbar ist: als Seite für Menschen, als HTML für Suchmaschinen, als strukturiertes JSON für Agenten. Der Aufwand dafür war ein Bruchteil dessen, was das Design gekostet hat.

Second Brain: wie ein Projekt dieser Größe koordiniert bleibt

Der Teil, den man in Projektberichten selten sieht, und ohne den die Migration nicht funktioniert hätte. Über Monate hinweg trifft man hunderte Entscheidungen, von denen jede später eine Frage aufwirft: Warum eigentlich so? Wenn diese Antworten in Chatverläufen liegen, sind sie verloren.

Deshalb hat das Projekt eine Wissensbasis im Repository — reines Markdown, 126 Dateien, nach dem Muster einer LLM-Wiki. Ihr Aufbau ist absichtlich einfach:

  • Ein Eingangskorb. Neue Dokumente, Exporte, Analysen landen in einem Ordner IN/. Der Satz «verarbeite den Eingang» genügt, damit die Inhalte gelesen, einsortiert, verlinkt und archiviert werden.
  • Unveränderliche Quellen. Alles Rohmaterial — Datenbank-Dumps, Suchkonsolen-Exporte, Konkurrenz-Analysen — liegt separat und wird nie bearbeitet. Zitate zeigen darauf.
  • Gepflegte Seiten mit Querverweisen. Domänenmodell, Migration, SEO, Technik, Wettbewerber, Entscheidungen, Glossar. Jeder Begriff bekommt beim ersten Auftreten eine eigene kurze Seite.
  • Ein Entscheidungsjournal. Datum, Entscheidung, Begründung. Das ist der Teil, der ein halbes Jahr später den größten Wert hat.
  • Ein Ingest-Log. Append-only, damit nachvollziehbar bleibt, wann welche Quelle eingearbeitet wurde.

Der praktische Nutzen zeigt sich in genau der Situation, in der dieser Text entstanden ist: Für den Rückblick musste ich nicht rekonstruieren, was passiert ist — die Zahlen, die Fehlschläge und die Begründungen standen dokumentiert da.

Und noch etwas, das mir wichtig ist zu erwähnen, weil es gegen die Idee einer Wissensbasis als Selbstzweck spricht: Die Wiki war an mehreren Stellen falsch. Beim Schreiben habe ich jede Zahl gegen den Code geprüft und Abweichungen gefunden — eine Umleitungsdatei mit 55 statt der dokumentierten 106 Regeln, 81 statt 59 Dedup-Paaren, 11.660 statt 6.971 Seiten, an einer Stelle sogar eine falsche CMS-Version im Einstiegsdokument. Nichts davon war dramatisch, aber es bestätigt eine Regel: Dokumentation ist ein Index, keine Wahrheit. Die Wahrheit steht im laufenden System, und wer eine Zahl veröffentlicht, prüft sie dort.

Was ich daraus mitnehme

Der Datenaudit ist die wertvollste Woche. Die Erkenntnis, dass Touren strukturierte Datensätze und keine Textwände waren, hat mehr Aufwand gespart als jede technische Optimierung danach. Wer sofort mit dem Import beginnt, baut Wochen auf einer falschen Annahme.

Herkunfts-IDs statt harter Zuordnungen. Jeder Datensatz trägt seine alte Nummer. Das macht Importe wiederholbar, Umleitungen trivial und Prüfungen gegen die Quelle möglich. Es ist der billigste Schritt mit dem größten Hebel.

Stille Grenzen sind gefährlicher als Fehler. Eine Umleitungsdatei, die ab Regel 120 schweigend aufhört zu wirken, hat mehr Sichtbarkeit gekostet als jeder sichtbare Bug. Alles, was ein System stillschweigend abschneidet, muss im eigenen Prozess eine Zahl bekommen.

Automatische Prüfungen fangen die eigenen Fehler. Der Sprung von 1.285 auf 80.231 HTML-Fehler war mein Fehler, nicht der von Joomla. Gefunden hat ihn kein Mensch, sondern eine Zahl in einem Automatisierungsschritt.

Nicht raten, wenn Daten fehlen. 145 Berichte ohne Tourzuordnung sind ein ehrlicher Zustand. Automatisch geratene Zuordnungen wären ein Verkaufsversprechen auf Basis einer Vermutung.

Was davon für kleine Betriebe übrig bleibt

Zwanzig Jahre Joomla und vier Sprachen klingen weit weg von einem Betrieb mit zehn Leuten. Das Muster darunter ist dasselbe, sobald eine Website älter als ein paar Jahre ist.

Drei Situationen, die in jedem kleinen Betrieb vorkommen:

  • Das alte System bekommt keine Updates mehr. Ein CMS ohne Sicherheitspatches ist kein technisches Detail, sondern ein offenes Fenster. Der Weg nach draußen führt nicht über einen «Relaunch», sondern über einen Datenauszug — und der ist meist möglich, auch wenn niemand mehr weiß, wie das System funktioniert.
  • Der Relaunch kostet Sichtbarkeit, nicht Geld. Die Rechnung für ein neues Design ist einmalig und kalkulierbar. Verlorene Rankings sind es nicht. Wer umzieht, braucht eine Abbildung jeder alten Adresse auf eine neue — und eine Prüfung, dass jede davon wirklich ankommt.
  • Inhalte liegen in Textwänden. Preise, Termine und Leistungen mitten im Fließtext bedeuten, dass niemand sie auswerten kann und jede Änderung Handarbeit ist. Struktur ist kein Selbstzweck: Erst durch sie werden «alle Angebote ohne gültigen Preis» oder eine automatische Übersetzung überhaupt möglich.

Der Unterschied zum Fall hier ist der Umfang, nicht der Ansatz. Für einen kleinen Betrieb braucht es keine 85 Importskripte und keine vier Sprachen — aber dieselben vier Schritte: erst die Daten ansehen, dann strukturieren, dann jede alte Adresse abbilden, und am Ende automatische Prüfungen, die Fehler finden, bevor Kunden sie finden.

Wenn Sie eine Website haben, die älter ist als das letzte Update ihres Systems: Offen für eine Festanstellung in Data Analytics und Automatisierung? Kontakt aufnehmen.

← Alle Projekte