La página 404 que no dejaba de desaparecer
Una ruta catch-all, un bug del framework y Caddy
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Tamaño
Espaciado
Fuente
Lo que debería haber sido simple
Toda web tiene enlaces rotos. Las páginas se renombran, las URLs se comparten y luego cambian, los buscadores indexan cosas que ya no existen. Cuando alguien llega a uno de esos callejones sin salida, lo que ve a continuación decide si se queda o se va.
Añadir una página 404 personalizada a un sitio en Astro suena rutinario. La documentación dice: crea src/pages/404.astro, se compila a dist/404.html, tu proveedor de hosting la sirve. Listo.
Solo que no estaba listo. La página se compilaba sin errores, sin advertencias, sin mensajes de conflicto. Pero la salida era incorrecta. En lugar de la página 404 personalizada, dist/404.html contenía una redirección a la página principal. La página 404 se sobrescribía en silencio.
Definiciones rápidas
¿Qué es un build estático?
Antes de que llegue ningún visitante, Astro procesa cada archivo fuente (plantillas, contenido, componentes) y escribe el resultado como archivos HTML, CSS y JavaScript planos en una carpeta llamada dist/. El servidor entrega esos archivos directamente: sin base de datos, sin procesamiento por petición. El conflicto que aparece en este artículo ocurre durante ese paso de build, antes incluso de que el sitio llegue a estar en producción.
¿Qué es una ruta catch-all?
Una ruta catch-all coincide con cualquier ruta de URL que no tenga un handler más específico. En este setup, [...index].astro captura todas las rutas a nivel raíz: si no existe contenido para una ruta dada, redirige a la página principal. Durante el build, /404 parece simplemente otra ruta más para que la maneje la catch-all, y ahí está el origen del problema.
¿Qué es Caddy?
Caddy es un servidor web: el software que recibe la petición del navegador y decide qué enviar de vuelta. Astro construye el sitio; Caddy lo sirve. Cuando un archivo no se encuentra, Caddy se encarga de ese error, y por eso la solución toca tanto el framework como el servidor.
El planteamiento
Este sitio corre sobre Astro 6.x, desplegado como build estático en un servidor con Caddy. Es multilingüe: inglés, alemán, español, francés, chino. La landing page usa una ruta catch-all con parámetro rest en la raíz:
src/pages/[...index].astroLa función getStaticPaths devuelve los códigos de idioma como parámetros. undefined para inglés (que genera /), de para alemán (/de), y así sucesivamente. Si un visitante llega a una ruta que no coincide con ningún content entry, la ruta catch-all redirige a la página principal.
El parámetro rest es estructuralmente necesario. undefined es una característica de Astro que solo funciona con parámetros rest, y es la única forma de servir el inglés en / sin un prefijo de idioma. Un parámetro [index] con nombre no puede hacerlo. Por eso la ruta catch-all no es una decisión de diseño que se pueda intercambiar. Es estructural.
Para el sitio funcionaba bien. Lo que lo rompió fue una página 404 personalizada.
El problema
Entra una src/pages/404.astro mínima: logo del sitio, mensaje breve, dos tarjetas que apuntan a las secciones principales. Compilar el sitio. Revisar dist/404.html.
El archivo existía, pero en lugar de una página útil con navegación, los visitantes veían una redirección silenciosa a la principal. Sin explicación, sin indicio de que algo hubiera fallado. El contenido era este:
<!doctype html><title>Redirecting to: /</title><meta http-equiv=refresh content="2;url=/"><a href=/>Redirecting from /404/ to /</a>Esa no es mi página 404. Es la redirección de la ruta catch-all. El [...index].astro de Astro interceptó /404 durante el build, no encontró ningún content entry que coincidiera, cayó al fallback Astro.redirect("/") y escribió esa redirección en dist/404.html, sobrescribiendo lo que 404.astro habría producido.
Sin errores, sin advertencias, sin indicio de que algo hubiera fallado. El build informaba 404.html (+24ms) como si todo estuviera bien.
Lo que dice la documentación
Las reglas de prioridad de enrutamiento relevantes aquí:
- Rutas reservadas (
_astro/,_server_islands/,_actions/) - Los segmentos de ruta más específicos ganan a los menos específicos
- Las rutas estáticas tienen prioridad sobre las dinámicas
- Los parámetros con nombre ganan a los parámetros rest
Según estas reglas, 404.astro (una ruta estática) debería ganar frente a [...index].astro (una ruta con parámetro rest). No lo hace. El archivo se genera uno encima del otro. La ruta catch-all escribe al final y lo sobrescribe.
Lo que no funcionó
Los arreglos obvios, y algunos menos obvios.
Devolver una respuesta 404 desde la ruta catch-all:
if (index === "404") { return new Response(null, { status: 404 });}Resultado: Astro omitió la creación de dist/404.html por completo. El archivo no existía.
Usar Astro.rewrite("/404"):
if (!entry?.data?.file_name) { return Astro.rewrite("/404");}Resultado: bucle detectado. El rewrite volvía a pasar por [...index].astro.
Establecer prerenderConflictBehavior: 'error' en la configuración de Astro:
Resultado: ningún error reportado. Astro no lo considera un conflicto.
Mover 404.astro a un subdirectorio (src/pages/_error/404.astro):
Resultado: Astro ignora los directorios que empiezan por guion bajo.
Renombrar [...index].astro a [index].astro:
Eso habría eliminado el comportamiento catch-all. Pero [...index].astro devuelve { params: { index: undefined } } para el inglés, de forma que se genere la raíz /. La documentación de Astro dice que los parámetros undefined son una característica exclusiva de los parámetros rest. Cambiar a un parámetro [index] con nombre rompería la página principal.
Los issues de GitHub
No es un problema nuevo. Los mantenedores de Astro lo han reportado y reconocido:
Issue #9103: «static route in subfolder gets overridden by Rest-parameter». Un mantenedor lo confirmó: «the priority is correct, it even works correctly on the dev server, but the file gets generated one on top of the other». El issue se cerró, y el problema de fondo sigue ahí.
Issue #9832: «Prerender page conflicts are silently ignored». Lo abrió el mismo mantenedor que diagnosticó #9103. El issue se asignó y más tarde se cerró; el comportamiento de fondo sigue reproduciéndose en este setup.
Issue #12175: «Custom 404 pages in localized sites». Sigue abierto. Un mantenedor de Astro lo cerró una vez diciendo «this is fixed», y luego no recordaba por qué cuando se reabrió.
La causa de fondo está en cómo el build estático de Astro genera archivos: las rutas se construyen de forma secuencial, y cuando la catch-all genera una ruta que coincide con un archivo ya escrito, lo sobrescribe. La prioridad de enrutamiento es correcta a nivel de resolución, pero no se aplica a nivel de escritura de archivo.
Dos soluciones
Una vez que se entiende el bug, hay dos soluciones limpias:
Opción A: copia tras el build. Mantener 404.astro pero con otro nombre (como not-found.astro). Se compila a dist/not-found/index.html sin conflicto. Después un script posterior al build lo copia a dist/404.html. Se obtiene toda la pipeline de Astro: fuentes, sistema de temas, componentes compartidos.
Opción B: HTML independiente. Poner un 404.html autosuficiente en el directorio Public/. Astro lo copia a dist/404.html como asset estático, saltándose por completo el sistema de enrutamiento. Ningún conflicto posible.
Por qué ganó el HTML independiente
La Opción B no tiene piezas móviles. Ningún script posterior al build que mantener, ningún truco de renombrado que documentar, ningún acoplamiento a un bug que quizá se arregle algún día (y eso exigiría después limpiar el workaround).
El precio: sin pipeline de fuentes de Astro, sin interruptor de tema claro/oscuro, sin componentes compartidos. La página usa fuentes de sistema como fallback y variables CSS del tema oscuro escritas a mano. Para una página con dos líneas de texto y dos tarjetas de navegación, es un precio que merece la pena pagar.
Si Astro arregla el bug de fondo, la ruta puede inshallah volver a la pipeline de Astro. La migración no es un solo paso: el setup de fuentes y el interruptor de tema tendrían que reconstruirse encima de la ruta. Merece la pena hacerlo una vez, no antes. Hasta entonces, la solución más simple es la mejor.
El lado de Caddy
La página 404 necesita que el servidor web la sirva de verdad. En Caddy, eso es una única directiva:
handle_errors { rewrite * /404.html file_server}Cuando file_server devuelve un 404 (archivo no encontrado), handle_errors lo captura, reescribe la petición a /404.html y sirve la página personalizada. Sin backend, sin configuración adicional.
La configuración por defecto de Caddy no envía cabeceras Cache-Control para los favicons. Por eso los navegadores Chromium muestran favicons desactualizados o ausentes tras una migración de servidor. Una directiva de cabecera de una línea lo arregla:
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"Detalles pequeños, pero el mismo patrón que el bug del 404: algo que parece correcto hasta que se comprueba.
A lo que se reduce
Los frameworks fallan en silencio. La prioridad de enrutamiento puede ser correcta a nivel de resolución y seguir siendo incorrecta a nivel de escritura de archivo. Un build que no reporta ningún error puede aun así publicar la salida equivocada. El workaround limpio más simple rara vez es el más ingenioso.
La página 404 funciona. Los visitantes que llegan a un enlace muerto ven una página clara con un camino a seguir. El código es un solo archivo HTML sin dependencias.
Inshallah aguantará hasta que se arregle el bug.
¿Esto afecta a cualquier sitio en Astro o solo a configuraciones concretas?
Solo los sitios que usan una ruta con parámetro rest a nivel raíz se topan con esto. La sobrescritura ocurre porque `[...index].astro` genera un archivo en `/404` durante el build. Un sitio Astro estándar con rutas fijas no tiene esa colisión.
¿Cómo saber si la página 404 está siendo sobrescrita en silencio?
Tras el build, abrir `dist/404.html` y leerlo. Si contiene una redirección `<meta http-equiv=refresh>` en lugar del contenido propio de la página, la catch-all la ha sobrescrito. El build no informará de ningún error en ninguno de los dos casos.
¿Esta solución exige Caddy o funciona también con otros servidores web?
El enfoque de `Public/` funciona con cualquier servidor web; la directiva `handle_errors` es específica de Caddy. Poner `404.html` en `Public/` salta por completo el sistema de enrutamiento de Astro y produce un archivo en `dist/` que cualquier servidor puede servir. Lo que cambia entre servidores es cómo se configuran para servir ese archivo en caso de error: Caddy usa `handle_errors`, Nginx usa `error_page`, Apache usa `ErrorDocument`. La solución del lado de Astro es universal.
¿Lo arreglará Astro?
El problema ha sido reportado, reconocido y parcialmente abordado, pero al momento de escribir esto sigue reproduciéndose en este setup. Hay tres issues de GitHub que lo cubren, enlazados arriba. Los mantenedores han cerrado dos; el comportamiento de fondo persiste. El issue #12175 sigue abierto. Si se resuelve por completo, la página 404 puede inshallah volver a la pipeline de Astro, aunque no sin reconstruir la integración de fuentes y temas.
Web 1 de 3
Volver a WebIngeniero aeroespacial
Emprendedor ético en público
Tú te ocupas de tu negocio
Yo me encargo del lado digital
Trabaja conmigo
- IA con honestidad
- Infraestructura privada
- Sitios web que rinden
Cuéntame sobre tu situación:
javed@javedab.com Más sobre mí