handbook técnico technical handbook
El sistema. The system.
Principios, decisiones y bases del sitio: por qué se construye así, cómo se toman las decisiones y qué piezas reutilizables sostienen el resultado. Principles, decisions and foundations behind the site: why it is built this way, how decisions are made and what reusable pieces hold it together.
01 · POR QUÉ 01 · WHY
Principios. Principles.
Principios técnicos opinionados que guían cómo construyo, reviso y entrego software. Opinionated technical principles that guide how I build, review and ship software.
`any` no es dogma, es triaje. `any` isn't dogma, it's triage.
TypeScript estricto bloquea en review por una razón concreta: cuando llega un bug, `any` es el primer sitio donde se esconde la causa raíz. Eliminarlo de antemano hace que el debugger devuelva señal en vez de ruido. Strict TypeScript blocks review for a concrete reason: when a bug lands, `any` is the first place root cause hides. Banning it up front means the debugger gives signal instead of noise.
CSS modules > Tailwind y styled-components. CSS modules > Tailwind and styled-components.
Cada componente con estilo lleva su `.module.css` gemelo. El CSS queda acoplado al único componente que lo usa, los nombres camelCase son explícitos y nada se filtra a global. Tailwind es eficiente para prototipos; en proyectos largos es deuda en el HTML. Every styled component ships with its twin `.module.css`. Styles stay coupled to the one component that uses them, camelCase names are explicit and nothing leaks to global. Tailwind is efficient for prototypes; on long-running projects it becomes debt in the HTML.
Los tests prueban comportamiento, no la forma del código. Tests cover behavior, not code shape.
Lógica testeable se prueba como función pura; render se prueba con `AstroContainer` cuando hay branching real. No se mockea lo que se puede ejecutar de verdad: un mock que pasa con la prod rota es la peor mentira posible. Testable logic gets tested as a pure function; render gets tested with `AstroContainer` when real branching is involved. You don't mock what you can actually run: a mock that passes while prod is broken is the worst kind of lie.
Un buen handoff vale más que el artefacto. A good handoff beats the artifact.
Un PR con descripción que explica el porqué y cómo se verificó vale más que mil tests verdes silenciosos. Si la siguiente persona que toca el código no entiende qué intentaba la última, los tests son recordatorios, no documentación. A PR whose description explains the why and how it was verified is worth more than a thousand silent green tests. If the next person touching the code can't see what the last one was after, tests are reminders, not documentation.
Las visual baselines son parte del ciclo, no del QA. Visual baselines belong in the loop, not in QA.
Si una feature toca CSS o HTML, regenerar las baselines en Docker (el mismo runtime que CI) es responsabilidad del implementer, no de un job nocturno. Feedback visual rápido durante el ciclo vale más que un mock perfecto al final. If a feature touches CSS or HTML, regenerating baselines in Docker (the same runtime CI uses) is the implementer's job, not a nightly check. Fast visual feedback inside the loop beats a perfect mock at the end.
02 · CÓMO 02 · HOW
Decisiones. Decisions.
Decisiones de UX y técnicas que tomaría de nuevo en otro proyecto similar, con sus alternativas consideradas. UX and technical decisions I would make again on a similar project, with the alternatives I considered.
Set extendido de 11 tokens (no mínimo de 5) Extended set of 11 tokens (not a minimal set of 5)
Cubrir superficies (bg, bg-elev, bg-elev-2), texto (fg, fg-dim, fg-mute), líneas (line, line-soft) y acento (accent, accent-dim, warn) permite jerarquías visuales finas sin volver a hardcodear valores. Covering surfaces (bg, bg-elev, bg-elev-2), text (fg, fg-dim, fg-mute), lines (line, line-soft) and accent (accent, accent-dim, warn) gives fine visual hierarchy without hardcoding fallbacks.
Tokens declarados por theme, sin herencia en cascada Tokens declared per theme, no cascade inheritance
Cada theme redeclara los 11 tokens explícitamente; un theme no hereda del anterior. Cambios fuera del set canónico se detectan al revisar el archivo. Each theme redeclares all 11 tokens explicitly; no theme inherits from another. Changes outside the canonical set are obvious when reviewing the file.
Geist self-hosted vía @fontsource Geist self-hosted via @fontsource
Sin Google Fonts en runtime: cero requests externos en cold-load de GH Pages, paridad de fuentes con CI, y cero dependencia de privacidad de terceros. No Google Fonts at runtime: zero external requests on GH Pages cold-load, font parity with CI, and no third-party privacy dependency.
View transitions del navegador con fallback CSS Browser view transitions with CSS fallback
El cambio de theme usa document.startViewTransition cuando está disponible y cae a transiciones CSS en navegadores que aún no lo soportan, sin saltos perceptibles. Theme switching uses document.startViewTransition when available and falls back to CSS transitions on browsers without support, with no jarring jump.
Ciclo dark → light → paper con shortcut T Cycle dark → light → paper with T shortcut
Un solo botón cíclico es más simple que un selector con dropdown; el orden empieza por el theme default (dark) y termina por el más exótico (paper). La tecla T da acceso rápido sin tocar el ratón. A single cyclic button is simpler than a dropdown selector; the order starts at the default theme (dark) and ends at the most exotic one (paper). T gives keyboard-first access without reaching for the mouse.
El theme-toggle muestra el icono del NEXT theme The theme toggle shows the NEXT theme icon
El icono representa la acción (al pulsar irás aquí), no el estado actual. Reduce confusión cuando alguien ve el botón sin contexto previo. The icon represents the action (clicking takes you here), not the current state. Reduces confusion for anyone seeing the button without prior context.
Brand jc con split-link en el nav Brand jc with split-link in the nav
El monograma jc es a la vez identidad y home-link, separado de los anchors de sección para que el primer click no requiera precisión de píxel. The jc monogram acts as identity and home link at once, kept separate from the section anchors so the first click does not require pixel precision.
Link discreto del Lab al design system Discreet link from Lab to the design system
El sistema detrás de las piezas del Lab vive en /the-system/. Un link mono pequeño al final del grid invita a explorar sin gritar; quien busca craft lo encuentra, el resto sigue de largo. The system behind the Lab pieces lives at /the-system/. A small mono link at the end of the grid invites exploration without shouting; those looking for craft find it, the rest move on.
Generación estática con Astro para GitHub Pages Static site generation with Astro for GitHub Pages
Elimina la necesidad de infraestructura serverless o CDN personalizado. Astro compila el CV a HTML/CSS/JS plano con Content Collections para contenido bilingüe, permitiendo hosting gratuito y sin latencia de servidor. Eliminates the need for serverless infrastructure or custom CDN. Astro compiles the CV to plain HTML/CSS/JS with Content Collections for bilingual content, enabling free hosting and zero server latency.
Alternativas consideradas Alternatives considered
Next.js/Nuxt/SvelteKit SSR hubieran requerido un backend. Hugo/Jekyll son menos flexibles para componentes interactivos. Frameworks MPA como Remix añaden complejidad innecesaria para un sitio personal estático. Next.js/Nuxt/SvelteKit SSR would have required a backend. Hugo/Jekyll are less flexible for interactive components. MPA frameworks like Remix add unnecessary complexity for a static personal site.
TypeScript estricto sin `any` en todo el código Strict TypeScript with no `any` throughout the codebase
Cierra la puerta a type-unsafety silenciosa y a regresiones tipadas. Obliga a usar `unknown` + narrowing, genéricos y tipos derivados de Zod. Trade-off: más tiempo de escritura inicial, pero mayor confianza en refactors futuros. Closes the door to silent type unsafety and typed regressions. Forces the use of `unknown` + narrowing, generics, and Zod-derived types. Trade-off: more upfront writing time, but higher confidence in future refactors.
Alternativas consideradas Alternatives considered
Permitir `any` aceleraba la escritura pero hacía invisibles los errores de tipo. `@ts-ignore` sin documentación es deuda técnica acumulada. La validación solo en runtime deja de ser verificable en compile time. Allowing `any` would speed up writing but hide type errors. Undocumented `@ts-ignore` is accumulated technical debt. Runtime-only validation is no longer verifiable at compile time.
TDD obligatorio con Vitest (opción B) Mandatory TDD with Vitest (option B)
Cubre lógica pura y helpers con tests unitarios; componentes con branching con render-tests vía `experimental_AstroContainer`; templates puros sin test (confianza vía paridad visual). Trade-off: ciclo más lento que TDD completo, pero sin overhead de tests sobre presentación pura. Covers pure logic and helpers with unit tests; components with branching via render-tests through `experimental_AstroContainer`; pure templates without tests (confidence via visual parity). Trade-off: slower cycle than full TDD, but no overhead of tests on pure presentation.
Sistema de tres temas (dark/light/paper) con CSS variables Three-theme system (dark/light/paper) with CSS variables
Preserva los tres temas del handoff sin recortes. Las CSS variables permiten cambio en runtime mediante el atributo data-theme aplicado al elemento raíz, sin cargar estilos alternativos. Trade-off: más tokens en el bundle (3× declaración), pero paridad visual completa y reutilización de componentes sin duplicado. Preserves all three handoff themes without cuts. CSS variables enable runtime switching via a data-theme attribute on the root element, without loading alternate stylesheets. Trade-off: more tokens in bundle (3× declaration), but full visual parity and component reuse without duplication.
Alternativas consideradas Alternatives considered
Dos temas (dark/light) simplificaban el sistema pero descartaban paper, que es identidad del handoff. Clases CSS por tema (.dark, .light, .paper) requerían cambios de clase y reload de estilos. CSS-in-JS hubiera acoplado lógica a componentes. Two themes (dark/light) would simplify the system but drop paper, which is part of the handoff identity. CSS classes per theme (.dark, .light, .paper) would have required class changes and stylesheet reloads. CSS-in-JS would have coupled logic to components.
Bilingüe sin routing por idioma (`i18nString` + `data-lang`) Bilingual without language routing (`i18nString` + `data-lang`)
Un único set de URLs sirve ambos idiomas: cada string bilingüe (frontmatter MDX o JSON) guarda `{ es, en }` validado por Zod, y el cambio de idioma muta el atributo data-lang del elemento raíz sin recargar. Trade-off: dos spans con atributo lang en el DOM por cada texto traducible, pero evita árbol de URLs duplicado, sitemap fragmentado y desincronización entre versiones. A single URL set serves both languages: every bilingual string (MDX frontmatter or JSON) stores `{ es, en }` validated by Zod, and language toggle mutates the data-lang attribute on the root element without reload. Trade-off: two lang-marked spans per translatable text in the DOM, but avoids duplicated URL trees, fragmented sitemaps, and version drift.
Alternativas consideradas Alternatives considered
El routing nativo i18n de Astro (`/es/...` + `/en/...`) fragmentaba el sitemap y forzaba mantener dos copias del árbol. Archivos paralelos por idioma (un `.mdx` para ES, otro para EN) duplicaban el frontmatter. Una sola lengua hubiera reducido la audiencia. Astro native i18n routing (`/es/...` + `/en/...`) fragmented the sitemap and forced maintaining two URL trees. Parallel files per language (one `.mdx` for ES, another for EN) duplicated frontmatter. A single language would have reduced audience reach.
TypeScript vanilla sin UI framework (Astro + script modules) Vanilla TypeScript with no UI framework (Astro + script modules)
Cero React/Vue/Svelte. La interactividad (theme toggle, lang toggle, piezas Lab, observers de scroll) vive en bloques de script dentro de archivos `.astro` que importan módulos TS desde `src/lib/`. Trade-off: hay que escribir manipulación DOM a mano, pero el payload JS es mínimo, no hay runtime de framework, y el comportamiento es directo de leer en el bundle. Zero React/Vue/Svelte. Interactivity (theme toggle, lang toggle, Lab pieces, scroll observers) lives in script blocks inside `.astro` files that import TS modules from `src/lib/`. Trade-off: hand-rolled DOM manipulation, but JS payload is minimal, no framework runtime, and behavior is directly readable in the bundle.
Alternativas consideradas Alternatives considered
Las integraciones Astro de React/Vue/Svelte hubieran añadido 30-50 KB de runtime para piezas que apenas mutan estado. Alpine/Stimulus/Web Components habrían introducido un mini-framework adicional para resolver lo que un módulo TS resuelve sin abstracciones. Astro React/Vue/Svelte integrations would have added 30-50 KB of runtime for pieces that barely mutate state. Alpine/Stimulus/Web Components would have introduced an additional mini-framework to solve what a plain TS module solves without abstractions.
Capa de contenido dual: collections MDX para narrativa, JSON+Zod para datos puntuales Dual content layer: MDX collections for narrative, JSON+Zod for structured data
MDX collections (experiencia, proyectos, OSS, side-projects) cuando el body largo es el valor: `getCollection()` + frontmatter tipado + render del cuerpo. JSON+Zod (hero, principios, decisions, etc.) cuando el shape es plano y se importa síncronamente al SSR. Trade-off: dos patrones que mantener, pero cada uno encaja con la naturaleza del dato: forzar uno solo introduciría fricción. MDX collections (experience, projects, OSS, side-projects) when the long body is the value: `getCollection()` + typed frontmatter + body render. JSON+Zod (hero, principles, decisions, etc.) when shape is flat and imports synchronously at SSR. Trade-off: two patterns to maintain, but each fits the shape of its data: forcing one would introduce friction.
Alternativas consideradas Alternatives considered
Solo collections para todo: `getCollection()` requiere async donde un import síncrono es trivial; un wrapper MDX para entries sin body es ceremonia inútil. Solo JSON: pierde body markdown rico (proyectos largos colapsan en concatenación de strings). La dualidad reconoce que ambos patrones encajan con shapes distintos. Collections-only: `getCollection()` requires async where a sync import is trivial; an MDX wrapper for body-less entries is needless ceremony. JSON-only: loses rich markdown bodies (long projects collapse into string concatenation). The duality acknowledges both patterns fit different shapes.
CSS plano con modules colocados (sin Tailwind/SASS/CSS-in-JS) Plain CSS with colocated modules (no Tailwind/SASS/CSS-in-JS)
Cada componente lleva su `.module.css` adyacente. Los tokens viven solo en `tokens.css`. Trade-off: sin abstracciones de utility (Tailwind), pero el payload CSS es mínimo (~2 KB), cambiar tokens es un one-liner, y la visibilidad del estilo es directa (sin indirección de nombres generados). Each component has its adjacent `.module.css`. Tokens live only in `tokens.css`. Trade-off: no utility abstractions (Tailwind), but CSS payload is minimal (~2 KB), changing tokens is a one-liner, and style visibility is direct (no generated-name indirection).
Alternativas consideradas Alternatives considered
Tailwind aceleraba el layout pero forzaba `class=` con listas largas y complicaba la customización del tema vía plugins PostCSS. SASS/LESS añaden transpilación. CSS-in-JS (styled-components, emotion) acoplaba estilos a JS y exigía hidratación. Tailwind would speed up layout but force long `class=` lists and complicate theme customization via PostCSS plugins. SASS/LESS add transpilation. CSS-in-JS (styled-components, emotion) would couple styles to JS and require hydration.
03 · QUÉ 03 · WHAT
Bases. Foundations.
Tipografía, espaciado y primitives: las piezas reutilizables sobre las que se construye el sitio. Typography, spacing and primitives: the reusable pieces the site is built on.
Tipografía Typography
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Body paragraph. Geist Sans con ss01 + cv11. The quick brown fox jumps over the lazy dog.
const tokens = ["bg", "fg", "accent"]; Espaciado y radios Spacing & radii
--radius
8px--radius-lg
14px--container
1180pxUI primitives UI primitives
Eyebrow
Lang
Tag
SectionHead
AgentsMcpIcon
AssistedDevIcon
DocumentIcon
EmailIcon
GithubIcon
LinkedinIcon
MoonIcon
MultiProviderIcon
PaperIcon
RagCitationIcon
RepoIcon
SparkleIcon
SunIcon
04 · TOKENS 04 · TOKENS
Tokens por theme. Tokens by theme.
Los 11 tokens canónicos redeclarados por theme. Cada columna muestra los valores reales aplicados. The 11 canonical tokens redeclared per theme. Each column shows the actual applied values.
Active theme: dark Cycle the theme with the button in the nav or by pressing T.
| Token | dark | light | paper |
|---|---|---|---|
| --bg | #0a0a0b | #fafaf8 | #f5f1e8 |
| --bg-elev | #111113 | #f3f3f0 | #ede7d6 |
| --bg-elev-2 | #18181b | #ebebe7 | #e3dcc6 |
| --fg | #fafafa | #0a0a0b | #1a1612 |
| --fg-dim | #a1a1aa | #52525b | #5c5447 |
| --fg-mute | #7d7d86 | #6e6e76 | #6f6757 |
| --line | #27272a | #d4d4d4 | #c8bfa5 |
| --line-soft | #1c1c1f | #e7e7e4 | #d8d0b8 |
| --accent | oklch(0.82 0.16 145) | oklch(0.48 0.18 145) | oklch(0.5 0.2 50) |
| --accent-dim | oklch(0.82 0.16 145 / 0.12) | oklch(0.48 0.18 145 / 0.1) | oklch(0.5 0.2 50 / 0.12) |
| --warn | oklch(0.78 0.15 60) | oklch(0.78 0.15 60) | oklch(0.78 0.15 60) |
05 · BUILD & RUNTIME 05 · BUILD & RUNTIME
Build & runtime. Build & runtime.
Métricas emitidas como endpoint JSON estático en cada build. Metrics emitted as a static JSON endpoint at every build.
- Build Build
- Cargando estado… Loading status…
- Desplegado Deployed
- Cargando estado… Loading status…
- Schema Schema
- Cargando estado… Loading status…
- Peso de página Page weight
- Cargando estado… Loading status…
- Peso JS JS payload
- Cargando estado… Loading status…
- Peso CSS CSS payload
- Cargando estado… Loading status…
- Rutas Routes
- Cargando estado… Loading status…