‹ Volver al blog

Cómo está hecho este blog: Astro, Cloudflare Pages y un dominio propio

· 14 min de lectura
Tabla de contenidos

Todo blog es un conjunto de decisiones sobre dónde ocurre el trabajo. Un CMS tradicional como WordPress hace el trabajo en tiempo de request: un lector pide una página, un servidor ejecuta código, consulta una base de datos, arma el HTML y lo devuelve. Ese modelo es flexible, y también es un compromiso permanente — un proceso que mantener vivo, una base de datos que respaldar, una superficie que parchar.

Este sitio toma la decisión opuesta. Todo el trabajo ocurre una vez, por adelantado, en mi máquina y en un servidor de build. Lo que llega al lector es el resultado terminado: HTML, CSS y menos de 2 KB de JavaScript por página, servidos como archivos desde una CDN. No hay servidor que correr, ni base de datos que mantener, ni nada ejecutándose en tiempo de request. Este artículo es un recorrido por ese pipeline, capa por capa, con los números reales del propio build de este sitio.

Dos herramientas cargan con casi toda la arquitectura, así que vale la pena nombrarlas desde el principio. Astro es un generador de sitios estáticos: toma contenido y plantillas y renderiza el HTML final en tiempo de build, y no envía JavaScript al cliente salvo que un componente lo pida de forma explícita. Cloudflare Pages es el host: construye el proyecto directamente desde un repositorio Git y sirve la salida desde su red global de edge. Entre esos dos está el contenido, que vive como archivos Markdown en el repo en vez de filas en una base de datos. El resto es cómo se conectan esas piezas.

Arquitectura de punta a punta de este blog: fuente en Markdown, Astro construye HTML estático, el resultado se envía a GitHub, Cloudflare Pages lo construye y lo sirve desde una CDN global, y los lectores llegan a través del dominio propio hernan.tech.

La capa de contenido: archivos en vez de base de datos

La primera decisión es dónde vive la escritura. Acá cada entrada es un archivo, y el repositorio es la base de datos. Una entrada es un documento Markdown bajo src/data/blog, con un bloque de frontmatter arriba para su metadata: título, descripción, fecha de publicación, tags. No hay panel de administración ni login. Para publicar, agrego un archivo y lo confirmo (commit).

El riesgo de tratar archivos como base de datos es que nada garantiza su forma. Una fecha mal escrita o un título olvidado normalmente aparecería como una página rota en producción, descubierta por un lector. Astro cierra esa brecha con las colecciones de contenido: una colección declara un esquema, y cada archivo se valida contra él durante el build. El esquema acá está escrito con Zod, una librería de esquemas para TypeScript, así que el frontmatter se verifica con tipos con el mismo rigor que el resto del código.

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/data/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDatetime: z.date(),
    tags: z.array(z.string()).default(["others"]),
    draft: z.boolean().default(false),
    translationKey: z.string().optional(),
  }),
});

El efecto es que el contenido inválido no puede llegar a producción. Si pubDatetime no es una fecha real, o falta el title, el build falla en mi máquina, mucho antes de que algo se publique. El error se mueve de la pantalla del lector a mi terminal, que es exactamente donde lo quiero.

Un campo de ese esquema hace más que validar. translationKey es la clave de unión para la internacionalización: dos entradas que comparten una clave se entienden como el mismo artículo en distintos idiomas. Este sitio es bilingüe, con el inglés servido desde la raíz y el español desde /es. El conmutador de idioma resuelve la contraparte de una entrada a través de esa clave, y cuando una entrada no tiene traducción, el conmutador se muestra deshabilitado en vez de enlazar a una página que no existe. La relación entre las dos versiones de idioma es dato, no un enlace cruzado manual que yo tenga que acordarme de actualizar.

El build: Astro y cero JavaScript por defecto

