PHP2Shell ließ mich dieses Blog auf Jekyll umstellen

Technik

Dafür wird eine Audiodatei mit KI-Stimme geladen. Gibt es sie noch nicht, erzeugt ein Dienst bei Amazon Web Services sie einmalig mit der Stimme von ElevenLabs und legt sie für alle weiteren Aufrufe ab. Übertragen wird nur der öffentliche Beitragstext, keine Daten von Ihnen; beim Abspielen verbinden Sie sich mit Amazon CloudFront (media.krautsource.info). Mehr in der Datenschutzerklärung.

Vorab: Warum der Umzug?

Der konkrete Auslöser, den Umzug endlich zu beginnen, war PHP2Shell: zwei kritische Lücken in PHP, die am 21. Oktober 2026 mit Notfall-Updates geschlossen wurden (BleepingComputer). Mein alter (WordPress-)Server lief auf PHP, und ich wollte nicht länger einen PHP-Server nebenbei betreiben.

Ich habe dieses Blog nicht aus Technikfreude umgezogen, sondern aus Mangel an Zeit. Der alte WordPress-Server hat mich monatlich etwa zehn Euro gekostet, und das für ein Blog, in dem ich aus Zeitgründen nicht mehr regelmäßig neue Inhalte mehr produziert habe. Aber das Wesentliche waren nicht die auflaufenden Kosten. Dazu kam die Pflege: Einmal im Monat musste sich jemand einloggen und Updates einspielen, die nicht automatisch kamen. Und PHP hatte immer wieder Sicherheitslücken, um die sich jemand kümmern musste. Dieser Jemand war nicht ich, sondern ein viel beschäftigter Freund, der das für mich im Hintergrund erledigt hat. Das war auf Dauer keine faire Aufgabe, für ein Blog, das ich selbst kaum noch gefüttert habe.

Eine statische Seite hat nichts davon: kein PHP, keine Datenbank, nichts zum Einloggen und Aktualisieren, und keinen Server, der monatlich Geld kostet. GitHub Pages ist kostenlos, nur für die Bilder (Amazon S3 und CloudFront) und die Domain fällt noch etwas an. (Gut, für das private Repo bei GitHub zahle ich auch 4 USD im Monat.) Es gibt weniger Wartung, weniger Angriffsfläche, und das Archiv bleibt trotzdem online. Mein Freund ist die Pflicht los.

Bisher hatte ich erst ein Jekyll-Projekt gebaut, und das war auch zehn Jahre her. An Jekyll reizte mich diese Vertrautheit - so wie es eins bei WordPress gewesen war. (Bei WordPress gibt es für jedes Problem ein Plugin als Lösung. Aber der Preis ist Ladezeit. Bei Jekyll ist das besser - und sicherer ist es obendrein, weil es statische Seiten liefert.) Aber es war so einfach, das alte Projekt mit Claude Code wieder zu beleben, dass ich das für alle meine Blogs auch machen wollte. Krautsource.info ist das größte und mir das liebste. Es hat 893 veröffentlichte Beiträge und über 1.300 Mediendateien. Das einzige, was ich wirklich aufgeben musste, waren Kommentare. Aber davon hatte ich eh nicht viele, und vielleicht setze ich auch in einer weiteren Ausbaustufe wieder auf das Indieweb mit Postbacks und Kommentaren von anderen Webseiten. Auch wenn das nach (leider) nach einer sterbenden Facette des alten Internets aussieht.

Aus dem ersten Umzug eines kleineren Projekts wurde dann eine Vorlage, die ich für die anderen Blogs wiederverwendet habe: Jede Domain bekommt ein eigenes Projekt, und der Ablauf ist immer derselbe (WordPress-Export einlesen, Seiten bauen, DNS umstellen). Dafür habe ich Claude Code ein Runbook erstellen lassen.

Das Besondere an Krautsource war nur, dass es das größte war. Deswegen eben der Start an einem kleineren Projekt. Die Bilder lagen schon bei WordPress in einem S3-Bucket, falls mal irgendwas kaputt geht. Daher kannte ich mich mit S3 und CloudFront schon ein bisschen aus.

Und dann kam PageSpeed

