Lo innovador, en cinco líneas
- La metáfora no es una animación, es la arquitectura. Cada ventana es una página estática con su
<title>, su descripción y su canónica; el escritorio solo sincroniza cuál está encima con la barra de direcciones. - Una sola isla de Preact. El resto del sitio es HTML del build. El texto de todas las ventanas está en el HTML aunque estén cerradas, así que se indexa igual que una página normal.
- Exportar el CV no genera un documento nuevo: promueve la ventana que ya existe a hoja A4 con
@media print. No hay una segunda copia del CV que pueda quedarse vieja. - Las ventanas se montan la primera vez que se abren. Una URL sirve su contenido y solo el suyo; abrir
/settingsno arrastra el CV, la galería y los cuatro proyectos. - Los dos idiomas son direcciones, no un
localStorage./cvy/es/cvson páginas distintas con suhreflang; cambiar de idioma navega, no repinta.
Qué es
Un portafolio personal con metáfora de escritorio: barra de menú, iconos, ventanas arrastrables, dock, tema claro/oscuro, una papelera con chistes dentro y una gaveta de terminal con un CPython de verdad. Se ve como un juguete y por debajo es un sitio estático corriente, que era justo el punto: la gracia solo cuenta si no se paga con SEO, con peso ni con accesibilidad.
Stack: Astro 5 (salida estática) · Tailwind v4 · una isla de Preact · Cloudflare Pages.
Tamaño: 48 archivos TS/TSX/Astro, ~7.900 líneas, 35 páginas generadas, 2 idiomas.
Decisiones de arquitectura interesantes
1. Una URL por ventana, y la ventana la sirve el servidor
Cada ventana está en un mapa de rutas (routes.ts) y de ahí sale una página estática. Añadir una entrada publica la ventana; quitarla la despublica. La isla recibe cuál abrir al montar, así que /photos llega con la galería ya abierta en el HTML, con su <title> propio y su canónica — que es lo que indexa un buscador y lo que se ve al compartir el enlace.
Los slugs están en inglés y son uno solo por ventana: el selector de idioma cambia el contenido, no la dirección. Los slugs viejos en español siguen vivos como 301.
2. Las ventanas se montan al abrirse, no todas de golpe
Window devuelve null mientras no esté montada, y useWindowManager arranca solo con la de la URL. Sin eso, cada una de las 35 páginas cargaría el CV entero, la galería, el changelog y los cinco proyectos para enseñar una sola ventana.
Es la clase de decisión que también fija lo que no se puede hacer: un botón en contacto que llame a window.print() a secas imprimiría una hoja en blanco, porque en /contact la ventana del CV no existe todavía. Por eso exportar desde otra ventana abre primero la del CV y espera a que esté en el DOM.
3. Imprimir es exportar
No hay un PDF subido a public/ que haya que regenerar cada vez que cambia una línea del CV, ni una plantilla aparte que lo maquete. La hoja de estilo de impresión esconde el escritorio entero y promueve .win[data-win="experience"] a página A4, conserve o no su ventana el estado de abierta.
Consecuencia buscada: Cmd+P en cualquier página del sitio produce exactamente el mismo documento que el botón. Un único marcado, cero deriva.
4. El peso que viajaba escondido en un atributo
La isla recibe sus props serializadas en un atributo del HTML. Mandarle los cuatro proyectos enteros en los dos idiomas eran 114 KB —el 81% de cada página— repetidos en todas las páginas del sitio, incluidas /settings y /trash, que no enseñan ningún proyecto.
Ahora viajan solo los metadatos del idioma de la página, y el cuerpo únicamente del proyecto que esa página abre. La portada pasó de 146 KB a 39 KB. El razonamiento que lo cierra: un atributo no lo indexa nadie, así que ese peso no era contenido, era lastre.
5. El idioma vive en la URL, no en el navegador
Mientras el español se elegía solo en cliente, las dos versiones eran la misma página para un buscador: una dirección, un HTML, dos contenidos según lo que dijera localStorage. Ahora /es/... son páginas estáticas de verdad, con su HTML en español, su canónica y su hreflang apuntando a la inglesa.
Los dos diccionarios (copy/es.ts y copy/en.ts) tienen la misma forma exacta —el tipo Copy se deriva del español—, así que añadir una clave en un idioma obliga a TypeScript a pedirla en el otro. Una traducción a medias no compila.
6. Lo que se traduce y lo que no, separados a propósito
Los colores, los nombres de archivo, las coordenadas y los href viven en site.ts; el texto, en copy/. Se unen por id. Si el teléfono estuviera escrito en los dos diccionarios acabaría divergiendo, y nadie se daría cuenta hasta que un visitante marcara el número viejo.
La misma regla aplica a los proyectos: la prosa está en content/projects/<idioma>/<clave>.md y todo lo que es igual en los dos idiomas —año, estado, stack, enlaces, orden— en un único mapa. Un .md sin su entrada en el mapa revienta el build a propósito, y una entrada sin .md también: no se puede quedar a medias.
7. Legible para quien no pinta el HTML
Un <h1> por página y un <main> que separa el contenido del cromo del sistema —la barra de menú y el dock se repiten idénticos en las 35 páginas—, datos estructurados generados desde los mismos arrays que pintan la pantalla, y /llms.txt: el sitio en Markdown, sin escritorio alrededor, para quien lee el HTML sin dibujarlo. Es un endpoint, así que no se queda viejo al añadir una ventana.
Lo que lo hace distinto
- La terminal ejecuta Python de verdad —CPython 3.14 sobre WebAssembly, en tu pestaña— y solo se descarga si escribes el comando. Tiene su propia ficha en este mismo índice.
- El CV se exporta desde la web que lo muestra, sin adjuntos que envejezcan.
- Un changelog que es parte del producto. Las versiones del escritorio están en una ventana, con fecha, y de la primera sale el
dateModifiedde los metadatos: un texto sin fecha verificable es un texto que nadie sabe si sigue vigente. - Tema claro/oscuro sin parpadeo, resuelto antes del primer pintado.
En una línea
Un portafolio con forma de sistema operativo donde lo interesante no es la metáfora sino lo que la metáfora no puede costar: cada ventana sigue siendo una página estática indexable, ligera y traducida, y el CV que se descarga es literalmente el que estás mirando.