Zum Inhalt springen
Alle Artikel

Wie diese Website gebaut ist: statisch, wo es geht, streamend, wo es muss

Diese Website ist auch ein Referenzprojekt. Sie hat drei Sprachen, Dutzende Seiten, mehr als ein Dutzend interaktive Tools und einen KI-Guide, der antwortet, während er schreibt. Sie erreicht in allen vier Lighthouse-Kategorien auf dem Handy 100 und läuft auf den Gratis-Stufen von Azure Static Web Apps und der Gemini-API. Nichts davon braucht einen exotischen Stack: React, Vite, ein Prerender-Schritt und eine Handvoll Prüfungen, die nichts Kaputtes ausliefern. So greifen die Teile ineinander, inklusive zweier Dinge, die ich erst durch Messen herausgefunden habe.

10 Min. Lesezeit

1. Eine Datenquelle, viele Ausgaben

Artikel, Leistungsseiten, Projekte und Tools liegen als typisierte Daten im Repository, eine Datei pro Artikel und pro Leistung. Der Build macht daraus alles, was Lesende oder Maschinen brauchen: die Seiten selbst, einen Index für Listen, eine Sitemap mit allen drei Sprachversionen jeder URL, einen RSS-Feed pro Sprache, ein Teilen-Bild pro Artikel und Sprache, die llms.txt-Zusammenfassung für KI-Assistenten und die Wissensdatei, aus der der KI-Guide antwortet.

Die letzten beiden wurden früher von Hand gepflegt und liefen beim ersten Produkt-Umbenennen auseinander. Jetzt erzeugt ein Skript sie bei jedem Build aus denselben Daten neu, und eine Prüfung schlägt fehl, wenn die eingecheckte Fassung nicht passt. Nichts wird doppelt geschrieben, also kann sich nichts widersprechen.

json
// package.json
"build": "node scripts/gen-posts-meta.mjs && node scripts/gen-knowledge.mjs
          && tsc --noEmit -p tsconfig.app.json && vite build
          && node scripts/sitemap.mjs && node scripts/prerender.mjs"

2. Jede Seite vorrendern, in jeder Sprache

Die Website ist eine React-App, aber niemand lädt eine leere Hülle. Nach dem Vite-Build startet ein Skript die gebaute Website lokal, öffnet jede Route in jeder Sprache in einem Headless-Chrome und speichert das fertige HTML in eine eigene Datei: jede Seite mal drei Sprachen. Jede hat eigenen Titel, eigene Beschreibung, kanonische URL, hreflang-Links auf die anderen beiden Sprachen und strukturierte Daten für Artikel, FAQs und Breadcrumbs. Derselbe Lauf erstellt pro Artikel ein Teilen-Bild.

Sprachen stecken im Pfad: Englisch im Root, Deutsch unter /de, Spanisch unter /es. Azure Static Web Apps liefert jede vorgerenderte Datei direkt über Rewrite-Regeln aus, die der Build erzeugt, und eine Sperre lässt den Build fehlschlagen, wenn zwei Regeln kollidieren, was einmal mit /de und /de/ passiert ist. Für Crawler ist das gewöhnliches statisches HTML; für Menschen übernimmt die React-App, und jeder weitere Klick ist sofort da.

Ein Repository, eine Pipeline: statisches HTML für jede Seite und Sprache, eine streamende Function für den KI-Guide und Prüfungen vor und nach jedem Deploy.
Architektur dieser Website: Inhaltsdaten im GitHub-Repository werden von GitHub Actions gebaut (Daten erzeugen, Typprüfung, Vite-Build, Sitemap, jede Seite mit Headless-Chrome vorrendern, End-to-End-Tests) und auf Azure Static Web Apps ausgeliefert; Besucher erhalten statisches HTML pro Sprache mit Inline-Sprachweiterleitung, und der KI-Guide ruft eine verwaltete Azure Function auf, die Antworten von Gemini 3.5 Flash mit Flash-Lite als Rückfall streamt; nach Deploys wird IndexNow benachrichtigt, und geplante Jobs prüfen Seiten stündlich und Links wöchentlich.

3. Den ersten Paint nicht verschenken

Drei Messungen haben den Code verändert. Erstens wurden deutsch- und spanischsprachige Besucher auf der Startseite erst nach dem 150 KB grossen Haupt-Bundle nach /de oder /es weitergeleitet, was auf dem Handy eine Sekunde kostete. Die Weiterleitung läuft jetzt als zehnzeiliges Inline-Skript ganz oben im HTML. Die Content Security Policy erlaubt genau dieses Skript über seinen SHA-256-Hash, und der Build schlägt fehl, wenn Skript und Hash auseinanderlaufen.

Zweitens ersetzte React die vorgerenderte Seite durch einen leeren Platzhalter, bis der Code-Chunk der Seite ankam, also blinkte der Inhalt, und Lighthouse zählte den zweiten Paint. Jetzt lädt der Browser diesen Chunk, bei Artikeln auch den Artikel selbst, bevor React startet, und das vorgerenderte HTML bleibt stehen, bis React dasselbe anzeigt. Drittens sprang die Webschrift nach dem ersten Paint ein und verschob den Text. Mit vorgeladener Schrift und font-display optional ist sie entweder rechtzeitig da oder wird für diesen Besuch nicht genutzt. Zusammen senkte das den grössten Paint auf dem Handy auf der Startseite von etwa 2,5 auf 1,1 Sekunden.