Kaum war dieses Blog von WordPress auf eine statische Seite umgezogen (Jekyll, GitHub Pages, Bilder bei Amazon S3 und CloudFront noch vor dem Bucket), habe ich den Google-PageSpeed-Test drüberlaufen lassen. Die Meldungen kannte ich fast alle (die Zitate unten sind wörtlich aus dem Bericht): Bilder zu groß für die angezeigte Fläche, kein modernes Bildformat, Caches zu kurz, Schrift blockiert den Text, das wichtigste Bild wird erst spät geladen, Layout springt.

Interessant ist weniger die Liste als die Frage, wie viel Aufwand jede Meldung macht. Das spielt mit Claude Code keine echte Rolle mehr - die Tokens, naja, sie werden abgerechnet. Da es bei mir nicht eilte mit den Verbesserungen, konnte ich jeweils bis zum nächsten Abend warten. Aber alle waren auch nicht wichtig. Irgendwo habe ich dann gesagt: passt schon, wie wir in Bayern sagen.

Hier mein Vergleich, wie es bei diesem Blog war und wie es bei WordPress gewesen wäre. Die Aussagen zum WordPress-Kern habe ich gegen die offiziellen Entwicklerhinweise geprüft (Links jeweils dabei, Stand 22. Oktober 2026). Alles zu Plugins, Caching und Hosting ist Erfahrung und nicht frisch nachgemessen. Wer es genau wissen will, sollte es für die eigene Installation prüfen.

1. Bilder in der richtigen Größe (responsive Bilder)

„Diese Bilddatei ist für die angezeigten Abmessungen (642x482) größer als nötig (1024x768). Verwende responsive Bilder, um die Bilddownloadgröße zu reduzieren.“

Google PageSpeed Insights

Hier: Die Bilder liegen schon in mehreren Größen im Bucket (300, 768, 1024 Pixel und mehr). Ein kleines Jekyll-Plugin ergänzt beim Bauen der Seite bei jedem Bild srcset, sizes, Breite und Höhe. In den Beiträgen musste ich nichts anfassen. Das sind gut 100 Zeilen Ruby plus ein Index der vorhandenen Größen (ein Skript liest ihn aus dem Bucket). Für neue Bilder gibt es ein Skript, das die Größen erzeugt. Alle Skripte hat Claude Code geschrieben.

Eine Falle hatte ich mir selbst gestellt: Das große Titelbild der Startseite hieß …-scaled.jpg und lud trotzdem als 943-KB-Datei mit 2560 Pixeln Breite. WordPress skaliert Uploads über 2560 Pixel seit Version 5.3 herunter und benennt die Datei so (Dev-Note zu 5.3). Die kleineren Varianten heißen dann aber ohne -scaled. Mein erster Index kannte diesen Fall nicht. Seit ich ihn berücksichtige, bekommt das Titelbild die passende Größe.

WordPress: Der Kern macht das seit Version 4.4 selbst (srcset und sizes, dazu eine mittlere Größe mit 768 Pixeln, Dev-Note). Die größte Größe im srcset ist standardmäßig auf 1600 Pixel begrenzt. Die Falle sind Themes und Bildbausteine, die feste Größen ausgeben, und Altbestände, die nie neu skaliert wurden. Dann beginnt das Regenerieren der Vorschaubilder per Plugin.

2. Moderne Formate: WebP und AVIF

„Wenn du ein modernes Bildformat (WebP, AVIF) verwendest oder die Bildkomprimierung erhöhst, könnte sich die Downloadgröße dieses Bilds verbessern.“

Google PageSpeed Insights

Hier: Ich habe es mit sechs Bildern aus einem Artikel getestet und mit dem Mac gerechnet, den ich ohnehin hatte (cwebp für WebP, sips für AVIF):

Bild JPEG WebP (q80) AVIF (q60)
Konzertfoto 768 px 67,8 KB −49 % −56 %
Konzertfoto 1024 px 106,3 KB −54 % −59 %
Konzertfoto 1536 px 204,3 KB −60 % −67 %
Hochformat 768×1024 160,1 KB −52 % −57 %
Bühne 1024 px 212,8 KB −40 % −48 %
Titelfoto, groß 943,6 KB −31 % −43 %

