Zurück zum Blog

Privacy-First-Analytics in eine Astro-Site einbinden

Astros View Transitions brechen Standard-Analytics-Skripte geräuschlos. Hier ist das richtige Muster, um jeden Routenwechsel ohne Cookies oder Cookie-Banner zu verfolgen.

Astros View Transitions tauschen den Seiteninhalt ohne vollständiges Browser-Reload aus — was bedeutet, dass jedes Analytics-Skript, das auf DOMContentLoaded oder load basiert, nur einmal beim ersten Aufruf feuert. Jede anschließende clientseitige Navigation ist unsichtbar. Das ist kein Konfigurationsfehler deinerseits; es ist eine fundamentale Konsequenz der Funktionsweise des ClientRouter.

Das gleiche Problem mit stiller Navigation besteht in SvelteKit 2 und Remix, aber die Lösung ist in jedem Framework anders. In Astro ist die Antwort das Lifecycle-Event astro:page-load.

Warum der normale Script-Tag versagt

Wenn <ClientRouter /> aktiv ist (opt-in über astro:transitions in Astro 4, standardmäßig aktiviert in Astro 5), laden Navigationen die Seite nicht neu. Astro holt die nächste Seite im Hintergrund, tauscht das DOM aus und aktualisiert die URL — alles ohne den aktuellen JavaScript-Kontext zu zerstören.

Ein Tracking-Skript, das über einen einfachen <script>-Tag geladen wird, wird von Astros Build-Pipeline als gebündeltes Modul-Skript behandelt. Gebündelte Modul-Skripte werden pro Seitensitzung genau einmal ausgeführt. Der Tracker initialisiert sich, feuert einen Pageview für die Landing Page und läuft dann nicht mehr, während der Nutzer navigiert.

Um bei jeder Navigation zu feuern, muss die Initialisierungslogik an den Navigations-Lifecycle von Astro gebunden werden. Der richtige Hook ist astro:page-load, der feuert, nachdem die neue Seite sichtbar ist und alle blockierenden Skripte geladen sind — sowohl beim initialen Laden als auch bei jeder nachfolgenden Transition.

Die korrekte Integration

Füge das Tracking-Skript einmal in dein Root-Layout (src/layouts/Layout.astro oder äquivalent) ein und hänge dann einen Listener für astro:page-load an:

---
// Layout.astro
---
<html lang="de">
  <head>
    <!-- Head-Inhalt -->
  </head>
  <body>
    <slot />

    <script
      is:inline
      src="https://api.monoid.website/tracker.min.js"
      data-site-id="DEINE_SITE_ID"
      async
    ></script>
  </body>
</html>

Die is:inline-Direktive weist Astros Bundler an, dieses Skript genau so zu lassen, wie es ist. Ohne sie würde Astro das Skript als Modul verarbeiten, es deduplizieren und die Wiederausführung unterdrücken. Mit is:inline wird der Script-Tag wortgetreu in das HTML jeder Seite ausgegeben.

Für Sites ohne View Transitions ist das alles, was du brauchst. Der Tracker feuert einmal beim Laden, fertig.

Navigation mit View Transitions handhaben

Wenn deine Site <ClientRouter /> verwendet, feuert der eingebaute history.pushState-Hook des Trackers korrekt nach jeder Transition — der Tracker lauscht intern auf Astros Navigationsereignisse. Keine zusätzliche Konfiguration erforderlich.

Zur Verifikation öffne den Netzwerk-Tab in den DevTools und filtere nach collect. Navigiere zwischen zwei Seiten. Du solltest pro Navigation eine POST-Anfrage sehen, einschließlich des initialen Ladens.

Wenn du den Tracker bedingt einbindest und ihn nach jedem DOM-Tausch neu initialisieren musst (z. B. nachdem eine dynamische Island eingebunden wurde), kannst du eine erneute Ausführung manuell auslösen:

<script data-astro-rerun>
  // Dieser Block wird nach jeder View Transition erneut ausgeführt.
  // Sparsam verwenden — die meiste Tracker-Logik sollte nicht hier stehen.
  if (window.__monoid) {
    window.__monoid.trackPageview();
  }
</script>

Das Attribut data-astro-rerun zwingt Inline-Skripte, nach jeder Transition erneut zu laufen. Beachte, dass es is:inline impliziert und daher nur für nicht gebündelte Skripte gilt.

Umgebungen trennen

Astro-Projekte haben typischerweise eine lokale Entwicklungs-, eine Preview- und eine Produktionsumgebung. Verwende eine Umgebungsvariable für die site_id und unterdrücke das Tracking in der Entwicklung:

---
const siteId = import.meta.env.PUBLIC_MONOID_SITE_ID
---

{siteId && (
  <script
    is:inline
    src="https://api.monoid.website/tracker.min.js"
    data-site-id={siteId}
    async
  ></script>
)}

Setze PUBLIC_MONOID_SITE_ID in .env für die Produktion und lass sie lokal undefiniert. Astro stellt Variablen mit dem Präfix PUBLIC_ dem Client zur Verfügung; serverseitige Variablen (ohne Präfix) sind im browserseitig ausgeführten Code nicht sichtbar.

Was du nicht brauchst

Kein Cookie-Consent-Banner. Keine CMP-Integration. Der Collection-Endpunkt berechnet einen täglichen Besucher-Hash aus IP-Adresse, User-Agent, einem serverseitigen Geheimnis und dem aktuellen Datum:

visitor_hash = SHA-256(IP + UA + SALT_SECRET + YYYY-MM-DD)

Der Hash wird alle 24 Stunden zurückgesetzt. Auf dem Gerät des Besuchers wird nichts gespeichert. Browser-Familie und Gerätetyp werden serverseitig aus dem Request-User-Agent für aggregierte Berichte abgeleitet — vollständige User-Agent-Strings, Browser-Versionen und persistente Identifikatoren werden nie gespeichert.

Astro-Sites sind oft vollständig statisch oder Edge-gerendert mit minimalem JavaScript. Ein Analytics-Skript mit weniger als 2 KB ohne Cookies und ohne Consent-Anforderung fügt sich natürlich in diese Philosophie ein. Es gibt kein npm-Paket zu pflegen, kein SDK zu aktualisieren und keine DSGVO-Rechtsgrundlage für die Analytics-Verarbeitung zu dokumentieren.