Cómo está hecho este blog
The Unnecessary Long Prologue
Siempre me gustó escribir. Desde que tengo uso de razón he escrito de todo. Mi primer recuerdo de algo escrito por mí fue un trabajo para el cole. El título era «Algunos hombres navideños» y parodiaba la película que por aquellas se estrenaba Algunos hombres buenos (de 1992, es decir, con 7 añitos). Trataba de un niño que iba hablando con distintas personas y cómo éstas respondían a su inocente «¡Feliz Navidad!». Algunos eran amables, educados y otros hoscos. No tenía más chicha la cosa. Recuerdo muchas más, desde cuentos, cómics, pequeñas historias que cabían en una cuartilla en máquina de escribir o alguna pequeña obra de teatro.
Que esos recuerdos me hayan acompañado hasta hoy creo que indica que disfruto de escribir y en general crear cosas nuevas. También elegí dedicarme a la tecnología por esa razón; fascinado por los videojuegos, también quise formar parte de ese club y hacer los míos. Y así con todo. Me encanta D&D y disfruto mucho dirigiendo partidas. Disfruto aprendiendo y me dedico a dar algunas clases. Me mueve, pienso, el deseo de compartir las sensaciones que me transmiten las historias con más gente.
A lo largo de todos esos años (desde 1992) siempre he buscado cuál es la mejor manera de dar a conocer lo que hago. Primero pasaba ratos dándole a la máquina de escribir, copiando algunos relatos que luego compartía con compañeros de clase. Luego la cosa se puso sofisticada con los ordenadores e Internet, lo que nos trajo Blogger, Medium, foros de todas clases y sabores y así. Nada de eso me satisfizo lo suficiente, además de que mi compromiso con la escritura en estas plataformas siempre ha sido tirando a bajo, pese a lo que disfruto con ello.
El caso es que hace unos cuantos años ya dije que hasta aquí, que había que tomarse esto un poco más en serio y que mi «yo» creativo me lo iba a agradecer. Aprovechando un proyecto en el que me había metido a trabajar en Google Cloud, me dije que aquí mismo. Registré mi dominio y me puse con una página web estática, usando el framework Hugo donde subir mi contenido usando markdowns. Todo aquello se guardaba en Cloud Storage y funcionaba bien. Pero igual. No me terminaba de hacer tilín. Había cosas que funcionaban a medias, los estilos eran algo rígidos y no casaban al 100% conmigo. Todo ello me hacía perder motivación. Las carencias técnicas me distraían y la creatividad se resentía, porque tengo que tener todo en perfecto estado para ponerme con ello.
Cuando llegó la IA me dije que ésta iba a ser la mIA (🥁) y que qué mejor manera de aprender sobre las nuevas y poderosas herramientas que haciendo las cosas bien de verdad. Ya no iba a esconderme de JavaScript porque ahora tenía un aliado muy potente en Claude Code y ahora iba a estresar al máximo aquello de «Pedid y se os dará».
Quería algo sencillo, austero, sin mucha carga cognitiva. No quería preocuparme de fotos, sidebars,… solo textos y que se leyesen bien. Algunas features interesantes como el tagline rotatorio, las etiquetas, el conteo de palabras y tiempo de lectura.
El resultado lo obtuve a las pocas horas, habiendo gastado muy poquito dinero en crédito de Claude (unos 2 ó 3 dólares) y habiéndomelo pasado genial en el proceso. Además, todo esto inspiró parte de la serie de escritos Una historia del futuro, que estoy disfrutando muchísimo al escribir.
Para los más geeks, aquí tenéis un resumen de cómo son las tripas de este blog. Espero lo disfrutéis tanto como yo haciéndolo.
Nota: Haciendo honor a la verdad y puesto que quiero que el contenido que se publique aquí sea auténtico y humano, he de decir que el texto a partir de aquí me he ayudado un poquito de Claude Code. Se me hace bola escribir documentación como a todo buen developer. Espero que no lo tomen como un EMOSIDO ENGAÑADO, sino como una pequeña licencia.
El generador: Astro
El núcleo del blog es Astro, un generador de sitios estáticos que internamente usa Vite. La salida es HTML puro: no hay JavaScript de framework en el cliente, no hay hidratación, no hay bundle que esperar a que se descargue. Una petición devuelve un fichero HTML, nginx lo sirve, listo.
Astro encaja bien aquí porque el contenido es texto. No necesito estado reactivo ni componentes dinámicos. Lo único que se ejecuta en el navegador es el script de analytics y un par de detallitos de interactividad que ya iremos viendo.
El contenido: Markdown y frontmatter
Cada entrada es un fichero .md dentro de la carpeta posts/. La URL sale directamente del nombre del fichero: posts/blog/como-esta-hecho-este-blog.md se convierte en /posts/blog/como-esta-hecho-este-blog.
El frontmatter es mínimo:
---
title: Cómo está hecho este blog
date: 2026-05-11
description: Arquitectura, decisiones técnicas y detalles de implementación.
tags: [tech]
---
Con draft: true se puede excluir una entrada de la lista pública sin borrar el fichero.
El sistema de etiquetas
Las etiquetas vienen de dos sitios, que se mezclan automáticamente:
- La carpeta: una entrada en
posts/literature/recibe automáticamente la etiquetaliterature. - El frontmatter: cualquier etiqueta adicional se declara en el array
tags.
El resultado se deduplica. Así, un relato de ciencia ficción en posts/literature/ con tags: [sci-fi] acaba con las etiquetas literature y sci-fi sin escribirlo dos veces.
Cada etiqueta tiene su propia página generada estáticamente en /tags/<etiqueta> que lista todas las entradas de esa categoría, y también tiene una descripción configurable que rota entre varias frases cada vez que se carga la página.
El diseño: sin frameworks de CSS
El CSS es un único fichero global de unas trescientas líneas. Sin Tailwind, sin Bootstrap. ¿Para qué, si el sitio es básicamente texto? Las hojas de estilos de utilidades añaden complejidad de build para muy poco beneficio en un caso de uso tan sencillo.
Las decisiones de diseño más relevantes:
- Tipografía: Georgia como fuente principal. Es serif, se lee bien en pantalla, está en todos los sistemas y no necesita cargar nada externo.
- Ancho máximo: 640px. Es la anchura de columna que favorece la lectura de texto largo.
- Modo oscuro: el CSS escucha
prefers-color-scheme: darky cambia la paleta de colores automáticamente. Sin toggle, sin JavaScript. - Fondo crema:
#fffef9en modo claro, más suave que el blanco puro.
El tiempo de lectura
Cada entrada muestra una estimación del tiempo de lectura calculada en build time, no en el cliente. El cálculo es elemental: contar palabras dividido entre 200 (palabras por minuto promedio), con un mínimo de 1 minuto.
const words = post.rawContent().trim().split(/\s+/).length;
const readingTime = Math.max(1, Math.round(words / 200));
Los taglines rotantes
En la página principal hay una frase en cursiva que cambia cada diez segundos. Las frases están definidas en un fichero de datos estático. Astro renderiza una frase aleatoria en el servidor durante el build; luego un pequeño script en el cliente toma el relevo y la va rotando con una transición de opacidad.
Es el único JavaScript no-trivial que corre en el navegador, y son quince líneas.
RSS y sitemap
Los dos están generados automáticamente por integraciones oficiales de Astro: @astrojs/rss y @astrojs/sitemap. El feed RSS está disponible en /rss.xml y hay un enlace en el footer de todas las páginas. El sitemap se genera en /sitemap-index.xml apuntando a todas las URLs públicas del sitio.
Los metadatos: Open Graph y Twitter Cards
Cada página incluye metadatos para que los previews en redes sociales funcionen: título, descripción, URL canónica, y las etiquetas og: y twitter:. Todo se genera desde el layout base a partir de las props que le pasa cada página.
El botón de compartir
Al final de cada entrada hay tres opciones para compartir: X, LinkedIn, y copiar el enlace. X y LinkedIn son simplemente enlaces a sus respectivas URLs de intent. El botón de copiar usa la API navigator.clipboard y cambia su texto a “Copied!” durante dos segundos como confirmación.
Las analíticas: GoatCounter
Necesitaba saber si alguien leía el blog, pero sin suscripciones de pago y sin cookies. Evalué varias opciones:
- Plausible cloud: un dashboard estupendo, pero ~9€/mes y no me terminaba de compensar para un blog personal.
- Plausible self-hosted: requiere Docker Compose + PostgreSQL + ClickHouse. Demasiada infraestructura para lo que es esto.
- Umami: buena alternativa open source, pero igualmente necesita una base de datos y un servidor corriendo.
- Logs de nginx: ya están ahí sin coste adicional, pero requieren parseo manual y no sobreviven reinicios del contenedor sin un volumen persistente.
- GoatCounter: gratis para uso personal, sin cookies, respetuoso con la privacidad, y solo necesita una etiqueta
<script>en el HTML.
Elegí GoatCounter. El dashboard está en raulrc.goatcounter.com y los datos se almacenan en servidores en la UE. Si algún día necesito control total de los datos, la versión self-hosted es un binario de Go que puedo desplegar sin base de datos externa.
El despliegue: Docker + AWS Lightsail
El Dockerfile usa un build multistage:
- Stage 1 (builder): imagen de Node, instala dependencias y ejecuta
astro build. La salida es un directoriodist/con HTML, CSS y assets estáticos. - Stage 2 (servidor): imagen
nginx:alpinede unos 25MB, copia únicamente eldist/del stage anterior. Node no existe en la imagen final.
FROM node:lts-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Astro genera URLs con estructura de directorios (posts/hello/index.html), así que la configuración por defecto de nginx las sirve correctamente sin reescrituras adicionales.
El contenedor corre en AWS Lightsail, que es la opción más simple y económica para desplegar un contenedor sin gestionar Kubernetes ni load balancers. No se espera una carga muy alta (por el momento) y además en esta instancia tengo algunos proyectos corriendo en paralelo, así que el coste peude considerarse mínimo. Únicamente hay que tener cuidado a la hora de reiniciarla, no detenerla por completo y perder la IP, lo que pondría patas arriba el acceso.
El despliegue continuo: GitHub Actions
Hacer git push a main activa automáticamente un workflow de GitHub Actions que redespliega el blog. El fichero .github/workflows/deploy.yml hace exactamente lo siguiente:
- Se conecta por SSH a la instancia de Lightsail usando credenciales almacenadas como secrets del repositorio.
- Hace
git pullpara traer el código nuevo. - Reconstruye la imagen Docker.
- Para y elimina el contenedor anterior.
- Arranca el contenedor nuevo con
--restart unless-stopped.
- name: Deploy to Lightsail
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.LIGHTSAIL_HOST }}
username: ${{ secrets.LIGHTSAIL_USER }}
key: ${{ secrets.LIGHTSAIL_SSH_KEY }}
script: |
cd ~/raulrc-blog
git pull origin main
docker build -t raulrc-blog .
docker stop raulrc-blog || true
docker rm raulrc-blog || true
docker run -d --restart unless-stopped -p 80:80 --name raulrc-blog raulrc-blog
El workflow también se puede lanzar manualmente desde la interfaz de GitHub con workflow_dispatch, útil para forzar un redespliegue sin necesidad de hacer un commit vacío.
El tiempo de ejecución es de unos pocos segundos, así que GitHub no nos pondrá ninguna pega por ello hasta los 2000 minutos al mes.
El dominio y el HTTPS
El dominio raulrc.com está registrado en Squarespace Domains (anteriormente Google Domains, que Squarespace adquirió en 2023). No uso ninguna otra función de Squarespace; es simplemente el registrador.
El HTTPS lo gestiona Cloudflare, que actúa como proxy delante del servidor. El flujo, visto de extremo a extremo junto con el pipeline de build, queda así (la imagen está hecha por Claude y el MCP de draw-io, BTW):

