kohlmajer.de



Webseiten mit Hugo

— zuletzt geändert am
835 Wörter, Lesezeit 5 min

Meine alte Webseite basierte auf dem PHP-Framework Yii. Ich nutze Yii weiterhin z. B. beruflich, aber für diese kleine Webseite ist das einfach zu viel Overhead. Ich war daher auf der Suche nach einem einfachen Webseiten-Generator und habe mich für Hugo entschieden 😊. Hugo ist ein statischer Generator, d. h. am Ende steht ein public-Ordner, den ich auf meinen Webserver hochlade — fertig. Es läuft keine Datenbank, kein PHP, nichts, was man patchen müsste. Bisher bin ich sehr zufrieden.

Ein eigenes Theme

Es gibt Hunderte Hugo Themes, mit denen sich die Webseite anpassen lässt. Ich habe einige ausprobiert, bin damit aber nicht wirklich glücklich geworden. Meine alte Webseite basierte im Wesentlichen auf »Vanilla«-Bootstrap, sodass ich mir selbst ein sehr einfaches Theme zusammengebaut habe:

themes/my-bootstrap-theme
├── assets
│   ├── css
│   │   ├── bootstrap.min.css
│   │   ├── my.css
│   │   └── syntax.css
│   └── js
│       └── bootstrap.bundle.min.js
└── layouts
    ├── baseof.html
    ├── home.html · page.html · section.html · taxonomy.html · term.html · 404.html
    ├── _partials
    ├── _default/_markup      (Render Hooks für Bilder und Links)
    └── shortcodes

In der hugo.yaml (ich verwende eine YAML- und keine TOML-Konfiguration 😇) wird es mit theme: "my-bootstrap-theme" eingebunden. my.css enthält nur, was sich mit Bootstrap-Klassen nicht ausdrücken lässt — ein gutes Dutzend Zeilen. syntax.css färbt die Codeblöcke und ist nicht handgeschrieben, sondern erzeugt:

hugo gen chromastyles --style=monokai > themes/my-bootstrap-theme/assets/css/syntax.css

Wer mag, kann das Theme als ZIP-Datei herunterladen — es ist der Stand dieser Webseite und steht unter der MIT-Lizenz. Eigenständig ist es nicht: Favicon, das Lizenzlogo der Fußzeile und ein paar Site-Parameter liegen im Projekt, nicht im Theme. Was gebraucht wird, steht im README.md im Archiv.

Alles liegt lokal

Bootstrap kommt nicht von einem CDN, sondern als Datei aus assets/. Das ist der Grund, warum ich in den Datenschutzhinweisen zusagen kann, dass beim Aufruf dieser Seite kein Dritter kontaktiert wird — bei einem CDN ginge Ihre IP-Adresse dorthin, ganz ohne Ihr Zutun.

Der Preis dafür: Updates muss ich selbst einspielen. Damit ich das nicht vergesse, gibt es eine Werkstattseite, die die eingebaute Version aus dem Dateikopf liest und mit dem aktuellen Stand vergleicht. Sie ist ein Entwurf und deshalb nie auf dem Server — denn für den Vergleich fragt sie ihrerseits einen Dritten, und genau das soll live ja nicht passieren.

Hugo hängt beim Einbinden einen Hash an (integrity="sha256-…", Subresource Integrity). Passt die Datei nicht dazu, lädt der Browser sie gar nicht erst.

Strenge Content-Security-Policy

Der Webserver schickt eine Content-Security-Policy, die mit default-src 'none' anfängt: erlaubt ist nur, was ausdrücklich dasteht — Stylesheets, Skripte und Bilder von dieser Domain, sonst nichts. Kein unsafe-inline.

Das hat Folgen für den Alltag: kein <style>-Block und kein style-Attribut. Alles Eigene gehört in my.css. Der Haken daran ist, dass ein Verstoß lokal nicht auffällt — hugo server schickt keine CSP, der Browser blockiert also erst auf dem Server 😒.

Ein nonce im HTML hilft übrigens nicht: er müsste bei jeder Antwort neu erzeugt werden, und genau das kann eine statische Seite nicht. Bei mir stand lange einer im Quelltext, fest verdrahtet und damit wirkungslos. Er ist inzwischen weg.

Deploy

Ein kurzes Shell-Skript baut die Seite und spiegelt sie per rsync über SSH auf den Server. Zwei Prüfungen davor haben sich als wichtig erwiesen, weil rsync --delete sonst ziemlich gründlich aufräumt:

  • Ist public/ leer, wird abgebrochen — sonst wäre gleich die ganze Live-Seite gelöscht.
  • Existiert das Zielverzeichnis nicht, wird abgebrochen — bei einem Tippfehler legt rsync sonst stillschweigend ein neues an, und der Deploy geht ins Nichts.

Dazu gibt es --dry-run und --buildonly.

Die Fußzeile zeigt Commit und Zeitstempel des ausgelieferten Builds — letzteren nach RFC 3339, also mit Datum, Uhrzeit und Zeitzone. Das Skript liest Zeitstempel und Commit aus dem Git-Stand und reicht es als HUGO_PARAMS_COMMIT an Hugo durch; sind noch Änderungen uncommittet, steht dort ein -dirty. So sehe ich beim bloßen Ansehen der Seite, was gerade oben liegt. Das Änderungsdatum der einzelnen Seiten kommt aus demselben Topf: enableGitInfo: true holt es aus dem Git-Log. Von Hand gepflegte »Stand: Juni 2025«-Zeilen habe ich dafür überall herausgeworfen — die veralten zwangsläufig.

Bilder

Bilder liegen als Ressourcen im jeweiligen Page Bundle, also im Ordner des Beitrags. Zwei Dinge habe ich dabei gelernt:

Erstens werden alle Dateien in so einem Ordner veröffentlicht, auch die, die keine Seite einbindet. Eine vergessene Druckvorlage geht damit bei jedem Deploy mit hoch.

Zweitens ist das Format wichtiger als die Qualitätsstufe. Die Hochwasserkarten sind Screenshots eines Kartendienstes — feine schwarze Linien und kleine Schrift auf weißer Fläche, also das Schlimmste, was man JPEG vorsetzen kann. Als WebP wiegen sie noch ein Drittel. Ein Render Hook verlinkt jedes Bild automatisch auf eine große Fassung, sofern eine danebenliegt: im Text steht die leichte Variante, die volle Auflösung lädt erst beim Klick.

Was ich wieder herausgeworfen habe

Meine Kontaktdaten standen früher rückwärts im Quelltext, damit Adresssammler sie schwerer finden. Das war keine gute Idee: Screenreader lesen die Buchstabenfolge so vor, wie sie im Dokument steht — also Kauderwelsch. Genutzt hat es ohnehin nichts, denn die security.txt nennt dieselbe Adresse im Klartext, so wie RFC 9116 es vorsieht.

Tags: