Bajo una sección del CLAUDE.md de este código base literalmente titulada “Pages ARE the page” hay una regla: un archivo en src/pages/ no puede ser una reexportación de una sola línea de un único componente. Nada de un about.astro que exista solo para importar y renderizar <AboutPage />. El propio archivo de ruta tiene que contener BaseLayout más el markup real de la página, directamente.
Quiero dejar por escrito por qué existe esa regla, porque no fue una preferencia estilística elegida en abstracto: sustituyó a un patrón que solía estar por toda esta aplicación, y la regla se lee de otra manera una vez que has visto a qué estaba reaccionando.
El antipatrón que sustituyó
La forma antigua era así: about.astro importa AboutPage.astro y lo renderiza, nada más. index.astro importa HomePage.astro y lo renderiza. roadmap.astro importa RoadmapPage.astro. Cada ruta de la aplicación era un simple contenedor de paso, y el contenido real (el layout, el markup, la lógica) vivía un archivo más allá, en un componente que existía por una única razón: ser la cosa a la que apuntaba el archivo de ruta.
El coste declarado es la indirección: para entender una sola ruta había que abrir dos archivos, no uno: el archivo de ruta para confirmar que de verdad era solo un paso intermedio, y luego el componente de página para ver qué se renderizaba realmente. Es un impuesto pequeño por ruta, pero es un impuesto que se paga en cada ruta, para siempre, a cambio de un beneficio que la segunda mitad de la regla nombra directamente: reutilización que nunca ocurrió. AboutPage.astro nunca se renderizó desde ningún sitio salvo about.astro. HomePage.astro nunca se reutilizó en ningún sitio salvo index.astro. El envoltorio existía únicamente para que el archivo de página pudiera mantenerse corto, y «mantenerse corto» no es un requisito real para un archivo de ruta: se supone que un archivo de ruta es lo que abres para entender la ruta, y hacerlo artificialmente corto enviando su contenido real a un salto de distancia no hace la ruta más fácil de entender, hace que requiera dos archivos.
Lo que la regla realmente pide
El reemplazo no es «poner todo en el archivo de ruta pase lo que pase». Es más estrecho: src/components/<area>/ sigue siendo el sitio correcto para una pieza genuinamente separada y nombrable (un formulario, una tarjeta, un diagrama), algo que es un concepto en sí mismo sin importar qué página lo use. La línea que traza la regla es reutilización-o-nombre, no ubicación. about.astro en este repositorio, por ejemplo, incorpora PageHeader, AuthorStatement y CtaBand como componentes reales: cada uno es algo nombrable (una cabecera de página, una tarjeta de biografía del autor, una banda de llamada a la acción) que podría aparecer plausiblemente en más de una página, y CtaBand en particular es exactamente el tipo de componente que se reutiliza entre rutas. Lo que about.astro no hace es entregar su propio markup de FAQ a un FaqSection.astro que no existe en ningún otro sitio: ese markup está directamente en el archivo de ruta, en línea, porque no es un concepto separado, es simplemente lo que es esta página.
La prueba, dicho de otro modo, no es «tiene este markup más de unas pocas líneas», sino «tendría sentido esto, y se usaría, en otro sitio». Un formulario tiene sentido en otro sitio. Una tarjeta tiene sentido en otro sitio. La lista de FAQ específica de esta página, estructurada exactamente para el contenido concreto de esta página, no lo tiene.
El mismo instinto, una capa más arriba: rutas planas
Hay una segunda regla, estrechamente relacionada, en el mismo archivo, justo debajo de la primera: preferir src/pages/<name>.astro a src/pages/<name>/index.astro. Una carpeta que no contiene más que un index.astro se describe como el mismo antipatrón, solo que en la capa de enrutamiento en lugar de en la capa de componentes: compra una estructura de subpaginación que no se está usando, a cambio de una ruta que ahora necesita una carpeta para expresar algo que un único archivo podría expresar igual de bien.
La excepción que se reserva es específica: recurrir a una carpeta solo cuando la ruta tiene hijos de verdad (un segmento dinámico como log/[slug].astro, o subpáginas reales que viven debajo de ella). Esa reserva hace trabajo real en este código base, no es solo una cobertura teórica. src/pages/log/ no es un archivo plano precisamente porque no es plana conceptualmente: contiene [slug].astro, una ruta dinámica con tantas páginas reales como entradas existan, que es exactamente el tipo de «hijos reales» para el que está escrita la excepción. Compárese con src/pages/about.astro o src/pages/roadmap.astro, ambos archivos planos que están directamente en src/pages/, porque ninguno tiene hijos: about es una página, no una familia de páginas, y darle una carpeta solo para alojar un index.astro supondría pagar el coste estructural de la carpeta sin obtener nada a cambio.
Es el mismo razonamiento que “pages ARE the page”, aplicado a un eje distinto. La regla de componentes dice: no crees un segundo archivo (un componente de página) a menos que lo que contenga sea genuinamente separado y reutilizable. La regla de enrutamiento dice: no crees un segundo nivel de directorio a menos que lo que haya debajo tenga de verdad más de una página. Ambas reglas son negativas a añadir estructura que solo existe para que el archivo actual sea más corto o la ruta actual sea más profunda, cuando nada en la ruta ni en el contenido necesita todavía realmente esa capa extra.
El trade-off que la regla acepta a propósito
El coste honesto está en el enrutamiento por idioma. La configuración en/es de Astro implica que cada ruta necesita un archivo físico por idioma: src/pages/about.astro y src/pages/es/about.astro tienen que existir ambos, como archivos reales y separados. Leí los dos enteros mientras escribía esta entrada, y son, estructuralmente, casi idénticos: mismos imports, mismas llamadas a getLocale/useTranslations, mismo árbol de componentes, mismo bloque <style> en línea hasta el mismo breakpoint de media query. La única diferencia sustancial entre ellos es a qué idioma resuelve getLocale(Astro) y, como consecuencia, de qué claves del diccionario tira useTranslations(lang) para el copy: el archivo inglés lee t.title y obtiene texto en inglés, el archivo español lee exactamente el mismo accesor t.title y obtiene texto en español, porque es la búsqueda de traducción la que hace el trabajo real de localización, no el markup del archivo de ruta.
Eso es duplicación real. Cada cambio estructural futuro en about.astro (una sección nueva, un ajuste de layout, un componente nuevo que se incorpore) tiene que hacerse dos veces, una en cada archivo, a mano, porque no hay ningún componente compartido de «cuerpo de página» al que apunten ambas rutas. Si lo hubiera, toda esta discusión sería irrelevante: la regla “pages ARE the page” y la regla «cada idioma necesita su propio archivo» juntas implican que las versiones en inglés y en español de una página son archivos físicamente distintos con una estructura físicamente duplicada, y punto.
El CLAUDE.md es explícito en que esto se acepta, no se pasa por alto: «esto es intencional, no es duplicación que haya que ‘arreglar’: cada página se mantiene autocontenida, y los dos archivos solo difieren en qué clave del diccionario leen». La alternativa (un componente compartido AboutPageBody.astro parametrizado por idioma, importado desde dos archivos de ruta que quedarían reducidos a envoltorios) eliminaría la duplicación y reintroduciría exactamente el antipatrón que la primera regla existe para prevenir: dos archivos que abrir para entender una ruta, por una «reutilización» que en realidad es solo reutilización entre una página y su propia traducción, que nunca fue el tipo de reutilización que la regla intentaba habilitar.
Qué descarta en la práctica «genuinamente separado y nombrable»
La cláusula de excepción de la regla (incorporar algo a src/components/<area>/ cuando es «una pieza genuinamente separada y nombrable») es fácil de enunciar y algo más difícil de aplicar con coherencia, porque a casi cualquier cosa se le puede poner un nombre a posteriori. La prueba real a la que sigo volviendo, mirando cómo está construido about.astro, no es «se me ocurre un nombre para esto», sino «existe plausiblemente un segundo llamador, no relacionado, para este componente». CtaBand pasa esa prueba sin ningún problema: es una banda de llamada a la acción que aparece allá donde el sitio quiere cerrar una página con el mismo empujón, y no sabe ni le importa qué página la renderizó. PageHeader también la pasa, igual que PostHeader para las entradas del log: una forma de cabecera reutilizada en cada página de un tipo dado, parametrizada por props, genuinamente agnóstica a qué página concreta la instanció.
Lo que no pasa la prueba es cualquier cosa moldeada por el contenido específico de una página en lugar de por un rol estructural reutilizable. La lista de FAQ de about.astro es markup, no un componente, precisamente porque un FaqSection extraído de ella recibiría como prop un array de preguntas y respuestas y las renderizaría, lo cual suena a componente real justo hasta que te das cuenta de que tendría exactamente un llamador, para siempre, porque existe exactamente una página de FAQ. Tener un componente con un único llamador no está mal, pero no compra nada que un bloque de JSX en línea bien organizado dentro del archivo de ruta no comprara ya, y sí cuesta el mismo impuesto de dos-archivos-para-entender-una-ruta por el que se nombró el antipatrón original. La línea no tiene que ver con la complejidad ni con el número de líneas (un bloque en línea de diez líneas y uno de cincuenta se tratan igual si ninguno tiene un segundo llamador plausible); tiene que ver específicamente con si lo que se está considerando extraer tiene, o cabe esperar razonablemente que tenga, una vida fuera de la única página que lo renderiza ahora mismo.
Por qué esto se lee como un trade real y no como un descuido
Sería fácil mirar about.astro y es/about.astro ahí, siendo casi imágenes especulares el uno del otro, y suponer que nadie se dio cuenta, o que obviamente más adelante llegará un componente base compartido. Habiendo leído realmente ambos archivos, no es eso lo que está pasando: la duplicación es estrecha y mecánica (imports, árbol de componentes, bloque de estilos), y la parte que no está duplicada (el copy real) se ha extraído correctamente, a los diccionarios de i18n, que es el único sitio donde este código base sí impone coherencia entre idiomas a nivel de tipos. La duplicación que queda tiene exactamente la forma que predicen las dos reglas enunciadas: las rutas se mantienen autocontenidas según la primera regla, y rutas autocontenidas multiplicadas por dos idiomas significa dos archivos con el mismo esqueleto, según la segunda.
Si esa es la decisión correcta para una aplicación mayor con decenas de rutas específicas por idioma y estructuralmente variables es una pregunta razonable, y probablemente no tenga una respuesta universal. Para un sitio de este tamaño, donde la estructura de cada página es estable y la única variable real por idioma es el copy, pagar un pequeño coste mecánico de duplicación para evitar reintroducir una indirección de componente-envoltorio que no aportaba ningún beneficio real de reutilización se lee, examinado de cerca, como el lado correcto de ese trade.
El mismo trade vuelve a aparecer, una capa más abajo, en el motivo por el que este sitio genera salida estática en lugar de renderizar rutas bajo demanda: consulta Estático desde el principio, para un sitio que nunca necesita estar al minuto para la otra mitad de esta peculiaridad de los dos idiomas: la generación estática es lo que convierte «cada ruta necesita un archivo por idioma» de una redirección en tiempo de ejecución en un hecho estructural que puedes ver directamente en el árbol de archivos.