Was gemeint ist: weg von jpg und png-Formaten. WebP spart fast so viel wie AVIF, ist aber überall verfügbar und viel schneller zu erzeugen. Beides habe ich dann für alle Bilder gemacht, die auf der Seite vorkommen: 1365 WebP- und 1018 AVIF-Dateien, zusammen etwa 111 MB, in einem Durchlauf mit einem Skript (JPEG als WebP mit Qualität 80 und als AVIF mit 60, PNG-Screenshots nur als verlustfreies WebP, damit Text scharf bleibt; animierte GIFs bleiben, wie sie sind). Im Seitenbau erscheint ein <picture>-Element, aber nur dort, wo die Dateien wirklich existieren. Ältere Beiträge haben Bilder in nur einer Größe ohne srcset, die brauchten einen eigenen Durchlauf: Auch sie stehen jetzt in <picture>, mit einer AVIF- und einer WebP-Quelle. Laufende Kosten: keine. Die Dateien liegen in einen Bucket, was den Umzug hoffentlich mal leichter macht.

WordPress: Der Kern nimmt WebP seit Version 5.8 an und AVIF seit 6.5 (Dev-Note zu 6.5). Er wandelt vorhandene JPEG- und PNG-Dateien aber nicht von selbst um, und AVIF funktioniert nur, wenn die Bildbibliothek des Servers (ImageMagick) es unterstützt. In „Werkzeuge → Website-Zustand → Info → Medien-Verarbeitung“ steht, was geht. Der Altbestand muss also konvertiert werden, und dafür greifen die meisten zu einem Plugin mit Abo (ein Bildoptimierer) oder zu einem CDN, das während der Auslieferung umrechnet. Das ist bequem, aber ein weiterer Dienst, der gepflegt und bezahlt wird und der bei Wechsel des Anbieters hängen bleibt.

3. Caching

„Wir sind der Meinung, dass alle statischen Assets dem hier beschriebenen Entscheidungsbaum folgen sollten: Cachefähige Ressourcen sollten eine sehr lange Lebensdauer (30 Tage oder 1 Jahr) haben.“

Google PageSpeed Insights

Hier: Das war der lehrreichste Punkt:

  • Bilder liegen in S3 hinter CloudFront. Ich wollte eine Header-Regel anlegen („ein Jahr, immutable“). Der Free-Plan von CloudFront verbietet aber ausdrücklich eigene Header-Regeln. Der Umweg ist einfach: den Header direkt als Metadaten an die Dateien im Bucket setzen. Bilder werden bei mir nie überschrieben, ein Jahr Cache ist also sicher. Ich habe das zuerst nur an den 19 Bildern eines Artikels ausprobiert und die Wirkung geprüft, danach an allen rund 17.800 Dateien. Das Kopieren der Objekte auf sich selbst mit neuen Metadaten dauerte fünf Minuten und kostete geschätzt weniger als zehn Cent.
  • CSS, JavaScript, Schriften liefert GitHub Pages aus, mit festen zehn Minuten Cache. Das lässt sich dort nicht einstellen. Meine Auswege: Das CSS (neun Kilobyte) und die Konfiguration der Cookie-Einwilligung stehen jetzt direkt im HTML. Das große Skript der Einwilligung (236 KB), sein CSS und die Schriften liegen versioniert auf der Medien-Domain, in einem Ordner, dessen Name ein Hash der Dateien ist, mit einem Jahr Cache. Ändert sich eine Datei, ändert sich der Hash und damit die Adresse. Ein Skript lädt hoch und trägt die Version in die Seite ein.
  • Schriften brauchen dabei eine Zugabe: Weil sie von einer anderen Adresse kommen als die Seite, verlangt der Browser einen CORS-Header. Eigene Header-Regeln gehen im Free-Plan nicht, aber eine kleine CloudFront-Funktion, die den Header setzt, ließ sich anhängen. Lokal beim Entwickeln lade ich Schriften und Skript wieder von der Seite selbst, denn dort erlaubt die Regel nur die echte Domain.

WordPress: Hier wird der Cache zum eigenen Projekt: Seiten-Cache-Plugin, Browser-Cache-Regeln in der Servereinstellung oder per Plugin, dazu meist ein CDN. Das ist alles machbar, hat aber viele Stellschrauben, die sich gegenseitig stören können (ein Plugin zum Minimieren, eines zum Cachen, der Server, das CDN). Dafür ist der Server dort steuerbar, was GitHub Pages nicht ist.

