„Mein Dark Mode Toggle funktioniert nicht mehr, nachdem ich View Transitions aktiviert habe.“ – Diesen Satz hören wir bei Never Code Alone in fast jedem Astro-Projekt. Der Grund ist simpel, aber die Lösung erfordert Verständnis: Mit dem ClientRouter ändert sich das Verhalten eurer Scripts grundlegend. Die gewohnte DOMContentLoaded-Logik greift nicht mehr, und genau hier kommt astro:page-load ins Spiel.
Seit über 15 Jahren begleiten wir bei Never Code Alone Teams bei der Migration zu modernen Frontend-Architekturen. View Transitions gehören zu den spannendsten Features, die das Web in den letzten Jahren hervorgebracht hat – aber sie bringen auch neue Herausforderungen mit sich. In diesem Artikel zeigen wir euch, wie ihr das astro:page-load Event richtig einsetzt und typische Fallstricke vermeidet.
Was ist das astro:page-load Event und wann wird es ausgelöst?
Das astro:page-load Event ist Teil von Astros View Transition Lifecycle. Es feuert am Ende des Navigationsprozesses – nachdem der neue Content geladen, das DOM ausgetauscht und alle neuen Scripts ausgeführt wurden. Der entscheidende Punkt: Dieses Event wird sowohl beim initialen Seitenaufruf als auch bei jeder Navigation per View Transition ausgelöst.
Bei klassischem Full-Page-Load setzt der Browser den kompletten Window-State zurück, lädt alle Scripts neu und triggert DOMContentLoaded. Mit View Transitions passiert das nicht. Astro führt stattdessen einen Soft-Load durch, bei dem der DOM-Inhalt ausgetauscht wird, aber der Window-State erhalten bleibt. Das ist performanter, bedeutet aber: Eure Scripts werden nicht automatisch neu ausgeführt.
Hier ist der Lifecycle im Überblick:
Navigation Start
↓
astro:before-preparation → Content wird geladen
↓
astro:after-preparation → Content ist geladen
↓
astro:before-swap → Altes DOM noch aktiv
↓
[DOM wird ausgetauscht]
↓
astro:after-swap → Neues DOM aktiv
↓
[Scripts werden ausgeführt]
↓
astro:page-load → Navigation abgeschlossen
Warum funktioniert DOMContentLoaded nicht mehr mit View Transitions?
Die kurze Antwort: Weil es kein neues Dokument gibt. DOMContentLoaded feuert genau einmal – wenn das initiale HTML-Dokument vollständig geparst wurde. Bei View Transitions wird kein neues Dokument geladen, sondern nur der Inhalt des bestehenden Dokuments ausgetauscht.
Das ist ein fundamentaler Unterschied zu klassischer Navigation. Hier ein Beispiel, das nach Aktivierung von View Transitions bricht:
// ❌ Funktioniert nur beim ersten Seitenaufruf
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('.hamburger-menu').addEventListener('click', toggleMenu);
});
Die Lösung ist simpel – ihr ersetzt DOMContentLoaded durch astro:page-load:
// ✅ Funktioniert bei jedem Seitenaufruf
document.addEventListener('astro:page-load', () => {
document.querySelector('.hamburger-menu').addEventListener('click', toggleMenu);
});
Der Event-Listener auf astro:page-load wird auch beim initialen Seitenaufruf getriggert, ihr braucht also keine Fallback-Logik.
Wie verhindert ihr Cannot read properties of null Fehler?
Das ist der häufigste Bug, den wir in Projekten sehen. Das Problem: astro:page-load feuert auf jeder Seite, aber euer Element existiert vielleicht nur auf einer bestimmten Seite. Wenn ihr dann querySelector aufruft und direkt darauf zugreift, kracht es:
// ❌ TypeError wenn #some-button nicht existiert
document.addEventListener('astro:page-load', () => {
document.querySelector('#some-button').addEventListener('click', handleClick);
});
Die Lösung ist defensive Programmierung – prüft immer, ob das Element existiert:
// ✅ Sicher auf allen Seiten
document.addEventListener('astro:page-load', () => {
const button = document.querySelector('#some-button');
if (button) {
button.addEventListener('click', handleClick);
}
});
Alternativ könnt ihr mit Optional Chaining arbeiten, wenn ihr nur eine Methode aufrufen wollt:
document.addEventListener('astro:page-load', () => {
document.querySelector('#some-button')?.addEventListener('click', handleClick);
});
Welche Alternative gibt es zu astro:page-load für komponentenspezifische Logik?
Web Components sind hier eure beste Wahl. Der Vorteil: Die connectedCallback Methode wird automatisch aufgerufen, wenn das Element ins DOM eingefügt wird – egal ob beim initialen Load oder nach einem View Transition Swap.
// ✅ Funktioniert mit und ohne View Transitions
<alert-button>
<button class="alert">Click me!</button>
</alert-button>
<script>
customElements.define('alert-button', class extends HTMLElement {
connectedCallback() {
this.querySelector('button').addEventListener('click', () => {
alert('Button was clicked!');
});
}
});
</script>
Dieser Ansatz hat mehrere Vorteile: Ihr braucht keine globalen Event-Listener, die auf jeder Seite feuern. Die Logik ist gekapselt und leicht testbar. Und wenn ihr View Transitions später wieder entfernt, funktioniert alles weiter.
Wie initialisiert ihr Third-Party Scripts bei View Transitions?
Analytics, Chat-Widgets, A/B-Testing-Tools – all diese Scripts erwarten einen Full-Page-Load. Bei View Transitions verschwinden sie oft nach der ersten Navigation. Der Trick: Ladet sie explizit bei jedem astro:page-load neu.
<script is:inline>
function loadAnalytics() {
// Existierendes Script entfernen
const existing = document.getElementById('analytics-script');
if (existing) existing.remove();
// Neues Script einfügen
const script = document.createElement('script');
script.id = 'analytics-script';
script.src = 'https://analytics.example.com/script.js';
document.body.appendChild(script);
}
document.addEventListener('astro:page-load', loadAnalytics);
</script>
Wichtig ist hier das is:inline Attribut. Es verhindert, dass Astro das Script bundelt und dedupliziert. Bei Third-Party Scripts wollt ihr genau diese Kontrolle behalten.
Für komplexere Integrationen wie Chat-Widgets müsst ihr oft zusätzlich die Cleanup-Logik implementieren:
document.addEventListener('astro:before-swap', () => {
// Widget vor dem Swap aufräumen
window.chatWidget?.destroy?.();
});
document.addEventListener('astro:page-load', () => {
// Widget nach dem Swap neu initialisieren
window.chatWidget?.init?.();
});
Was ist der Unterschied zwischen astro:page-load und astro:after-swap?
Beide Events signalisieren, dass der DOM-Austausch stattgefunden hat, aber zu unterschiedlichen Zeitpunkten:
astro:after-swap feuert sofort nach dem DOM-Swap, bevor neue Stylesheets geladen und neue Scripts ausgeführt werden. Ideal für visuelle Anpassungen, die keinen Flash verursachen sollen – wie das Setzen des Dark Mode Themes.
astro:page-load feuert am Ende des gesamten Navigationsprozesses, nachdem alle neuen Scripts ausgeführt wurden. Ideal für Initialisierungen, die auf vollständig gerenderte Komponenten zugreifen müssen.
Praktisches Beispiel für Dark Mode:
// ✅ Kein Flash – Theme wird vor dem Paint gesetzt
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
});
// ✅ Toggle-Button braucht den fertigen DOM
document.addEventListener('astro:page-load', () => {
const toggle = document.querySelector('#theme-toggle');
if (toggle) {
toggle.addEventListener('click', switchTheme);
}
});
Wie nutzt ihr data-astro-rerun für Scripts die immer neu laufen sollen?
Manchmal wollt ihr, dass ein inline Script bei jeder Navigation komplett neu ausgeführt wird. Dafür gibt es das data-astro-rerun Attribut:
<script is:inline data-astro-rerun>
console.log('Dieses Script läuft bei jeder Navigation');
initializePageSpecificFeature();
</script>
Ohne dieses Attribut werden inline Scripts nur einmal ausgeführt und bei späteren Navigationen übersprungen, wenn Astro erkennt, dass das Script bereits geladen wurde.
Ein typischer Anwendungsfall sind page-spezifische Initialisierungen:
---
// ProductPage.astro
const { productId } = Astro.props;
---
<script is:inline data-astro-rerun define:vars={{ productId }}>
trackProductView(productId);
loadProductReviews(productId);
</script>
Beachtet: data-astro-rerun macht das Script automatisch is:inline, ihr könnt also kein TypeScript verwenden. Für komplexere Logik bleibt astro:page-load der bessere Ansatz.
Warum feuert astro:page-load mehrfach bei Browser-Navigation?
Das ist ein bekanntes Verhalten, das Teams oft überrascht. Wenn Nutzer die Browser-Zurück-Taste verwenden, kann astro:page-load erneut feuern, obwohl die Seite aus dem bfcache (Back-Forward Cache) kommt.
Das führt zu Problemen, wenn ihr Event-Listener hinzufügt, ohne die alten zu entfernen:
// ❌ Listener akkumulieren sich
document.addEventListener('astro:page-load', () => {
document.querySelector('#button').addEventListener('click', handleClick);
});
Nach einigen Vor-Zurück-Navigationen hat der Button plötzlich fünf Click-Listener. Die Lösung: Entfernt alte Listener oder verwendet eine Guard-Variable:
// ✅ Mit Cleanup
let currentHandler = null;
document.addEventListener('astro:page-load', () => {
const button = document.querySelector('#button');
if (!button) return;
if (currentHandler) {
button.removeEventListener('click', currentHandler);
}
currentHandler = handleClick;
button.addEventListener('click', currentHandler);
});
Oder eleganter mit AbortController:
let controller = new AbortController();
document.addEventListener('astro:page-load', () => {
controller.abort();
controller = new AbortController();
document.querySelector('#button')?.addEventListener('click', handleClick, {
signal: controller.signal
});
});
Wie debuggt ihr View Transition Probleme effektiv?
Debugging von View Transitions ist nicht trivial, weil mehrere Events in schneller Folge feuern. Unser bewährter Ansatz: Loggt jeden Event mit Timestamp und aktuellem URL.
const events = [
'astro:before-preparation',
'astro:after-preparation',
'astro:before-swap',
'astro:after-swap',
'astro:page-load'
];
events.forEach(event => {
document.addEventListener(event, () => {
console.log(`[${Date.now()}] ${event}`, {
url: location.pathname,
readyState: document.readyState
});
});
});
Für Production-Debugging könnt ihr einen Request-ID-Ansatz verwenden:
document.addEventListener('astro:before-preparation', (e) => {
const requestId = crypto.randomUUID();
console.log(`Navigation started: ${requestId}`, e.to);
sessionStorage.setItem('currentNavigationId', requestId);
});
document.addEventListener('astro:page-load', () => {
const requestId = sessionStorage.getItem('currentNavigationId');
console.log(`Navigation completed: ${requestId}`, location.pathname);
});
Welche Best Practices gelten für Production-Ready View Transitions?
Nach hunderten Projekten haben wir klare Patterns identifiziert, die funktionieren:
Event-Listener zentral verwalten: Statt in jeder Komponente eigene astro:page-load Listener zu haben, erstellt eine zentrale Registry:
// lib/page-init.js
const initializers = [];
export function onPageLoad(fn) {
initializers.push(fn);
}
document.addEventListener('astro:page-load', () => {
initializers.forEach(fn => {
try {
fn();
} catch (e) {
console.error('Initializer failed:', e);
}
});
});
Globale Listener bevorzugen: Statt Listener auf spezifische Elemente zu setzen, nutzt Event Delegation:
// ✅ Überlebt DOM-Swaps automatisch
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-action="toggle-menu"]');
if (button) toggleMenu();
});
State in sessionStorage oder Window: Wenn ihr State zwischen Navigationen teilen müsst, nutzt sessionStorage oder Properties auf dem Window-Objekt – beide überleben View Transitions.
Graceful Degradation einplanen: Testet eure Seite auch ohne View Transitions. Nutzer mit älteren Browsern oder deaktiviertem JavaScript sollten trotzdem eine funktionierende Seite sehen.
Euer nächster Schritt
View Transitions sind gekommen, um zu bleiben. Mit dem astro:page-load Event habt ihr das Werkzeug, um eure Scripts zuverlässig zu initialisieren – egal ob beim ersten Besuch oder nach der zehnten Navigation. Der Schlüssel liegt im Verständnis des Lifecycles und in defensiver Programmierung.
Ihr kämpft mit View Transitions in eurem Projekt oder wollt eure Astro-Anwendung auf das nächste Level heben? Wir bei Never Code Alone sind seit über 15 Jahren auf Softwarequalität, Open Source und remote Consulting spezialisiert. Von Code-Reviews über Architektur-Beratung bis zur Hands-on Implementierung – wir helfen euch, moderne Web-Technologien richtig einzusetzen.
Kontakt: Schreibt einfach eine E-Mail an roland@nevercodealone.de – wir schauen uns eure Situation an und finden gemeinsam die beste Lösung.
Never Code Alone – Gemeinsam für bessere Software-Qualität!