Un sitio estático igual necesita algo que convierta el contenido en páginas, y ese es el trabajo de Astro. El detalle que más importa es su modelo de ejecución. Muchos frameworks renderizan en el navegador, enviando un bundle de JavaScript que reconstruye la página en el cliente después de cargar. Astro lo invierte: renderiza a HTML en tiempo de build y no envía ningún runtime al cliente salvo que un componente específico pida hidratación — Astro llama “islands” a esos componentes interactivos.

Este sitio no usa islands. El único JavaScript que llega al lector es un pequeño helper de prefetch. Esa decisión se ve en los números. Este es el tamaño de transferencia medido de una página de artículo, comprimido (gzip):

RecursoGzip
HTML6,8 KB
CSS3,8 KB
JS1,0 KB
Total11,6 KB

Para tener contexto, la página mediana que registra el HTTP Archive supera los 2 MB. Esta página es unas 170 veces más pequeña, y la mayor parte de esa diferencia es JavaScript que nunca se envía.

Trabajar en tiempo de build también cambia lo que “cuestan” las funcionalidades. En una plataforma dinámica, la búsqueda, las imágenes sociales y los feeds suelen ser plugins que corren en tiempo de request o servicios que conectas y mantienes. Acá son pasos del build que producen salida estática y después no tienen ningún costo en runtime:

  • La búsqueda la maneja Pagefind, que construye un índice estático durante el build y ejecuta la consulta en el navegador contra ese índice. El índice es el único recurso genuinamente pesado del proyecto, así que se carga solo en /search y nunca en una página de artículo.
  • Las imágenes de previsualización social — las tarjetas que aparecen al compartir un enlace — se generan una por entrada con astro-og-canvas, renderizadas desde fuentes incluidas en el repo, así que el build no necesita red y produce la misma salida cada vez.
  • El feed RSS y el sitemap se regeneran en cada build, uno por locale, con las rutas de imágenes /og/ excluidas del sitemap.
  • El resaltado de sintaxis lo hace Shiki en tiempo de build, emitiendo dos temas (github-light y github-dark) que se intercambian con variables CSS. Ningún resaltador corre en el navegador.

El hilo común es que cada uno de estos corre una vez, cuando se construye el sitio, y no deja atrás más que archivos.

Salida y despliegue: de una carpeta al edge

Cuando el build termina, astro build ha escrito el sitio completo en una carpeta llamada dist/ — cada página, feed e imagen, lista para servir. En este sitio, un build limpio en Node 24 produce:

  • 2,58 segundos en total. Astro reporta las 30 páginas en sí en 1,57 s; el resto es sincronización de contenido, indexado de Pagefind y generación de imágenes OG.
  • 30 páginas, que cubren ambos idiomas, cada página de etiqueta, el archivo, el feed RSS y el sitemap.
  • 3,0 MB de salida total para todo el sitio.

Llevar esa carpeta a internet es donde entra Cloudflare Pages, y el modelo es dirigido por Git, no manual. No construyo localmente y subo. En vez de eso, hago push a GitHub; Cloudflare detecta el push, ejecuta npm run build en sus propios runners y distribuye el dist/ resultante por su red de edge, de modo que el lector recibe el sitio desde un lugar cercano. El log de despliegue de este sitio se ve así:

Log de despliegue de Cloudflare Pages: clona el repo, npm ci, npm run build (Astro construye 30 páginas en 1,57s), Pagefind indexa las páginas, el build termina en 2,58s, publicado en hernan.tech sin errores. Estadísticas al pie: 2,58s de build, 30 páginas, 3,0 MB de salida, ~2 KB de JS por artículo, 500 builds gratis al mes, $0 de costo.

Como el despliegue está atado al repositorio, hay varias cosas que vienen gratis y que de otro modo serían configuración y mantenimiento:

  • El TLS lo aprovisiona y renueva Cloudflare. El certificado no es algo que yo administre.
  • Los preview deploys le dan a cada rama su propia URL aislada, así un cambio se puede revisar en vivo antes de tocar el dominio de producción.
  • La configuración del edge vive en el repo. Dos archivos, _headers y _redirects, definen cabeceras de seguridad y enrutan URLs antiguas, y están versionados junto al código como todo lo demás.

