Caso de estudio · Proyecto
Un sitio estático en Astro, en dos idiomas, donde cada cifra se calcula de un registro tipado y cada página se prueba en un navegador real.
- Stack
- Repositorio
- github.com/bgalvandev/brunogalvan.dev
- Demo
- Sin demo pública
Sección 1 de 4: Contexto
Mi portafolio, reconstruido en septiembre de 2026 con la gramática de un único diseño de referencia. Se publica como HTML estático con un solo script empaquetado para el movimiento; cada texto visible vive en un catálogo en español y otro en inglés, y cada número se deriva del registro al compilar el sitio.
Sección 2 de 4: Arquitectura
Registro
src/data guarda los roles, los proyectos y el stack; los años, los conteos y el gráfico de trayectoria se calculan de ahí al compilar.
Rutas
Las URL localizadas viven como pares en un solo archivo, así que cada página existe en ambos idiomas con hreflang recíproco.
Tokens
Colores y tipografías salen de tokens semánticos con un valor claro y uno oscuro; una banda oscura los resuelve de nuevo cambiando el esquema de color.
Movimiento
GSAP y Lenis corren en un solo contexto que se revierte por completo con movimiento reducido o con el control de pausa.
Sección 3 de 4: Decisiones
Sin framework en el navegador
Salida estática; el único script es el paquete de movimiento, y cada página funciona sin JavaScript.
Una política hecha de hashes
El build calcula el hash de cada script y estilo en línea y lo suma a la Content Security Policy, así que un bloque editado nunca sale con un hash viejo.
Probado tal como se publica
Playwright recorre el build de producción con axe en ambos temas y reproduce la política en cada página.
Decisiones por escrito
El hosting, la librería de movimiento y el scroll suave tienen cada uno un ADR con las opciones comparadas y el costo en kilobytes.
Sección 4 de 4: En el código
// Cloudflare Pages reads `_headers` from the build output. The Content Security
// Policy is derived from the built HTML: every inline script and style block
// becomes a sha256 source, so the pre-paint theme initializer and the font
// pipeline's @font-face block keep working without 'unsafe-inline', and an
// edited inline block can never ship with a stale hash.
const inlineScript = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g;
const inlineStyle = /<style[^>]*>([\s\S]*?)<\/style>/g;
const sha256 = (text) =>
`'sha256-${createHash('sha256').update(text).digest('base64')}'`;