html
<!-- index.html: runs before CSS, fonts and the app bundle -->
<script>
  (function () {
    var l = location, p = l.pathname, s = new URLSearchParams(l.search), q = s.get("lang"), w;
    if (/^\/(de|es)(\/|$)/.test(p)) return;
    var ok = function (x) { return x === "en" || x === "de" || x === "es"; };
    try { w = ok(q) ? q : p === "/" ? [localStorage.getItem("dominik-lang"), (navigator.language || "").slice(0, 2)].filter(ok)[0] : ""; } catch (e) {}
    if (w !== "de" && w !== "es") return;
    s.delete("lang");
    var r = s.toString();
    l.replace("/" + w + (p === "/" ? "" : p) + (r ? "?" + r : "") + l.hash);
  })();
</script>
<!-- staticwebapp.config.json: script-src 'self' 'sha256-…' -->

4. Ein KI-Guide, der auf einem statischen Host streamt

Der Guide unten rechts ist meine eigene Chat-Engine, die hinter dem Produkt Helptempo: eine Web-Komponente im Shadow DOM, damit das CSS der Website sie nicht zerschiessen kann, und ein Server-Handler, der auf jedem Host läuft. Auf dieser Website läuft der Handler als verwaltete Azure Function. Er antwortet aus der generierten Wissensdatei plus den Überschriften der Seite, auf der die Besucherin gerade ist, mit Gemini 3.5 Flash und Gemini 3.5 Flash-Lite als Rückfall, der nur einspringt, bevor das erste Wort gesendet ist. Vorbereitete Antworten machen die vorgeschlagenen Einstiegsfragen sofort, und ein Limit pro Besucher hält den Gratis-Key am Leben.

Fürs Streaming brauchte es das Programmiermodell v4 von Azure Functions mit eingeschalteten HTTP-Streams. Die Überraschung war ein einziger Header: Mit Content-Type 'text/event-stream; charset=utf-8' hielt Azure Static Web Apps die ganze Antwort zurück, bis sie fertig war; mit genau 'text/event-stream' kam jedes Stück an, sobald es geschrieben war. Ich habe das mit acht Varianten auf einer Preview-Umgebung eingegrenzt, bevor ich die Produktion angefasst habe. Jetzt erscheinen die ersten Worte nach etwa 1,2 bis 1,5 Sekunden statt der ganzen Antwort nach 2,5 bis 3.

js
// api/chat/index.js — Azure Functions v4 on Static Web Apps
const { app } = require("@azure/functions");
app.setup({ enableHttpStream: true });
app.http("chat", { methods: ["GET", "POST"], authLevel: "anonymous", route: "chat",
  handler: async (req) => (await chat).azureFunctions(req) });

// in the engine: exactly this content type, no charset suffix
// "Content-Type": "text/event-stream"

5. Prüfungen, die das Ausliefern verweigern

Jeder Push auf main baut die Website, prüft die Typen und führt End-to-End-Tests gegen die gebauten Dateien aus, bevor irgendetwas ausgeliefert wird: Jede Route in jeder Sprache muss in Handy- und Desktop-Breite genau ein h1, kein horizontales Scrollen, keine kaputten Bilder und keine Konsolenfehler haben. Der Fragebogen, der Sprachwechsel, geteilte Tool-Links und der Startablauf haben eigene Prüfungen, darunter eine, die das JavaScript ausbremst und fehlschlägt, sobald die vorgerenderte Überschrift verschwindet. Die Chat-API hat eigene Prüfungen, und das Wissen des KI-Guides wird gegen 30 Besucherfragen mit Fakten getestet, die er sagen muss und nicht sagen darf.

Nach dem Deploy meldet die Website über IndexNow, welche URLs sich geändert haben. Ausserhalb der Deploys prüft ein geplanter Job jede Stunde, dass alle Seiten, Artikel, Leitfaden-PDFs, Feeds und der Chat antworten, und ein wöchentlicher Job prüft jeden Quellenlink und jedes Video in den Artikeln. Ein kaputter Link oder eine tote Seite färbt einen Workflow rot und landet in meinem Postfach, bevor ein Besucher darüber stolpert.

6. Was es kostet, und was ich wiederverwenden würde

Gehostet wird im Free-Plan von Azure Static Web Apps mit seinen verwalteten Functions, und der Guide läuft auf einem Gratis-Gemini-Key mit Rückfallmodell, die laufenden Kosten liegen also nahe null. Die Kompromisse sind ehrliche: Das Gratis-Kontingent des Modells ist tagsüber manchmal aufgebraucht und das Rückfallmodell antwortet, und ein statischer Host heisst, dass das Kontaktformular an einen Formulardienst sendet statt an ein eigenes Backend.

Was ich für jede Firmenwebsite wiederverwenden würde: vorrendern statt Server-Rendering, wenn sich Inhalte ein paar Mal am Tag ändern, jede maschinenlesbare Datei aus einer Datenquelle erzeugen, messen vor dem Optimieren und die Pipeline das Ausliefern verweigern lassen, statt darauf zu bauen, dass Menschen an die Prüfungen denken. Die Chat-Engine ist von Grund auf wiederverwendbar; deshalb ist sie ein Produkt geworden.

Fazit

Eine schnelle, mehrsprachige Website mit Live-KI-Guide braucht keine Serverfarm: alles vorrendern, den ersten Paint schützen, nur streamen, was streamen muss, und die Pipeline Nein sagen lassen.

Weitere Artikel

    Kontakt aufnehmen

    Erzählen Sie mir von Ihrem Projekt, ich antworte innerhalb von 1–2 Werktagen.

    Dominik Heinrich
    Unternehmen
    MADO Consulting

    Weltweit verfügbar, remote. Vor Ort auf Anfrage, je nach Land.

    Ich antworte innerhalb von 1–2 Werktagen. Freie Termine: siehe Kalender

    Öffnet hier den Buchungskalender von Calendly. Beim Laden werden Daten an Calendly übertragen (siehe Impressum).