Una de las primeras decisiones cuando empecé erandel.com fue cómo iba a desplegar todo. Como ya conté en por qué hago juegos web, el ciclo "detectar → arreglar → desplegar" es lo que más valor me da trabajando solo. Esto solo funciona si publicar una versión nueva es trivial. Esta es la pieza que hace que sea trivial.
El stack en una frase
S3 + CloudFront + Lambda@Edge, todo provisionado con AWS CDK, y una capa de prerender estático con Puppeteer encima de cada SPA en Vue. Cero servidores propios, cero contenedores, cero base de datos para servir la web.
Un bucket, varias apps
El sitio público vive en un único bucket de S3 — prod.erandelcom-webapp — pero contiene varias aplicaciones distintas:
- landing en la raíz:
/,/blog,/games/..., etc. - triara en
/triara/, accesible desdetriara.erandel.com. - picturim en
/picturim/, accesible desdepicturim.erandel.com. - vectron en
/vectron/, accesible desdevectron.erandel.com.
El truco es que cada subdominio entra a CloudFront con un comportamiento distinto: el origen es siempre el mismo bucket, pero el path se reescribe para que el subdominio "vea" su carpeta como si fuera la raíz. Esto me ahorra mantener cuatro distribuciones diferentes y cuatro infras paralelas.
Lambda@Edge para el routing limpio
Las SPAs no tienen archivos físicos en cada ruta. Si un usuario entra directamente a /blog/welcome-to-erandel, S3 no encuentra ese archivo y devolvería un 404. Una pequeña Lambda@Edge (en us-east-1, como obliga CloudFront) intercepta la request y la redirige al index.html correspondiente. Es el equivalente al típico try_files de nginx, pero distribuido por todo el edge de AWS.
Tener la lambda en us-east-1 mientras el resto de la infra vive en eu-west-1 obligó a hacer doble bootstrap del CDK — un detalle pequeño pero molesto la primera vez.
El prerender: por qué y cómo
Una SPA recién montada solo manda al crawler un <div id="app"></div> vacío. Para SEO, AdSense y cualquier preview en redes sociales, eso es invisible. La solución que tengo es un script de Node, scripts/prerender.mjs, que se ejecuta justo después del vite build:
- Levanta un
vite previewlocal en el puerto 4319. - Lanza Puppeteer en modo headless.
- Recorre una lista de rutas (incluyendo cada post del blog).
- Espera a que la página termine de hidratarse y a que el título no esté vacío.
- Vuelca el HTML resultante en
dist/<ruta>/index.html.
Después de eso, cada URL pública tiene un HTML real e indexable detrás. La SPA sigue hidratando encima en el navegador, así que la experiencia de cliente no cambia — solo cambia lo que ven los crawlers en el primer fetch.
Un detalle importante con el blog
Como cada post del blog tiene su propia ruta, cada vez que añado uno nuevo (como este) tengo que añadirlo también a la lista de rutas del prerender. Es un paso manual a propósito: prefiero esa fricción a generar la lista dinámicamente y arriesgarme a prerenderizar borradores o entradas a medias.
El comando de despliegue
El script app/deploy.sh reúne todo. Cada app tiene su propio npm run deploy:prod que hace básicamente cuatro cosas:
- Sube la versión del
package.jsonconnpm version patch. - Compila la app (
vite build) y, en el caso de la landing, ejecuta el prerender. - Sincroniza los assets a S3 con
aws s3 syncusando--size-onlypara los binarios. - Vuelve a sincronizar los
.htmlen una segunda pasada, esta vez sin caché agresiva, para que los cambios de HTML se vean al instante.
El último paso es invalidar la caché de CloudFront para los .html y para la raíz. Los _assets/ están versionados con hash en el nombre, así que no necesitan invalidación nunca.
¿Cuánto tarda en estar arriba un cambio?
Desde que lanzo ./deploy.sh landing hasta que el cambio es visible para todos los usuarios pasan, en condiciones normales, menos de tres minutos. Esa es exactamente la propiedad que me importa: la fricción para arreglar algo es tan baja que no me lo pienso dos veces. Si encuentro una errata en este mismo post mientras lo escribo, la corrijo, la subo y a los pocos minutos está arreglada para todo el mundo.
Lo que no está aquí (y por qué)
No hay CI/CD automático todavía. El despliegue lo lanzo a mano desde mi máquina. Para un proyecto en solitario me funciona — me obliga a estar consciente de cada deploy y a no romper nada de madrugada por accidente. Cuando el proyecto crezca o se sume alguien más, lo siguiente será mover este pipeline a GitHub Actions con un rol IAM dedicado. No antes.
Si te interesa cómo está montado el bucket, la distribución o las lambdas, todo el código está en la carpeta infra/cdn del repo. Es CDK en TypeScript, sin trucos.