Die 404-Seite, die immer wieder verschwand
Eine Catch-All-Route, ein Framework-Bug und Caddy
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Größe
Abstand
Schrift
Was einfach sein sollte
Jede Website hat tote Links. Seiten werden umbenannt, URLs werden geteilt und dann geändert, Suchmaschinen indexieren Dinge, die es längst nicht mehr gibt. Wenn jemand auf eine dieser Sackgassen stößt, entscheidet das, was er als Nächstes sieht, darüber, ob er bleibt oder geht.
Eine eigene 404-Seite zu einer Astro-Site hinzuzufügen klingt nach Routine. Die Doku sagt: lege src/pages/404.astro an, sie wird zu dist/404.html gebaut, dein Hosting-Provider liefert sie aus. Fertig.
Außer, es war nicht fertig. Die Seite wurde ohne Fehler gebaut, keine Warnungen, keine Konfliktmeldungen. Aber die Ausgabe war falsch. Statt der eigenen 404-Seite enthielt dist/404.html eine Weiterleitung auf die Startseite. Die 404-Seite wurde still überschrieben.
Kurze Definitionen
Was ist ein Static-Site-Build?
Bevor ein Besucher überhaupt eintrifft, verarbeitet Astro jede Quelldatei (Templates, Inhalte, Komponenten) und schreibt das Ergebnis als reine HTML-, CSS- und JavaScript-Dateien in einen Ordner namens dist/. Der Server liefert diese Dateien direkt aus: keine Datenbank, keine Verarbeitung pro Anfrage. Der Konflikt in diesem Beitrag entsteht während dieses Build-Schritts, bevor die Seite überhaupt online geht.
Was ist eine Catch-All-Route?
Eine Catch-All-Route trifft auf jeden URL-Pfad zu, für den es keinen spezifischeren Handler gibt. In diesem Setup fängt [...index].astro jeden Pfad an der Wurzel ab: existiert kein Content zu einem bestimmten Pfad, leitet sie auf die Startseite um. Während des Builds sieht /404 für die Catch-All-Route nach einem ganz normalen Pfad aus, was die Quelle des Problems ist.
Was ist Caddy?
Caddy ist ein Webserver: die Software, die die Anfrage eines Browsers entgegennimmt und entscheidet, was zurückgeschickt wird. Astro baut die Seite; Caddy liefert sie aus. Wird eine Datei nicht gefunden, kümmert sich Caddy um diesen Fehler, weshalb der Fix sowohl das Framework als auch den Server betrifft.
Der Aufbau
Diese Seite läuft auf Astro 6.x, als Static Build auf einen Server mit Caddy deployed. Sie ist mehrsprachig: Englisch, Deutsch, Spanisch, Französisch, Chinesisch. Die Landing Page nutzt eine Catch-All-Route mit Rest-Parameter an der Wurzel:
src/pages/[...index].astroDie getStaticPaths-Funktion liefert Sprachcodes als Parameter zurück. undefined für Englisch (daraus wird /), de für Deutsch (/de) und so weiter. Trifft ein Besucher eine Route, die keinem Content-Entry entspricht, leitet die Catch-All-Route auf die Startseite um.
Der Rest-Parameter ist strukturell notwendig. undefined ist in Astro ein Feature, das nur bei Rest-Parametern funktioniert, und der einzige Weg, Englisch unter / ohne Sprachpräfix auszuliefern. Ein benannter [index]-Parameter kann das nicht. Die Catch-All-Route ist also keine Design-Entscheidung, die man einfach tauschen kann. Sie ist tragend.
Für die Seite hat das funktioniert. Was es kaputt gemacht hat, war eine eigene 404-Seite.
Das Problem
Eine minimale src/pages/404.astro kommt rein: Logo, kurzer Text, zwei Karten, die auf die Hauptbereiche zeigen. Site bauen. dist/404.html prüfen.
Die Datei war da, aber statt einer hilfreichen Seite mit Navigation bekamen Besucher eine stille Weiterleitung auf die Startseite zu sehen. Keine Erklärung, kein Hinweis darauf, dass etwas schiefgegangen war. Der Inhalt war dieser:
<!doctype html><title>Redirecting to: /</title><meta http-equiv=refresh content="2;url=/"><a href=/>Redirecting from /404/ to /</a>Das ist nicht meine 404-Seite. Das ist die Weiterleitung der Catch-All-Route. Astros [...index].astro hat /404 während des Builds abgefangen, keinen passenden Content-Entry gefunden, den Fallback Astro.redirect("/") ausgelöst und diese Weiterleitung nach dist/404.html geschrieben. Was auch immer 404.astro produziert hätte, wurde überschrieben.
Keine Fehler, keine Warnungen, kein Hinweis darauf, dass etwas schiefgegangen war. Der Build meldete 404.html (+24ms), als wäre alles in Ordnung.
Was die Dokumentation sagt
Die hier relevanten Routing-Prioritätsregeln:
- Reservierte Routen (
_astro/,_server_islands/,_actions/) - Spezifischere Pfadsegmente schlagen unspezifischere
- Statische Routen haben Vorrang vor dynamischen Routen
- Benannte Parameter schlagen Rest-Parameter
Nach diesen Regeln sollte 404.astro (eine statische Route) gegen [...index].astro (eine Rest-Parameter-Route) gewinnen. Tut sie aber nicht. Die Datei wird übereinander generiert. Die Catch-All-Route schreibt zuletzt und überschreibt sie.
Was nicht funktioniert hat
Die offensichtlichen Lösungen und ein paar weniger offensichtliche.
Eine 404-Response aus der Catch-All-Route zurückgeben:
if (index === "404") { return new Response(null, { status: 404 });}Ergebnis: Astro hat das Erzeugen von dist/404.html komplett übersprungen. Die Datei existierte gar nicht.
Astro.rewrite("/404") verwenden:
if (!entry?.data?.file_name) { return Astro.rewrite("/404");}Ergebnis: Loop erkannt. Das Rewrite ging wieder durch [...index].astro.
prerenderConflictBehavior: 'error' in der Astro-Config setzen:
Ergebnis: kein Fehler gemeldet. Astro betrachtet das nicht als Konflikt.
404.astro in ein Unterverzeichnis verschieben (src/pages/_error/404.astro):
Ergebnis: Astro ignoriert Verzeichnisse, die mit einem Unterstrich beginnen.
[...index].astro in [index].astro umbenennen:
Das hätte das Catch-All-Verhalten entfernt. Aber [...index].astro gibt { params: { index: undefined } } für Englisch zurück, um die Wurzel / zu erzeugen. Die Astro-Doku sagt, dass undefined-Parameter ein Feature sind, das nur bei Rest-Parametern funktioniert. Ein Wechsel zu einem benannten [index]-Parameter würde die Startseite brechen.
Die GitHub-Issues
Das ist kein neues Problem. Es wurde von Astro-Maintainern gemeldet und bestätigt:
Issue #9103: „static route in subfolder gets overridden by Rest-parameter.“ Ein Maintainer hat bestätigt: „the priority is correct, it even works correctly on the dev server, but the file gets generated one on top of the other.“ Das Issue wurde geschlossen, das zugrundeliegende Problem bleibt.
Issue #9832: „Prerender page conflicts are silently ignored.“ Erstellt vom selben Maintainer, der #9103 analysiert hat. Das Issue wurde zugewiesen und später geschlossen; das zugrundeliegende Verhalten reproduziert sich in diesem Setup weiterhin.
Issue #12175: „Custom 404 pages in localized sites.“ Noch offen. Ein Astro-Maintainer hat es einmal mit der Begründung „this is fixed“ geschlossen und konnte sich dann nicht mehr erinnern, warum, als es wieder geöffnet wurde.
Die Ursache liegt darin, wie Astros Static Build Dateien erzeugt: Routen werden sequenziell gebaut, und wenn die Catch-All-Route einen Pfad generiert, der zu einer bereits geschriebenen Datei passt, überschreibt sie diese. Die Routing-Priorität stimmt auf Auflösungsebene, wird aber nicht auf Schreibebene durchgesetzt.
Zwei Lösungen
Wenn man den Bug einmal verstanden hat, gibt es zwei saubere Workarounds:
Option A: Post-Build-Copy. 404.astro bleibt, bekommt aber einen anderen Namen (etwa not-found.astro). Sie wird konfliktfrei zu dist/not-found/index.html gebaut. Ein Skript nach dem Build kopiert sie dann zu dist/404.html. Man bekommt die volle Astro-Pipeline: Fonts, Theme-System, geteilte Komponenten.
Option B: Standalone HTML. Eine eigenständige 404.html kommt ins Public/-Verzeichnis. Astro kopiert sie als statisches Asset nach dist/404.html, vollständig am Routing-System vorbei. Kein Konflikt möglich.
Warum Standalone-HTML gewonnen hat
Option B hat keine beweglichen Teile. Kein Skript nach dem Build, das gepflegt werden muss, kein Umbenennungs-Trick, der dokumentiert werden will, keine Kopplung an einen Bug, der vielleicht irgendwann gefixt wird (und dann das Aufräumen des Workarounds nach sich zieht).
Der Preis: keine Astro-Font-Pipeline, kein Hell/Dunkel-Theme-Umschalter, keine geteilten Komponenten. Die Seite nutzt System-Font-Fallbacks und fest verdrahtete Dark-Theme-CSS-Variablen. Für eine Seite mit zwei Zeilen Text und zwei Navigations-Karten ist das ein vertretbarer Preis.
Wenn Astro den zugrundeliegenden Bug fixt, kann die Route inshallah zurück in die Astro-Pipeline wandern. Die Migration ist kein Einschritt: Font-Setup und Theme-Umschalter müssten auf der Route neu aufgebaut werden. Einmal lohnt es sich, vorher nicht. Bis dahin ist die einfachere Lösung die bessere.
Die Caddy-Seite
Die 404-Seite muss vom Webserver auch tatsächlich ausgeliefert werden. In Caddy ist das eine einzige Direktive:
handle_errors { rewrite * /404.html file_server}Wenn file_server ein 404 (Datei nicht gefunden) zurückgibt, fängt handle_errors das ab, schreibt die Anfrage auf /404.html um und liefert die eigene Seite aus. Kein Backend nötig, keine weitere Konfiguration.
Caddys Default-Konfiguration liefert keine Cache-Control-Header für Favicons. Chromium-Browser zeigen deshalb nach einer Server-Migration veraltete oder fehlende Favicons an. Eine einzeilige Header-Direktive behebt das:
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"Kleinigkeiten, aber dasselbe Muster wie beim 404-Bug: etwas, das in Ordnung aussieht, bis man genauer hinschaut.
Worauf es hinausläuft
Frameworks versagen lautlos. Routing-Priorität kann auf Auflösungsebene stimmen und auf Schreibebene trotzdem falsch sein. Ein Build, der keine Fehler meldet, kann trotzdem die falsche Ausgabe ausliefern. Der einfachste saubere Workaround ist selten der cleverste.
Die 404-Seite funktioniert. Besucher, die auf einen toten Link stoßen, sehen eine klare Seite mit einem Weg weiter. Der Code ist eine HTML-Datei ohne Abhängigkeiten.
Das wird inshallah halten, bis der Bug gefixt ist.
Betrifft das jede Astro-Seite oder nur bestimmte Setups?
Nur Seiten mit einer Rest-Parameter-Route an der Wurzel sind betroffen. Die Überschreibung passiert, weil `[...index].astro` während des Builds eine Datei für `/404` erzeugt. Eine Standard-Astro-Seite mit festen Routen hat diese Kollision nicht.
Wie erkennt man, ob die eigene 404-Seite still überschrieben wurde?
Nach dem Build die Datei `dist/404.html` öffnen und durchlesen. Steht dort eine `<meta http-equiv=refresh>`-Weiterleitung statt des eigenen Seiteninhalts, hat die Catch-All-Route sie überschrieben. Der Build meldet in beiden Fällen keinen Fehler.
Setzt diese Lösung Caddy voraus, oder funktioniert sie auch mit anderen Webservern?
Der `Public/`-Ansatz funktioniert mit jedem Webserver; die `handle_errors`-Direktive ist Caddy-spezifisch. Eine `404.html` in `Public/` umgeht Astros Routing komplett und erzeugt eine Datei in `dist/`, die jeder Server ausliefern kann. Was sich zwischen Servern unterscheidet, ist die Konfiguration für die Auslieferung bei Fehlern: Caddy verwendet `handle_errors`, Nginx `error_page`, Apache `ErrorDocument`. Der Astro-seitige Fix ist überall derselbe.
Wird Astro das beheben?
Das Problem ist gemeldet, anerkannt und teilweise adressiert worden, aber zum Zeitpunkt dieses Beitrags reproduziert es sich weiterhin in diesem Setup. Drei GitHub-Issues decken es ab, oben verlinkt. Zwei davon haben die Maintainer geschlossen; das zugrundeliegende Verhalten bleibt. Issue #12175 ist weiterhin offen. Wird der Bug vollständig behoben, kann die 404-Seite inshallah zurück in die Astro-Pipeline wandern, allerdings nicht ohne den Wiederaufbau der Font- und Theme-Integration.
Web 1 von 3
Zurück zu WebLuft- und Raumfahrtingenieur
Ethischer Unternehmer in der Öffentlichkeit
Sie kümmern sich um Ihr Geschäft
Ich kümmere mich um die digitale Seite
Zusammenarbeiten
- KI mit Ehrlichkeit
- Private Infrastruktur
- Websites die performen
Erzählen Sie mir von Ihrer Situation:
javed@javedab.com Mehr über mich