La page 404 qui n'arrêtait pas de disparaître
Une route catch-all, un bug de framework et Caddy
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Taille
Espacement
Police
Ce qui aurait dû être simple
Tout site a des liens morts. Les pages sont renommées, les URL sont partagées puis modifiées, les moteurs de recherche indexent des choses qui n’existent plus. Quand quelqu’un tombe sur l’une de ces impasses, ce qu’il voit ensuite décide s’il reste ou part.
Ajouter une page 404 personnalisée à un site Astro ressemble à une routine. La doc dit : crée src/pages/404.astro, elle est construite en dist/404.html, ton hébergeur la sert. Fini.
Sauf que ce n’était pas fini. La page était construite sans erreurs, sans avertissements, sans messages de conflit. Mais la sortie était fausse. Au lieu de la page 404 personnalisée, dist/404.html contenait une redirection vers l’accueil. La page 404 était écrasée, en silence.
Définitions rapides
Qu'est-ce qu'un build statique ?
Avant qu’un seul visiteur n’arrive, Astro traite chaque fichier source (templates, contenus, composants) et écrit le résultat sous forme de fichiers HTML, CSS et JavaScript bruts dans un dossier nommé dist/. Le serveur délivre ces fichiers directement : pas de base de données, pas de traitement par requête. Le conflit décrit dans cet article apparaît pendant cette étape de build, avant même que le site ne soit en ligne.
Qu'est-ce qu'une route catch-all ?
Une route catch-all correspond à n’importe quel chemin d’URL qui n’a pas de handler plus spécifique. Dans ce setup, [...index].astro attrape tous les chemins à la racine : si aucun contenu n’existe pour un chemin donné, elle redirige vers la page d’accueil. Pendant le build, /404 ressemble simplement à un chemin de plus que la catch-all doit gérer, et c’est là que se trouve la source du problème.
Qu'est-ce que Caddy ?
Caddy est un serveur web : le logiciel qui reçoit la requête d’un navigateur et décide quoi renvoyer. Astro construit le site ; Caddy le sert. Lorsqu’un fichier n’est pas trouvé, Caddy gère cette erreur, ce qui explique pourquoi la solution touche à la fois le framework et le serveur.
Le contexte
Ce site tourne sur Astro 6.x, déployé en build statique sur un serveur qui fait tourner Caddy. Il est multilingue : anglais, allemand, espagnol, français, chinois. La page d’accueil utilise une route catch-all avec paramètre rest à la racine :
src/pages/[...index].astroLa fonction getStaticPaths renvoie les codes de langue comme paramètres. undefined pour l’anglais (qui génère /), de pour l’allemand (/de), et ainsi de suite. Si un visiteur atteint un chemin qui ne correspond à aucun content entry, la route catch-all redirige vers l’accueil.
Le paramètre rest est structurellement nécessaire. undefined est dans Astro une fonctionnalité réservée aux paramètres rest, et c’est le seul moyen de servir l’anglais à / sans préfixe de langue. Un paramètre [index] nommé ne peut pas faire ça. La route catch-all n’est donc pas un choix de design qu’on peut remplacer. Elle est porteuse.
Pour le site, ça fonctionnait bien. Ce qui l’a cassé, c’est une page 404 personnalisée.
Le problème
Une src/pages/404.astro minimale arrive : logo du site, message court, deux cartes pointant vers les sections principales. Construire le site. Vérifier dist/404.html.
Le fichier existait, mais au lieu d’une page utile avec une navigation, les visiteurs voyaient une redirection silencieuse vers l’accueil. Aucune explication, aucun signe qu’il y avait eu un problème. Le contenu était celui-ci :
<!doctype html><title>Redirecting to: /</title><meta http-equiv=refresh content="2;url=/"><a href=/>Redirecting from /404/ to /</a>Ce n’est pas ma page 404. C’est la redirection de la route catch-all. Le [...index].astro d’Astro a intercepté /404 pendant le build, n’a trouvé aucun content entry correspondant, est tombé sur le fallback Astro.redirect("/"), et a écrit cette redirection dans dist/404.html, écrasant ce que 404.astro aurait produit.
Aucune erreur, aucun avertissement, aucun signe qu’il y avait eu un problème. Le build rapportait 404.html (+24ms) comme si tout allait bien.
Ce que dit la documentation
Les règles de priorité de routage pertinentes ici :
- Routes réservées (
_astro/,_server_islands/,_actions/) - Les segments de chemin plus spécifiques l’emportent sur les moins spécifiques
- Les routes statiques ont priorité sur les routes dynamiques
- Les paramètres nommés l’emportent sur les paramètres rest
D’après ces règles, 404.astro (une route statique) devrait l’emporter sur [...index].astro (une route avec paramètre rest). Ce n’est pas le cas. Le fichier est généré par-dessus. La route catch-all écrit en dernier et l’écrase.
Ce qui n’a pas fonctionné
Les corrections évidentes, et quelques-unes moins évidentes.
Renvoyer une Response 404 depuis la route catch-all :
if (index === "404") { return new Response(null, { status: 404 });}Résultat : Astro a complètement sauté la création de dist/404.html. Le fichier n’existait pas du tout.
Utiliser Astro.rewrite("/404") :
if (!entry?.data?.file_name) { return Astro.rewrite("/404");}Résultat : boucle détectée. Le rewrite est repassé par [...index].astro.
Définir prerenderConflictBehavior: 'error' dans la config Astro :
Résultat : aucune erreur signalée. Astro ne considère pas cela comme un conflit.
Déplacer 404.astro dans un sous-dossier (src/pages/_error/404.astro) :
Résultat : Astro ignore les dossiers qui commencent par un tiret bas.
Renommer [...index].astro en [index].astro :
Cela aurait supprimé le comportement catch-all. Mais [...index].astro renvoie { params: { index: undefined } } pour l’anglais afin de générer la racine /. La doc d’Astro indique que les paramètres undefined sont une fonctionnalité réservée aux paramètres rest. Passer à un paramètre [index] nommé casserait la page d’accueil.
Les issues GitHub
Ce n’est pas un problème nouveau. Il a été signalé et reconnu par des mainteneurs d’Astro :
Issue #9103 : «static route in subfolder gets overridden by Rest-parameter». Un mainteneur l’a confirmé : «the priority is correct, it even works correctly on the dev server, but the file gets generated one on top of the other». L’issue a été fermée, et le problème de fond persiste.
Issue #9832 : «Prerender page conflicts are silently ignored». Ouverte par le même mainteneur qui avait diagnostiqué #9103. L’issue a été assignée puis fermée ; le comportement de fond se reproduit toujours dans ce setup.
Issue #12175 : «Custom 404 pages in localized sites». Toujours ouverte. Un mainteneur d’Astro l’a fermée une fois en disant «this is fixed», puis ne se souvenait plus pourquoi quand elle a été rouverte.
La cause profonde tient à la façon dont le build statique d’Astro génère les fichiers : les routes sont construites de manière séquentielle, et quand la route catch-all génère un chemin qui correspond à un fichier déjà écrit, elle l’écrase. La priorité de routage est correcte au niveau de la résolution, mais elle n’est pas appliquée au niveau de l’écriture du fichier.
Deux solutions
Une fois le bug compris, il y a deux contournements propres :
Option A : copie post-build. Garder 404.astro mais le renommer (par exemple not-found.astro). Il est construit en dist/not-found/index.html sans conflit. Ensuite un script post-build le copie vers dist/404.html. On récupère toute la pipeline Astro : polices, système de thème, composants partagés.
Option B : HTML autonome. Mettre un 404.html autonome dans le dossier Public/. Astro le copie dans dist/404.html comme asset statique, en contournant complètement le système de routage. Aucun conflit possible.
Pourquoi le HTML autonome l’a emporté
L’option B n’a aucune pièce mobile. Aucun script post-build à maintenir, aucune astuce de renommage à documenter, aucun couplage à un bug qui pourrait être corrigé un jour (ce qui obligerait ensuite à nettoyer le contournement).
Le prix : pas de pipeline de polices Astro, pas d’interrupteur thème clair/sombre, pas de composants partagés. La page utilise des polices système en fallback et des variables CSS du thème sombre écrites en dur. Pour une page avec deux lignes de texte et deux cartes de navigation, c’est un prix qui vaut la peine.
Si Astro corrige le bug de fond, la route peut inshallah revenir dans la pipeline d’Astro. La migration n’est pas une seule étape : il faudrait reconstruire le setup des polices et l’interrupteur de thème par-dessus la route. Ça vaut la peine une fois, pas avant. D’ici là, la solution la plus simple reste la meilleure.
Du côté de Caddy
La page 404 a besoin que le serveur web la serve vraiment. Dans Caddy, c’est une seule directive :
handle_errors { rewrite * /404.html file_server}Quand file_server renvoie un 404 (fichier non trouvé), handle_errors l’attrape, réécrit la requête vers /404.html et sert la page personnalisée. Aucun backend nécessaire, aucune configuration supplémentaire.
La config par défaut de Caddy ne renvoie aucun en-tête Cache-Control pour les favicons. Les navigateurs Chromium affichent donc des favicons périmés ou manquants après une migration de serveur. Une directive d’en-tête d’une ligne corrige ça :
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"Des petits détails, mais le même motif que le bug 404 : quelque chose qui a l’air correct jusqu’à ce qu’on regarde.
Ce à quoi ça se ramène
Les frameworks échouent en silence. La priorité de routage peut être correcte au niveau de la résolution et tout de même fausse au niveau de l’écriture du fichier. Un build qui ne signale aucune erreur peut quand même livrer la mauvaise sortie. Le contournement propre le plus simple est rarement le plus astucieux.
La page 404 fonctionne. Les visiteurs qui tombent sur un lien mort voient une page propre avec un chemin pour continuer. Le code est un seul fichier HTML sans dépendances.
Inshallah ça tiendra jusqu’à ce que le bug soit corrigé.
Est-ce que cela touche tous les sites Astro, ou seulement certaines configurations ?
Seuls les sites qui utilisent une route avec paramètre rest à la racine sont concernés. L'écrasement se produit parce que `[...index].astro` génère un fichier à `/404` pendant le build. Un site Astro standard avec des routes fixes n'a pas cette collision.
Comment savoir si la page 404 est silencieusement écrasée ?
Après le build, ouvrir `dist/404.html` et le lire. S'il contient une redirection `<meta http-equiv=refresh>` au lieu du contenu de la page, la catch-all l'a écrasé. Le build ne signalera aucune erreur dans un cas comme dans l'autre.
Cette solution exige-t-elle Caddy, ou fonctionne-t-elle aussi avec d'autres serveurs web ?
L'approche `Public/` fonctionne avec n'importe quel serveur web ; la directive `handle_errors` est spécifique à Caddy. Placer `404.html` dans `Public/` contourne entièrement le routage d'Astro et produit un fichier dans `dist/` que n'importe quel serveur peut servir. Ce qui change entre serveurs, c'est la manière dont on les configure pour servir ce fichier en cas d'erreur : Caddy utilise `handle_errors`, Nginx utilise `error_page`, Apache utilise `ErrorDocument`. La correction côté Astro est universelle.
Astro va-t-il corriger ça ?
Le problème a été signalé, reconnu et partiellement traité, mais au moment d'écrire ces lignes il se reproduit toujours dans ce setup. Trois issues GitHub le couvrent, liées plus haut. Les mainteneurs en ont fermé deux ; le comportement de fond persiste. L'issue #12175 reste ouverte. Si le bug est entièrement résolu, la page 404 peut inshallah revenir dans la pipeline d'Astro, sans pouvoir éviter la reconstruction de l'intégration des polices et du thème.
Web 1 sur 3
Retour à WebIngénieur aérospatial
Entrepreneur éthique en public
Vous gérez votre activité
Je m'occupe du côté numérique
Travailler avec moi
- IA avec honnêteté
- Infrastructure privée
- Sites web performants
Parlez-moi de votre situation :
javed@javedab.com En savoir plus