DNS: apuntar el dominio a Pages

Con el sitio desplegándose, la última conexión es el dominio. Mapear tanto el ápice (hernan.tech) como www a Pages se reduce a dos registros:

Tipo    Nombre          Valor
CNAME   hernan.tech     <proyecto>.pages.dev
CNAME   www             <proyecto>.pages.dev

Hay una sutileza escondida en la primera línea. Un CNAME en el ápice de un dominio es inválido según el RFC 1034, porque el ápice ya tiene otros registros (como SOA y NS) y un CNAME no puede coexistir con ellos. Cloudflare lo resuelve con CNAME flattening: acepta el CNAME para el ápice pero responde las consultas con los registros A/AAAA resueltos del destino, así que la raíz se comporta como un registro A aunque tú la configures como CNAME. En un registrador sin flattening, apuntarías el ápice directamente a las direcciones IP anycast de Pages.

Del lado del lector nada de esto es visible. Escribe el dominio, el DNS lo enruta a la copia más cercana del sitio, y carga sobre una conexión que Cloudflare aseguró. De mi lado fue una configuración de una sola vez.

Saber quién lee: analítica sin rastreo

Un sitio estático no tiene un servidor registrando solicitudes, así que la pregunta evidente es cómo veo si alguien lo lee. La respuesta habitual — poner Google Analytics — desharía dos de las cosas sobre las que está montado este sitio: agrega JavaScript en el cliente y rastrea al lector. Cloudflare ofrece su propia analítica que evita ambas.

Cloudflare Web Analytics es gratuito y prioriza la privacidad. No pone cookies, no escribe nada en localStorage y no genera una huella (fingerprint) del visitante a través de su dirección IP ni de su User-Agent, así que no necesita banner de cookies. Lo que reporta es la forma agregada del tráfico:

  • Vistas de página y visitantes, a lo largo del tiempo.
  • Páginas más vistas — qué entradas se leen de verdad.
  • Referentes — de dónde viene el tráfico.
  • Países, navegadores, sistemas operativos y tipos de dispositivo — segmentos amplios, no individuos.

También muestra los Core Web Vitals medidos desde visitas reales y no desde una prueba de laboratorio: Largest Contentful Paint (LCP), Interaction to Next Paint (INP) y Cumulative Layout Shift (CLS) — velocidad de carga, capacidad de respuesta y estabilidad visual. Para un sitio que compite por ser liviano, ese es el circuito de retroalimentación que confirma que sigue liviano en terreno.

Hay una tensión honesta que conviene nombrar. Esas métricas de campo las recolecta un pequeño beacon de JavaScript que usa la Performance API del navegador, así que activarlas agrega algo de código en el cliente a un sitio que casi no envía nada. La alternativa no cuesta nada extra: como cada solicitud ya pasa por el edge de Cloudflare, el panel reporta el tráfico del lado del servidor sin ningún beacon — cambias el detalle de los Core Web Vitals por mantener la página sin scripts. En cualquier caso, la analítica es parte del mismo plan gratuito, sin un tercero en el camino.

Cuánto cuesta

Con la arquitectura en su lugar, el costo se desprende de ella. El cómputo, el ancho de banda del edge y el TLS son todos $0 en el plan gratuito, lo que deja al dominio como el único gasto recurrente.

ÍtemBlog dinámicoEste montaje
Hosting / servidor$5–20 / mes$0
Base de datosincluida o aparteninguna
TLS$0–50 / año o a mano$0, renovado solo
CDN / ancho de bandamedido o aparte$0, sin medición
Dominio$10–20 / año$10–20 / año
Total anual$70–250+$10–20 (solo dominio)

El dominio es también donde vive la única trampa real de precio, y vale la pena ser preciso. El número que importa es el de renovación, no la promoción del primer año. Un dominio .tech puede costar un par de dólares al registrarlo y después renueva a $40–50 al año; un .com simple se mantiene estable en $10–15 al año, renovación incluida. Lee la fila de renovación antes de comprometerte con un nombre.

