wernicke IT
Astro Tailwind Webentwicklung Performance

Neue Website: Warum ich auf Astro gesetzt habe

Wer selbst Webauftritte baut, hat die eigene Website meistens am längsten auf der To-do-Liste. Das war bei mir nicht anders. Die alte Seite war funktional, aber sie war über die Zeit gewachsen und hatte mehr Ballast als nötig. Irgendwann hat man genug davon, sich um Dinge zu kümmern, die man eigentlich längst wegwerfen müsste.

Der Neuaufbau war eine gute Gelegenheit, konsequent auf das Wesentliche zu reduzieren.

Warum Astro

Die Anforderungen waren klar: statische Ausgabe, kein JavaScript-Framework, minimale Abhängigkeiten, saubere Content-Verwaltung. Astro erfüllt das von Haus aus.

Astro rendert standardmäßig kein JavaScript aus. Was im Browser landet, ist HTML und CSS. Interaktive Elemente werden nur dort eingebunden, wo sie wirklich gebraucht werden. Für eine Seite wie diese, die primär Inhalte präsentiert, ist das genau das richtige Modell.

Hinzu kommt die Content Layer API, die seit Astro 5 stabil ist. Collections lassen sich über ein Zod-Schema typsicher definieren, und der glob()-Loader lädt Markdown-Dateien aus einem Verzeichnis. Das Ergebnis ist ein vollständig typisierter Zugriff auf Frontmatter-Felder zur Build-Zeit, ohne Laufzeit-Overhead.

Tailwind v4

Mit Tailwind v4 hat sich das Setup grundlegend verändert. Es gibt keine tailwind.config.js mehr. Konfiguration findet in der CSS-Datei statt:

@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));

Das klingt nach weniger Kontrolle, ist aber das Gegenteil. CSS Custom Properties ersetzen die JavaScript-Konfiguration, Theme-Variablen werden direkt im Stylesheet definiert, und @theme erlaubt es, das Spacing- und Farbsystem zu erweitern. Für jemanden, der CSS kennt, ist das deutlich intuitiver als der JS-Config-Ansatz.

Einen Nachteil gibt es: CSS-Language-Server wie der in Zed eingebaute kennen @plugin und @custom-variant nicht und melden sie als ungültig. Das ist ein Editor-Problem, kein Code-Problem. Temporärer Workaround über .zed/settings.json:

{
  "lsp": {
    "vscode-css-language-server": {
      "settings": { "css": { "validate": false } }
    }
  }
}

Content Collections und i18n

Die Seite ist zweisprachig (Deutsch und Englisch). Statt eines i18n-Plugins habe ich einen pragmatischen Ansatz gewählt: je eine Markdown-Datei pro Sprache in der pages-Collection.

src/content/pages/
  home-de.md
  home-en.md

Die Frontmatter-Felder enthalten alle strukturierten Texte (Hero, Sections, Kontakttext), der Markdown-Body die Fließtexte. Astro’s render() wandelt den Body in HTML um, <Content /> bindet ihn ein. Das trennt Struktur und Text sauber voneinander, ohne dass man dafür ein vollständiges CMS braucht.

Blog-Posts folgen demselben Muster. Ein lang-Feld im Frontmatter steuert, welche Posts auf /blog/ und welche auf /en/blog/ erscheinen.

Dark Mode ohne Flash

Dark Mode per prefers-color-scheme ist bequem, aber sobald man dem Nutzer die Wahl lassen will, braucht man eine eigene Lösung. Die typische Herausforderung: localStorage lässt sich nur per JavaScript auslesen, JavaScript läuft erst nach dem Render. Ohne Vorkehrungen blitzt der falsche Modus kurz auf.

Das lässt sich mit einem kleinen Inline-Script im <head> verhindern, das synchron ausgeführt wird, bevor der Browser irgendetwas rendert:

<script is:inline>
  (function () {
    const t = localStorage.getItem('theme') || 'dark';
    if (t === 'dark') document.documentElement.classList.add('dark');
  })();
</script>

Das Script ist bewusst minimal und hat keine Abhängigkeiten. Es setzt die .dark-Klasse auf <html>, bevor das erste Pixel gerendert wird.

Eine interessante Konsequenz davon: SVG-Logos lassen sich nicht über prefers-color-scheme in einem <picture>-Element umschalten, weil das Media Query und nicht die Klasse auswertet. Die Lösung sind zwei <img>-Tags mit Tailwind-Utility-Klassen:

<img src="/images/logo-light.svg" class="block dark:hidden" />
<img src="/images/logo-dark.svg"  class="hidden dark:block" aria-hidden="true" />

Einfach, vorhersehbar, funktioniert.

Typografie und Schrift

Als Schrift habe ich mich für Atkinson Hyperlegible Next entschieden. Die Schrift wurde speziell für bessere Lesbarkeit und Zugänglichkeit entwickelt, unterscheidet ähnliche Zeichen konsequenter als die meisten Systemschriften und wirkt trotzdem nicht klinisch. Selbst gehostet, kein Tracking, kein Google Fonts.

Das @tailwindcss/typography-Plugin liefert die Prose-Stile für Blog-Posts. Inline-Code bekommt einen dezenten teal-getönten Hintergrund statt der Standard-Backtick-Dekoration, die das Plugin sonst hinzufügt:

.prose :where(code):not(:where(pre *)) {
  background-color: rgb(20 184 166 / 0.08);
  border-radius: 0.25rem;
  padding: 0.1em 0.35em;
}
.prose :where(code):not(:where(pre *))::before,
.prose :where(code):not(:where(pre *))::after {
  content: none;
}

TypeScript und Astro

Ein kleiner, aber nerviger Stolperstein: Der TypeScript-Language-Server findet astro:content nicht, wenn tsconfig.json das .astro/-Verzeichnis nicht explizit einschließt. Astro generiert dort die Typ-Deklarationen für Collections via astro sync. Ohne den Eintrag überschreibt das eigene include die Basis-Config:

{
  "extends": "astro/tsconfigs/strict",
  "include": ["src", ".astro/types.d.ts"]
}

Nach dem ersten astro dev oder astro sync sind alle Collection-Typen vollständig, inklusive typsicherer Frontmatter-Felder.

Was übrig bleibt

Keine Cookies, kein Analytics, kein JavaScript-Framework. Eine statische HTML-Ausgabe, die auf einem einfachen Webserver läuft. Performance und Datenschutz sind damit nicht das Ergebnis von Optimierungsarbeit, sondern von bewussten Entscheidungen im Aufbau.

Das ist der richtige Ansatz für eine Seite dieser Größe. Und wahrscheinlich auch für die meisten anderen.


Fragen zum Aufbau oder Interesse an einem ähnlichen Webauftritt? Sprechen Sie mich gerne an.