Cada ruta de este sitio (inglés, español, las entradas del log, todo) sale de astro build como una carpeta de archivos HTML junto a su CSS y su JS. Ningún proceso servidor lee una solicitud, decide qué renderizar y escribe una respuesta. El astro.config.mjs de este proyecto no fija ningún modo output, y eso es deliberado: estático (output: "static", el valor por defecto) es lo que ocurre cuando no activas nada más. No he llegado aquí por dejar una opción sin tocar; he mirado lo que el sitio necesita hacer realmente y estático era la respuesta que encajaba.
La propia configuración es lo bastante corta como para leerla entera:
export default defineConfig({
site: "https://albgott.com",
prefetch: true,
i18n: {
defaultLocale: "en",
locales: ["en", "es"],
routing: {
prefixDefaultLocale: false,
redirectToDefaultLocale: false,
},
},
integrations: [
sitemap({
i18n: {
defaultLocale: "en",
locales: { en: "en-US", es: "es-ES" },
},
}),
mdx(),
],
});
Dos integraciones (sitemap y mdx) y un bloque de i18n que dice que el inglés va sin prefijo (/about) y el español vive bajo su propio prefijo (/es/about), sin redirección automática según el idioma del navegador. Nada de esto habla con un runtime de servidor, un adaptador de base de datos o una función edge. Esa ausencia es la arquitectura.
Lo que lo estático elimina realmente
Es fácil describir la salida estática como “sin servidor”, pero el marco útil es más estrecho: no hay servidor en el momento de la solicitud. La distinción importa porque buena parte del trabajo real (renderizar MDX a HTML, resolver los diccionarios de i18n, construir el sitemap) sigue ocurriendo. Simplemente ocurre una vez, en tiempo de compilación, en lugar de una vez por visitante.
Ese único cambio elimina toda una categoría de superficie operativa:
- Ningún runtime que mantener vivo. Un proceso Node sirviendo páginas SSR puede caerse, filtrar memoria o necesitar un reinicio tras actualizar una dependencia. Un directorio de archivos HTML no puede caerse. No hay nada en ejecución que pueda hacerlo.
- Ninguna superficie de ataque en tiempo de solicitud para la propia aplicación. Cada ruta de este sitio es algo que una CDN o un hosting estático puede servir directamente desde disco. No hay código de aplicación analizando la solicitud de un cliente, ni estado de sesión en el servidor, ni superficie de inyección en una plantilla que renderiza datos por usuario, porque no hay nada por usuario. Toda la categoría de incidentes de tipo “el manejador SSR de la ruta tenía un bug” no aplica.
- Caché trivial. La clave de caché de un archivo estático es simplemente su ruta. Sin malabares con la cabecera
Vary, sin invalidación de caché ligada a sesión o estado de autenticación, sin preocuparse de si un edge de la CDN cacheó una respuesta personalizada para el usuario equivocado. Cada visitante de/log/static-first-for-a-site-that-never-needs-to-be-freshrecibe exactamente los mismos bytes, así que la CDN puede conservarlos indefinidamente hasta el siguiente deploy. - Sin la rutina de parcheado de un runtime. No hay una versión de Node que mantener al día en un servidor, ni un aviso de seguridad SSR a nivel de framework que seguir, porque lo que sirve las solicitudes en producción es un servidor de archivos HTTP plano (o un edge de CDN), no un runtime de aplicación.
Lo que cuesta, honestamente
Nada de esto es gratis. La salida estática hace una apuesta concreta, y esa apuesta tiene un precio:
Cambiar el contenido exige recompilar y volver a desplegar. Si corrijo una errata en esta entrada, esa corrección no existe en ningún sitio hasta que el pipeline de CI vuelve a ejecutar astro build y los nuevos archivos llegan al hosting. Para un sitio con contenido editorial eso está bien: controlo el ritmo de publicación, y “la compilación tarda un par de minutos” no es un coste real cuando nada aquí es urgente. Pero es una restricción real, no hipotética: no hay ningún panel de administración que edite una página y la publique al instante.
Sin personalización por solicitud. Cada visitante de una URL dada recibe el mismo HTML. No existe eso de “conectado como X, mostrar su panel”, porque no hay código en tiempo de solicitud que compruebe quién pregunta. Si este sitio necesitara alguna vez una página con aspecto distinto según quién la mire, la salida estática no podría hacerlo por sí sola: necesitaría JavaScript en el cliente para obtener y renderizar la parte personalizada después de que se cargue la carcasa estática, que es una herramienta distinta (y más limitada) que SSR.
Sin lógica de A/B ni enrutado condicional en tiempo de solicitud. Algo como “el 50% de los visitantes ve la variante A, el 50% ve la variante B, decidido por solicitud” necesita una decisión tomada cuando llega la solicitud. El hosting estático no puede tomar esa decisión: solo puede servir un archivo fijo por URL. Conseguir A/B testing sobre una salida estática significa o bien variantes en tiempo de compilación en URLs distintas, o empujar la división a JS en el cliente tras cargar la página, lo que añade exactamente la complejidad de runtime que se eligió estático para evitar.
El tiempo de compilación escala con el contenido, y eso acaba notándose. Ahora mismo el log tiene más de quince entradas en dos idiomas y recompilar es lo bastante rápido como para no notarlo. Eso no se sostendrá a una escala arbitraria: un sitio con decenas de miles de páginas necesitaría compilaciones incrementales o una estrategia de renderizado distinta. Este sitio no está cerca de esa línea, pero merece la pena nombrarla como el punto en el que “simplemente recompílalo todo” deja de ser gratis.
Dónde este trade-off cambia de sentido
Elegiría algo distinto para un sitio distinto, y merece la pena ser concreto sobre cuál, en lugar de señalar vagamente hacia “los sitios más grandes necesitan SSR”.
Un sitio con estado real por usuario (un panel que muestra los datos de una cuenta concreta, una aplicación donde lo que ves depende de quién eres) necesita renderizado en tiempo de solicitud o, como mínimo, obtención de datos en tiempo de solicitud. La salida estática no tiene mecanismo para “renderizar esto de forma distinta según quién pregunta”.
Un sitio con precios o inventario en vivo (donde el número en la página tiene que reflejar lo que es cierto ahora mismo, no lo que era cierto cuando se ejecutó la última compilación) necesita SSR o bien obtención de datos en el cliente contra una API en vivo. Hornear un precio en un archivo HTML estático es sencillamente incorrecto desde el momento en que el precio cambia y el archivo no se ha recompilado.
Un sitio donde el contenido debe actualizarse en segundos tras un evento origen (un marcador deportivo en directo, una página de estado que refleja un incidente en curso) no puede tolerar “esperar a la siguiente compilación”. Eso es un problema en tiempo real, y la generación estática resuelve un problema distinto: “este contenido cambia de vez en cuando y de forma predecible, con un ritmo que controlo yo”.
La forma real de este sitio (un log de ingeniería, un portfolio, páginas informativas estáticas) no está cerca de ninguna de esas líneas. Nadie necesita una vista personalizada de una entrada del blog. No hay ningún precio en esta página que pueda quedarse obsoleto. Cuando publico una entrada, está bien que el sitio en producción la refleje unos minutos después en lugar de al instante. Cada razón para recurrir a SSR es una razón que no aplica aquí, y cada coste de lo estático (recompilar para publicar, sin personalización, sin lógica en tiempo de solicitud) es un coste que este sitio nunca iba a pagar de todos modos. Eso no es una coincidencia; es el argumento real para elegir estático desde el principio, construido hacia atrás desde lo que el sitio necesita en lugar de hacia delante desde lo que está de moda ahora mismo.
La arruga de los dos idiomas
El único lugar donde la salida estática crea fricción real aquí es la internacionalización, y merece la pena ser concreto sobre por qué, porque “estático e i18n no combinan” no es exactamente la lección correcta: la lección real es más estrecha. La configuración de i18n dice prefixDefaultLocale: false y redirectToDefaultLocale: false: el inglés vive sin prefijo en la raíz, el español vive bajo /es, y no hay redirección automática según el idioma del navegador del visitante. En una configuración SSR, esa última parte podría gestionarse por solicitud (inspeccionar Accept-Language, decidir el idioma, renderizar en consecuencia) sin necesitar URLs separadas para esa propia decisión. La salida estática no puede tomar esa decisión en tiempo de solicitud, porque no hay código en tiempo de solicitud ejecutándose en absoluto. La solución es exactamente lo que hace este sitio, la misma que describo con más detalle en Cada página es su propio archivo: dos rutas físicas por página, src/pages/about.astro y src/pages/es/about.astro, cada una renderizando su propio diccionario en tiempo de compilación, con la detección de idioma (si la hay) ocurriendo en el cliente después de que la carcasa estática ya se haya cargado.
Eso no es un coste oculto: es el precio visible y estructural del trade-off, y aparece directamente en el árbol de archivos en lugar de en algún comportamiento de runtime que solo descubrirías bajo carga. Cada ruta necesita un archivo por idioma. Son más archivos que mantener, y significa que “añadir una página” es en realidad “añadir dos páginas”, algo que las propias convenciones del proyecto señalan explícitamente en lugar de intentar disimularlo con una capa de generación de código. Prefiero ver ese coste por adelantado en la estructura del repositorio a tenerlo escondido tras una redirección de runtime que solo se revela cuando alguien solicita un idioma que el servidor no esperaba.
El prefetch como la otra mitad de la apuesta
La configuración también activa prefetch: true, y merece la pena conectar ese ajuste con la decisión de ir estático desde el principio en lugar de tratarlo como un capricho sin relación. El prefetch funciona porque el destino es un archivo estático: Astro puede obtener en segundo plano la siguiente página probable en el instante en que un enlace entra en el viewport o recibe el hover, precisamente porque obtenerla pronto no tiene efectos secundarios ni coste en el servidor: son los mismos bytes que recibiría cualquier otro visitante, servidos desde la misma caché de la CDN, tanto si el visitante acaba haciendo clic como si no. Prueba el mismo truco contra una ruta SSR y estarás invocando especulativamente renderizado en el servidor para páginas que nadie va a pedir quizá nunca, lo cual sí es un coste real por cada obtención especulativa, no gratis. La salida estática es lo que convierte “adivinar por adelantado y obtener pronto” en una estrategia sin ninguna desventaja digna de preocupación, y el prefetch es un ejemplo pequeño y concreto de una mejora de experiencia de usuario que sale casi gratis de la arquitectura, en lugar de necesitar su propia justificación.