Dieser Blog ist eine statische Seite auf Basis von Astro 7, entstanden an einem einzigen Wochenende, und dieser erste Beitrag erzählt, wie es dazu kam. Zum einen, weil das Schreiben selbst die komplette Publishing-Pipeline einmal durchtestet. Zum anderen, weil ein paar der Entscheidungen (und Fehlschläge) unterwegs es wert sind, geteilt zu werden.
Die Kurzfassung: Ich habe einen phasenweisen Plan geschrieben, ihn Claude Code als Arbeitsanweisung für das Projekt gegeben und jeden Diff vor dem Commit selbst reviewt. Die eigentliche Arbeit hat der Plan geleistet. Interessant wird es jedoch an den Stellen, an denen die Realität dazwischenfunkt.
Statisch, und zwar bewusst
Die erste Entscheidung war die einfachste: kein SSR. Ein persönlicher Blog hat keinen Zustand pro Request. Zwischen zwei Besuchern, die denselben Artikel aufrufen, ändert sich nichts. Vorgerendertes HTML ist für SEO mindestens so gut wie Server-Rendering und läuft auf günstigem Static Hosting statt auf einem Node-Prozess, den jemand am Leben halten muss.
Es gibt genau eine Stelle, an der SSR verlockend wäre: die Sprache des Besuchers zu erkennen. Das übernimmt stattdessen ein kleines Inline-Script. Beim ersten Besuch, ohne gespeicherte Präferenz, prüft es navigator.language, leitet / einmalig auf /de/ um, wenn der Browser auf Deutsch steht, und speichert die Wahl. Bei jedem weiteren Besuch tut es nichts. Kein Server nötig, und es übergeht niemals eine Entscheidung, die der Leser selbst getroffen hat.
Domain-Logik vor UI
Der Plan hat eine Reihenfolge erzwungen, die ich inzwischen jedem empfehlen würde, der auch nur eine kleine Seite baut: zuerst die gesamte nicht-visuelle Logik, als pure Functions, mit Tests, bevor auch nur eine Komponente existiert. Slugs, Datumsformatierung, Lesezeit, Draft-Filterung, verwandte Beiträge, Übersetzungs-Lookup, Feed-Transformation. Nichts davon importiert irgendetwas aus Astro:
/** Permalink path: EN unprefixed, DE under /de/. */
export function pathOf(post: BlogPostLike): string {
const prefix = post.data.lang === 'de' ? '/de' : '';
return `${prefix}/blog/${slugOf(post)}/`;
}
Die Funktionen arbeiten gegen ein strukturelles Interface, das beschreibt, wie ein Content-Eintrag aussieht, nicht gegen Astros eigene Typen. Damit waren sie in reinem Vitest trivial testbar, und das hat sich sofort ausgezahlt. Die Draft-Regel (“Drafts landen in Production niemals im Build, nirgendwo”) ist eine einzige getestete Funktion, angewendet in jedem getStaticPaths(), in den Feeds und damit auch in der Sitemap. Sie ist keine Konvention, an die sich jemand erinnern muss.
Zwei Sprachen ohne i18n-Framework
Die Seite ist zweisprachig: Englisch ohne Präfix, Deutsch unter /de/. Sie nutzt dafür nichts außer Astros eingebautem i18n-Routing und einem typisierten Dictionary. Bei nur zwei Sprachen ist ein vollwertiges i18n-Framework Overhead. Englisch ist ein einfaches Objekt, das die Keys definiert, Deutsch ist ein Partial davon, und eine zehnzeilige Fallback-Funktion liefert Englisch für alles, was fehlt. Der Compiler kennt jeden Key. Ein Tippfehler in einem Übersetzungs-Key ist damit ein Build-Fehler und keine Seite, die stillschweigend auf Englisch bleibt.
Eine Regel aus dem Plan, die ich inzwischen sehr zu schätzen weiß: Der Sprachumschalter verlinkt immer auf die Übersetzung der aktuellen Seite, niemals auf die Startseite der anderen Sprache. Hat ein Artikel keine Übersetzung, verschwindet der Umschalter, statt den Leser irgendwo abzuladen, wo er nicht hinwollte.
Design-Tokens gegen den Template-Look
Die visuelle Identität stand fest, bevor eine einzige Zeile CSS existierte: sechs benannte Farben pro Modus, zwei Schriften (IBM Plex Mono für Display, IBM Plex Sans für Fließtext, self-hosted über Astros Fonts-API) und genau ein Signature-Element. Dieses Element ist die bernsteinfarbene Rechtecksignal-Linie unter dem Header, eine Anspielung auf Signalverläufe und Lastkurven aus der Energiewelt, in der ich arbeite. Dark und Light Mode tauschen lediglich die Werte der Custom Properties; duplizierte Styling-Logik gibt es nirgendwo.
Was tatsächlich schiefgegangen ist
Astros CSP-Support gegen Shiki. Der Plan enthielt einen optionalen Punkt: Astros eingebauten Content-Security-Policy-Support aktivieren. Im echten Browser getestet, gingen zwei Dinge gleichzeitig kaputt. Das Syntax-Highlighting verlor sämtliche Token-Farben, weil hash-basiertes style-src keine Inline-style=""-Attribute abdeckt, und genau so färbt Shiki seine Tokens. Und eines der Inline-Scripts (Theme-Umschaltung vor dem ersten Paint) wurde blockiert. Für einen statischen Blog ohne User-Input lohnt sich dieser Tausch nicht. Dokumentiert, zurückgebaut, weitergemacht.
Trailing Slashes. Interne Links erzeugten /blog/post, während Canonical-URLs und hreflang-Alternates /blog/post/ sagten. Beides funktionierte im Browser, und genau deshalb überlebt diese Sorte Inkonsistenz, bis jemand genau hinschaut. Eine Zeile in pathOf hat es überall gleichzeitig behoben. Das ist der Vorteil, wenn jeder Link durch eine einzige getestete Funktion läuft.
Ein lokaler DNS-Cache mit langem Gedächtnis. Nachdem die Domain über Cloudflare auf Render verdrahtet war, war die Seite weltweit erreichbar. Außer auf genau dem Rechner, der sie gebaut hatte. macOS hatte die negative DNS-Antwort aus der Zeit gecacht, bevor die Records existierten. Falls du jemals eine frische Domain deployst und dein eigener Browser darauf besteht, dass sie nicht existiert, während dein Handy sie problemlos lädt: sudo dscacheutil -flushcache.
Der Workflow
Der Teil des Setups, den ich über alles andere stellen würde, ist die Pausendisziplin. Nach jeder Phase: zusammenfassen, geänderte Dateien auflisten, Tests laufen lassen, anhalten, den Diff reviewen und erst dann committen. Ein AI Pair Programmer ist schnell. Die Pausen sind das, was jede Änderung reviewbar und die Architektur langweilig gehalten hat, im besten Sinne des Wortes.
Diesen Artikel zu veröffentlichen ist der letzte Test der Pipeline: eine Markdown-Datei, ein Commit, ein Push, und Render baut die Seite neu. Wenn du das hier liest, hat es funktioniert.