4. Das wichtigste Bild nicht verzögern (LCP) und feste Größen (CLS)

„Wenn du den LCP optimieren möchtest, mache das LCP-Bild im HTML-Code direkt sichtbar und vermeide Lazy Loading.“

Google PageSpeed Insights

„Lege eine explizite Breite und Höhe für Bildelemente fest, um Layoutverschiebungen zu reduzieren und den CLS-Wert zu verbessern.“

Google PageSpeed Insights

Hier: Eine Zeile in der Vorlage für das erste Bild (sofort laden, hohe Priorität), und das Plugin trägt Breite und Höhe ein. Wo es keine Größenvarianten gibt, liest ein Skript die Maße aus den ersten Bytes der Datei (rund 600 Bilder, ohne die Dateien ganz zu laden). Jekyll hat mit Kramdown kein wirkliches Konzept zur Bildoptimierung, aber zum Glück sind fast nur Bilder das Performance-Bottleneck bei den rasend schnellen Auslieferungszeiten.

WordPress: Hier hat der Kern viel erledigt. Seit 5.5 laden Bilder standardmäßig lazy, aber nur, wenn Breite und Höhe gesetzt sind, und der Kern ergänzt diese Maße für Medienbilder (Dev-Note 5.5). Seit 6.3 bekommt das wahrscheinlichste LCP-Bild (das erste Bild über 50.000 Quadratpixel) fetchpriority="high", und die ersten drei Bilder werden nicht mehr lazy geladen (Dev-Note 6.3). Die Auswahl ist eine Heuristik: Sie kann auch mal ein Logo oder einen Cover-Block treffen statt des echten Titelbilds. Ärger machen oft Page-Builder und Themes, die das umgehen.

5. Schriftdarstellung

„Du kannst font-display auf ‚swap‘ oder ‚optional‘ setzen, damit der Text immer sichtbar ist.“

Google PageSpeed Insights

Hier: Eine Zeile pro Schriftschnitt im CSS, dazu ein Preload für die zwei wichtigsten Dateien. Die Schriften liegen bei mir selbst (auf der eigenen Medien-Domain), es gibt keine Google-Fonts-Anfrage und damit auch kein Datenschutzthema.

WordPress: Je nach Theme steht die Schrift im Theme-CSS oder wird von Google Fonts geladen. Beides lässt sich ändern, braucht aber ein Child-Theme oder ein Plugin.

6. Große Inhalte erst auf Klick laden

Das ist keine Google-Meldung, aber das gleiche Thema. Im Beitrag über das Journalismus-Festival in Perugia liegt eine interaktive Visualisierung der Festival-Themen, eine einzelne Seite mit rund 7,5 MB. Direkt eingebettet würde sie jeden Besuch des Beitrags verlangsamen, auch wenn niemand sie ansieht.

Hier: Ich habe sie wie die YouTube-Videos hinter einen Hinweis der Einwilligungsverwaltung (Klaro) gesetzt. Statt des Diagramms steht dort: „Möchtest du externe Inhalte von Themen-Visualisierung (interaktiv, ca. 7,5 MB) laden?“, mit den Schaltern „Einmal laden“ und „Immer laden“. Die Größe steht im Namen des Dienstes, deshalb erscheint sie im Hinweis und in der Liste der Cookie-Einstellungen. Datenschutz und Ladezeit hängen hier an derselben Einstellung. Darunter steht ein Link, der die Visualisierung in voller Größe öffnet.

Durch Claude Code kann es sein, dass ich das häufiger brauchen werde - ich glaube, der Trend bei Inhalten, auch geschriebenen, geht zu Interaktivität und Macht für den Nutzer/die Nutzerin.

WordPress: Dafür gibt es Plugins für „Lazy Loading von iframes“ oder Cookie-Banner mit Platzhaltern. Das hängt davon ab, welches Banner man nutzt, und jedes Embed muss einzeln eingestellt werden.

Was dabei auffällt

  Statische Seite (hier) WordPress