En texto plano, el camino de una petición es:
Navegador → Cloudflare (TLS) → AWS Lightsail (HTTP en el 80)
Cloudflare termina la conexión TLS con el navegador y reenvía el tráfico al contenedor en texto plano. Esto significa que el contenedor de nginx solo escucha en el puerto 80 y no necesita gestionar certificados. Cloudflare renueva el certificado automáticamente y además proporciona una capa de caché y protección frente a bots sin coste adicional para un blog personal.
Los DNS de raulrc.com apuntan a Cloudflare, y desde ahí se configura el registro A que apunta a la IP de Lightsail.
Lo que no hay
No hay base de datos. No hay CMS. No hay sistema de comentarios. No hay autenticación. El blog es una colección de ficheros de texto con un proceso de build por encima, y eso lo hace cómodo de mantener y, sobre todo, de entender cuando vuelvo al código pasado un tiempo.
Cuando quiero publicar una entrada, creo un fichero Markdown, hago git push, y el contenedor se reconstruye solo. Ya está.
Apagando Claude Code desde aquí.
Epílogo
Puede que publique todo esto como Open Source más adelante, pero por ahora prefiero tener las vergüenzas ocultas a la vista. Por primera vez sí que puedo decir que estoy satisfecho con todo; tengo el dominio, una estética minimalista que a mí se me hace cómoda para leer y pleno control sobre qué hay por debajo. Además de que las tareas de mantenimiento y actualización son ahora divertidas y dado el poquísimo tiempo que tengo para dedicarle (unos pocos minutos al día, literalmente), pues el truco ha salido bien.
Llenarlo de contenido es igualmente entretenido y gratificante, así que espero que disfrutéis leyendo tanto como yo compartiendo.