Zurück zum Blog ·

Der statische Generator ohne Abhängigkeiten

Diese Website wird von einem einzigen Python-Skript gebaut. Kein Hugo, kein Jekyll, kein Eleventy, kein Framework, kein pip install. Nur build.py, das Python-Standardbibliothek importiert und sonst nichts. Wenn du das seltsam findest – ich verstehe das. Es gibt dafür ausgereifte Generatoren mit tausenden Nutzern, Themes, Plugins. Warum etwas Eigenes bauen?

Die Antwort ist nicht „weil ich es kann". Es ist: weil ich verstehen will, was da passiert.

Die Ausgangslage

Als ich die Seite aufsetzte, schaute ich mir kurz an, was ein statischer Generator für mich tun müsste: Markdown einlesen, in HTML umwandeln, in ein Template stecken, Dateien rausschreiben. Das ist kein Hexenwerk – es ist Textverarbeitung. Und Textverarbeitung in Python ist, was die Standardbibliothek am besten kann.

Ich hatte zwei Optionen:

Für eine Seite mit ein paar Seiten und einem Blog ist die zweite Option nicht mutig, sondern schlicht die kleinere Wartungslast. Der Generator ist jetzt 850 Zeilen, und jede davon habe ich mit einer konkreten Anforderung geschrieben, nicht aus einem Framework-Default übernommen.

Was das Skript tatsächlich tut

Der Kern ist überschaubar. Ein parse_page() liest Frontmatter und Textkörper aus einer Markdown-Datei. Ein md_to_html() wandelt den Text um – bewusst eine kleine Teilmenge von Markdown: Überschriften, Listen, Zitate, Codeblöcke, Fett, Kursiv, Links, Bilder. Mehr brauche ich nicht, und jede nicht unterstützte Syntax ist ein Feature: Sie kann auch nicht kaputtgehen.

Das Template ist eine einzige base.html mit Platzhaltern wie {{title}}, {{content}}, StartBeratungBlogShop. Der Build ersetzt die Platzhalter und schreibt die fertigen Seiten nach public/. public/ wird bei jedem Lauf komplett gelöscht und neu erzeugt – es gibt keinen Zustand, der hängen bleibt.

Zwei Dinge, die ich bewusst eingebaut habe, weil sie mir vorher bei anderen Projekten wehgetan haben:

Zweisprachigkeit als erste Klasse. Jeder Post existiert in content/de/blog/ und content/en/blog/. Das alt:-Feld im Frontmatter verlinkt die Sprachfassungen gegenseitig, hreflang-Tags zeigen Suchmaschinen, welche Version für welche Sprache ist. Das ist kein Plugin, sondern zehn Zeilen im render().

Cache-Busting über Inhalts-Hash. style.css liegt mit max-age=604800 im Browser-Cache. Ändere ich das CSS, laden Besucher trotzdem bis zu sieben Tage die alte Datei – das neue HTML zur alten CSS. Die Lösung: Der Build berechnet den SHA-256-Hash des Dateiinhalts und schreibt die Datei als style.<hash>.css. Gleicher Inhalt, gleicher Dateiname, kein unnötiger Cache-Bruch. Das ist der Fehler, der mir vorher ein Kontaktformular zerschossen hat – jetzt ist er im Build selbst abgefangen.

Was ich nicht selbst gebaut habe

Ein Generator, der nur meinen Anwendungsfall kennt, ist an einer Stelle gnadenlos ehrlich: Er kann nichts, was ich nicht explizit eingebaut habe. Und genau das ist der Punkt. Ich habe keine generische Lösung gebaut, sondern meine Lösung. Das heißt:

Die Abwägung

Ich würde niemandem raten, für ein großes Projekt einen Generator selbst zu schreiben. Bei einer Seite mit einem Blog, zwei Sprachen und einer Handvoll Seiten ist die Rechnung aber eine andere: Die Lernkurve für ein fremdes Framework ist höher als die für die vier Funktionen, die ich wirklich brauche. Und der Preis der Selbstbaulösung – „ich muss alles selbst machen" – ist hier gleichzeitig der Gewinn: Ich weiß, was jede Zeile tut.

Die ehrliche Kernaussage ist dieselbe wie bei den Backups, über die ich letzte Woche geschrieben habe: Der beste Generator ist nicht der mit den meisten Features, sondern der, den du tatsächlich betreibst und bei dem du nicht raten musst, warum etwas kaputt ist.

Der Quellcode ist die Dokumentation. Er hat 850 Zeilen, keine Abhängigkeiten, und ich kann jede davon erklären.