Bildgrößen Plugin beim Bauen, einmalig im Kern, Altbestand regenerieren
WebP/AVIF (auch ältere Bilder) Skript, einmalig, kostenlos Kern nimmt Dateien an, Umwandlung per Plugin oder CDN
Caching Header an den Dateien, ein paar Grenzen von GitHub (Skript, Schriften auf die eigene Domain) Cache-Plugin, Servereinstellungen, CDN
Kritisches Bild, feste Größen eine Vorlage, ein Skript großteils im Kern, Heuristik, Themes stören
Große Inhalte erst auf Klick Klaro-Dienst, ein Eintrag Plugin oder Banner-Einstellung je Embed
Dauerhafter Betrieb kein Server, keine Updates PHP, Datenbank, Updates, Sicherheit

Der wichtigste Unterschied ist nicht eine einzelne Maßnahme. Bei der statischen Seite sind alle Optimierungen Arbeit, die einmal beim Bauen oder Hochladen passiert. Danach liefert die Seite fertige Dateien aus, und es läuft keine Datenbank und kein PHP, die Zeit kosten. Bei WordPress passiert vieles zur Laufzeit, und wer das beschleunigen will, baut Zwischenschichten, die gepflegt werden müssen.

Der Preis ist, dass ich mehr selbst bauen muss: ein Plugin, fünf Skripte, ein paar AWS-Befehle (zum Teil wegen der Grenzen von GitHub Pages und des CloudFront-Free-Plans). Wer lieber klickt, ist mit einem gut eingerichteten WordPress bequemer unterwegs. Mir gefällt, dass jede Maßnahme hier in einer Datei steht, die ich nachlesen und mit Git zurückdrehen kann.

Design und Google-Hilfe

Wenn ich mir überlege, was den Prozess getrieben hat, waren das im Wesentlichen zwei Impulse: Ich wollte wirklich Feature-Parität mit dem alten Blog herstellen. Auch, was das Design angeht. Das Theme habe ich etwa Claude Code rekreieren lassen. Ein paar Aufgaben schlummern noch in meiner To-Do-Liste Todoist.

  • DONE: Für eine Zeit hatte ich in den alten Posts noch Amazon Polly verbaut mit einer Vorlesefunktion. Das wollte ich wieder haben, aber etwas besser machen. In der Arbeit setzen wir Elevenlabs ein mit den Stimmen von v4. Damit wollte ich vertrauter werden. Also habe ich mir eine Anwendung bauen lassen, die nach und nach per Anforderungsfunktion die Sprachdateien erzeugt. Dafür habe ich mit drei Minuten Tonaufnahmen meiner Stimme Elevenlabs meine Stimme klonen lassen. Vielleicht ist das gruselig, ich bin mir noch nicht sicher. Gebt mir Feedback über das Kontaktformular oder meine Social Profiles, solange es noch keine Kommentare gibt.

To Dos

  • Trennlinien zwischen den Posts
  • related links zu thematisch passenden Posts auf krautsource.info

tl;dr

  • Anlass: PHP2Shell, zwei kritische PHP-Lücken. Ich wollte keinen PHP-Server mehr nebenbei am Laufen halten, für ein Blog, das ich zeitlich kaum noch füttere.
  • Umzug: Von WordPress auf Jekyll und GitHub Pages, die Bilder bleiben in Amazon S3 und CloudFront. Kein Server mehr, keine monatlichen Updates, keine 10 bis 15 Euro im Monat, und mein Freund ist die Pflicht los.
  • PageSpeed (mobil, ein Artikel als Beispiel): Leistung 93, Barrierefreiheit 100, Best Practices 100, SEO 100.
  • Was dafür nötig war: Responsive Bilder per kleinem Plugin, WebP und AVIF für alle Bilder (je nach Bild 31 bis 67 % kleiner als JPEG), ein Jahr Cache auf allen Bildern, Klaro und Schriften auf eine eigene Domain, das erste Bild sofort laden, feste Bildmaße. Das Meiste ist ein einmaliges Skript oder ein Eintrag in einer Datei.
  • Bei WordPress erledigt der Kern vieles davon schon (responsive Bilder, Lazy Loading, LCP-Hinweis). Der Rest, vor allem Formate, Cache und Hosting, ist ein Stapel aus Plugins, Servereinstellungen und Diensten, die man pflegen muss.
  • Größere Inhalte (hier eine 7,5-MB-Visualisierung) lade ich erst auf Klick, mit Größenangabe.