La otra pregunta que invita esa tabla es dónde termina el plan gratuito, ya que “gratis” hace trabajo real en ella. Los techos publicados de Cloudflare son 500 builds al mes, ancho de banda y solicitudes sin medición, 100 dominios propios, 20.000 archivos por deploy y 25 MiB por archivo. Hay dos maneras de pasar de $0: más de 500 builds en un mes te lleva al plan Pro de $20 al mes, y cualquier Pages Function que agregues consume del plan gratuito de Workers de 100.000 solicitudes por día antes de facturar. Un blog estático no alcanza ninguna — 500 builds son dieciséis deploys cada día, y no hay funciones que invocar.

Lo que “estático” igual incluye

Es fácil leer “estático” como “limitado” — una carpeta de páginas planas y poco más. Acá es al revés. Como el trabajo ocurre en tiempo de build, la salida puede ser rica sin costar nada en runtime. Todo lo de la lista de abajo es parte de este sitio, y todo se entrega como archivos estáticos:

  • Markdown con tipos — frontmatter validado contra un esquema Zod en el build.
  • Rápido por defecto — ~12 KB por artículo, sin framework de cliente.
  • Accesible — navegable por teclado y compatible con lector de pantalla (VoiceOver).
  • Responsive — del teléfono al escritorio.
  • Amigable con SEO — URLs canónicas, metadata, datos estructurados.
  • Modo claro y oscuro.
  • Búsqueda estática — texto completo con Pagefind, corriendo en el navegador.
  • Borradores y paginación.
  • Sitemap y feed RSS — regenerados por locale en cada build.
  • Soporte MDX — componentes dentro del Markdown cuando una entrada lo necesita.
  • Tabla de contenidos colapsable en cada entrada.
  • Construido con buenas prácticas y altamente personalizable.
  • Generación dinámica de imágenes OG — una tarjeta social renderizada por entrada.
  • Listo para i18n — la separación inglés/español descrita más arriba.

Ninguno de estos es un servicio corriendo en algún lado con una cuenta asociada. Cada uno es un paso del build que produce archivos, que es justo el punto: el conjunto de funcionalidades de un sitio dinámico, entregado con el perfil operativo de uno estático.

Dónde se acaba lo estático

Sería deshonesto presentar esto como la respuesta a todo, así que vale la pena marcar el límite. Lo estático es el modelo correcto cuando la respuesta es la misma para cada visitante: blogs, documentación, portafolios, sitios de marketing. En el momento en que necesitas lógica de servidor por request — sesiones autenticadas, un store de comentarios en vivo, un carrito, cualquier cosa personalizada al momento del request — lo puramente estático ya no basta por sí solo.

El límite no es un muro. Puedes agregar comportamiento en tiempo de request a una base estática con Pages Functions o Workers, manejando las partes dinámicas en el edge mientras el resto sigue estático. Pero si un producto es mayormente lógica en tiempo de request, no debería partir de una arquitectura estática-primero; las partes estáticas serían la excepción y no la regla. Un blog es el caso opuesto, y por eso el calce es tan limpio acá.

La conclusión

Lo que destaca, después de construirlo, es lo poco que el sistema pide atención continua. El costo recurrente es un dominio, y la tarea recurrente es git push. Todo lo que va entre el editor y el edge — validación, renderizado, indexado, generación de imágenes, TLS, distribución — ocurre una vez en tiempo de build y después no me pide nada más.

Ese es el verdadero resultado de mover el trabajo al tiempo de build: no solo los números pequeños, sino dónde termina el trabajo. Queda en un lugar que puedo ver, versionar y volver a ejecutar, en vez de un proceso que tengo que mantener vivo. Lo que queda por pensar es la parte que siempre fue el punto — la escritura.


El diagrama de arquitectura es editable — el archivo fuente de draw.io vive junto a esta entrada. Las cifras de build provienen de un build local limpio (Node 24, Astro 6) y varían según el contenido y el hardware. Las opiniones aquí son mías y no representan a mi empleador.