Español · English
Full-stack engineer building with AI agents. Portafolio y Lab Notes —by an eternal junior—: un blog que documenta las decisiones detrás de lo que construyo, no tutoriales genéricos.
Es mi portafolio (/me) y mi blog (/notes) en un solo sitio, y el primer proyecto de la marca elvinlab. Cada nota cuenta una decisión concreta: el problema, qué probé, qué descarté y por qué, con números. Está construido para ser rápido, accesible, honesto y fácil de operar, y para que otra persona pueda adoptarlo cambiando solo configuración y contenido.
Además aloja @elvinlab/core, la base de diseño (tokens, temas e i18n). Por ahora no crece ni sale de este repositorio: ver ADR 0019.
Estas ideas ya están decididas y se repiten en el código, los tests y los documentos:
- Un blog de decisiones, no de tutoriales. Cada nota es una decisión: contexto, qué elegí y qué resultó, con números y nombres en lugar de adjetivos.
- Nace en el proyecto, se mueve a
corecuando se repite. No se diseña la base compartida por adelantado.corees solo presentación (tokens, temas, i18n): nunca hacefetchni guarda datos. - Medido, no prometido. Presupuestos de rendimiento y accesibilidad que rompen el CI si no se cumplen.
- Accesible y respetuoso. WCAG AA, objetivos táctiles de 44 px, respeta
prefers-reduced-motion, tipografías autoalojadas, el sitio no establece cookies propias y el idioma del navegador nunca redirige. - Honesto. Solo afirma lo que puede verificar (también en privacidad), no anuncia proyectos que no existen, no enlaza repositorios privados y nunca publica mi correo.
- White-label por diseño. Otra persona lo adopta solo con configuración y contenido; el estilo (layout, movimiento, motivos) queda en el código. Un test construye el sitio con otra identidad y falla si se filtra algo mío.
- Español por defecto, inglés opcional. Una nota, un idioma. La interfaz está en ambos.
- Simple de operar. Un solo entorno, un push a
maines un release y el CI corre una vez antes de desplegar.
Este sitio se construyó con agentic-dev-setup, mi entorno de desarrollo multi-agente de código abierto (MIT): Claude Code piensa y revisa, y modelos más baratos teclean. El trabajo acotado se delega a agentes de OpenCode a través de herdr, OmniRoute los enruta a modelos gratuitos con qwen3:14b local como último escalón, y Engram da memoria compartida a todos.
flowchart LR
U([Yo]) --> C[Claude Code<br/>nivel 3 · revisa cada diff]
C -- delega vía herdr --> O[OpenCode<br/>niveles 1 y 2]
O --> R{{OmniRoute}}
R --> M[Modelos gratuitos<br/>Groq · NVIDIA · Mistral · Gemini]
R --> L[(qwen3:14b local)]
C <--> E[(Engram<br/>memoria compartida)]
O <--> E
Así se trabaja aquí: cada tarea es un issue escrito como un brief de delegación con un nivel (1 trivial, 2 acotado, 3 complejo); el desarrollo es con TDD estricto; y un modelo más caro revisa cada diff que escribió uno más barato. La historia completa está en la nota Claude piensa, los modelos baratos teclean.
Hoy: el desarrollo lo lleva Claude Code como orquestador, con el ecosistema gentle-ai para el ciclo de revisión y verificación, y delega el trabajo acotado a OpenCode como en el diagrama. pi (el Gentleman) y Codex quedan como alternativas. La arquitectura, los presupuestos de calidad y las convenciones del repo no dependen de la herramienta.
| Capa | Tecnología | Para qué se usa |
|---|---|---|
| Framework | Astro 7 + MDX | Páginas prerenderizadas, contenido en MDX e islas solo donde hacen falta |
| Interfaz | Tailwind CSS 4, Preact 11, TypeScript 6 (estricto) | Estilos con tokens semánticos; una sola isla (el formulario de contacto) |
| Contenido | Content Collections con Zod 4, Expressive Code | Notas MDX y JSON validados al compilar; bloques de código con marcos y copiar |
| SEO y compartir | Sitemap, RSS, JSON-LD, Sharp, Satori | Metadatos, imágenes optimizadas y una tarjeta al compartir por nota |
| Tipografía y gráficos | IBM Plex Sans, JetBrains Mono, Pixelify Sans, Press Start 2P (Fontsource) y WebGL2 propio | Fuentes autoalojadas y fondos animados del banner, sin librerías |
| Hosting | Cloudflare Workers + Wrangler | Un solo entorno; dominio y DNS en Cloudflare |
| Servicios | Turnstile, Resend, Web Analytics, Giscus | Anti-bot, correo del formulario, analítica mínima y comentarios en GitHub Discussions |
| Calidad | Vitest 5, Playwright + axe-core, Lighthouse CI, Biome, dependency-cruiser | Tests, accesibilidad, rendimiento, formato y fronteras de arquitectura |
| Herramientas | pnpm 12 (workspaces), mise (Node 24), GitHub Actions, Dependabot | Monorepo, versiones fijas, CI/CD y actualizaciones semanales |
| Desarrollo asistido | agentic-dev-setup: Claude Code, herdr, OpenCode, OmniRoute, Ollama y Engram → hoy: Claude Code + gentle-ai (pi y Codex opcionales) | Cómo se construyó (sección anterior) |
- Lab Notes: índice por año con filtro, índice lateral con progreso, registro de decisión, tiempo de lectura, notas relacionadas, anterior/siguiente y RSS.
- «Lo que más se mueve»: en el índice de
/notes/, las notas más activas según huellas, comentarios y lecturas de los últimos 30 días; se actualiza conpnpm pulse:snapshotantes de un release. - Modo lectura opcional en cada nota: una sola columna, sin banner animado ni barra lateral, para leer cómodo en móvil y escritorio.
- Comentarios y reacciones con Giscus, cargados solo al llegar a ellos.
- Tarjetas al compartir propias para cada nota, cada case page de experimento y
/me, generadas al compilar. - Páginas de experimento (
/experiments/<slug>/, ES/EN): el problema, mi parte, la galería, el resultado y, al final, la nota relacionada; cada sección aparece solo si tiene contenido. /me: portafolio con experiencia, formación y experimentos; se imprime limpio a PDF.- Formulario de contacto seguro: Turnstile, límite de envíos y falla cerrado si falta configuración.
- Changelog público, páginas de privacidad y términos y tema claro/oscuro con fondos animados elegibles.
- Aspecto configurable: el preset
appearance(minimalofull) fija la escala tipográfica, la altura de los banners y el detalle de las notas, yhomeactiva o apaga cada sección de la portada. - Suscripción por correo: quien lee recibe las notas nuevas (y, de vez en cuando, un aviso de un proyecto) con doble confirmación y baja en una página; el envío lo disparo yo a mano. Guía:
docs/SUBSCRIPTION.md. - Una tarjeta del último experimento: fija después de cada nota y en la home, con una variante flotante opcional (
nudge.floating, apagada en este sitio); al cerrarla se esconde un tiempo (nudge.reappearAfterDays). - Un botón anónimo de huella ("estuve aquí") en las notas, y pequeños detalles de interfaz: volver arriba, un aviso para cambiar de idioma, un botón de tema y otro que cambia el efecto del banner.
- Un aspecto de cuaderno (tinta dibujada a mano, sellos, un error tachado con su corrección) y detalles pixelados.
- Bilingüe (ES/EN) sin redirecciones por idioma.
- Instalable como app (manifest + íconos), sin service worker.
Lo que se hace a mano y se suele olvidar, con comandos para copiar y pegar en docs/HOW-TO.md:
| Quiero... | Receta |
|---|---|
| Escribir y publicar una nota | Receta 1 |
| Avisar por correo de una nota o de un proyecto nuevo | Receta 2 |
| Cambiar «Empieza aquí» | Receta 3 |
| Actualizar el ranking «más activas» (antes de cada release) | Receta 4 |
| Cambiar la tarjeta «Ahora» de la portada | Receta 5 |
| Agregar o editar un proyecto, sus imágenes y datos | Receta 6 |
| Actualizar CV, disponibilidad, experiencia y certificados | Receta 7 |
| Anotar el cambio en el changelog y liberar a producción | Recetas 8 y 9 |
Todo lo opcional es un interruptor en site.config.ts, dentro de features. Apagado significa que no se envía nada: ni ruta, ni entrada de menú, ni script ni CSS. Un valor inválido rompe el build y nombra el problema.
Interruptor (features.) |
Qué agrega | Necesita |
|---|---|---|
blog |
Lab Notes: índice, notas y RSS | |
comments |
Comentarios y reacciones con Giscus en las notas | el bloque giscus |
contact |
El formulario /contact |
secretos de Turnstile y de correo (falla cerrado sin ellos) |
me |
La página de portafolio /me |
|
credentials |
Certificados y títulos en /me |
|
education |
Página /education/ y bloque «Formación reciente» de /me |
|
experiments |
/experiments y sus secciones en la home y en /me |
|
experimentNudge |
La tarjeta del último experimento (apagada por defecto) | experiments |
changelog |
El /changelog público |
|
readingMode |
El modo lectura en las notas | |
readingProgress |
Recuerda dónde te quedaste en una nota y marca las leídas | |
marks |
El botón de huella "estuve aquí" | una base D1 y un limitador de envíos |
subscribe |
Suscripción por correo con doble confirmación | blog, D1, secretos de Turnstile y de correo |
backToTop, languageHint, themeToggle, backgroundPicker |
Detalles de interfaz (encendidos por defecto) |
Más allá de los interruptores:
- Secciones de la home (
home): pills del hero, tarjeta de autor, tarjeta de contratación, huellas, tarjeta "Now", pilares, índice del cuaderno y experimentos, cada una encendida o apagada. El presetappearance(minimalofull) fija el punto de partida. - Colores, radios y tipografías:
packages/core/src/tokens/tokens.json. Efecto del banner:background. Aviso del sitio:notice. - Textos: tu nombre, bio y enlaces en
site.config.ts; las notas y las entradas del portafolio enapps/web/src/content/; los textos de interfaz (ES/EN) enapps/web/src/shared/i18n/index.ts; privacidad y términos en suscontent.ts. - Longitud de los textos: cada texto de la interfaz y 26 campos visibles de
site.config.tstienen una longitud máxima, en español e inglés, comprobada por tests, así que un texto más largo falla con su nombre en vez de desbordar. Tabla endocs/CONFIGURATION.md.
Estos interruptores y secciones de la home son todo lo configurable hoy; el resto del diseño es código. La referencia completa, con cada clave y su valor por defecto, está en docs/CONFIGURATION.md.
Todo esto lo verifica el CI; si algo falla, no hay despliegue:
- Lighthouse móvil ≥ 95 en las cuatro categorías, CLS ≤ 0,1 y LCP ≤ 2,7 s (pruebas).
- JavaScript ≤ 30 KiB (gzip) por página.
- Accesibilidad con axe (WCAG 2 A, AA y 2.1 AA) en tema claro y oscuro y en 360, 768 y 1280 px.
- Más de 1800 tests unitarios y más de 500 pruebas e2e en navegador.
- Un test comprueba que el sitio no establece cookies propias y otro que nada del dueño se filtra en una compilación con otra identidad.
Necesitas mise (instala las versiones fijas de Node y pnpm).
git clone https://github.com/elvinlab/elvinlab.dev.git
cd elvinlab.dev
mise install # Node 24 y pnpm 12
mise exec -- pnpm install # dependencias
mise exec -- pnpm exec playwright install chromium # una vez, para los tests de navegador
mise exec -- pnpm --filter web dev # http://localhost:4321El formulario de contacto necesita secretos de Cloudflare; sin ellos muestra el estado «no disponible». En la guía de configuración está cómo probarlo en local con las claves de prueba.
| Comando | Qué hace |
|---|---|
pnpm --filter web dev |
Servidor de desarrollo (incluye los borradores de notas) |
pnpm --filter web build |
Compilación de producción en apps/web/dist |
pnpm test |
Tests unitarios |
pnpm test:e2e |
Tests de navegador: smoke, accesibilidad, tema, comentarios, modo lectura |
pnpm typecheck · pnpm lint · pnpm depcruise |
Tipos, formato y lint, fronteras de arquitectura |
pnpm check:js-budget · pnpm test:lighthouse |
Presupuestos de JavaScript y de Lighthouse |
pnpm test:white-label |
Compila con otra identidad y busca filtraciones del dueño |
pnpm new-post "Título" |
Crea el borrador de una nota |
pnpm publish-note <slug> |
Publica un borrador de punta a punta (mueve, fecha, changelog, build, commit y release; --dry-run, --no-release) |
pnpm verify |
Plan de verificación por alcance para los cambios sin commitear (--run lo ejecuta) |
pnpm pulse:snapshot |
Actualiza la lista «Lo que más se mueve» desde huellas y lecturas (solo lectura; necesita sesión de wrangler y CLOUDFLARE_ANALYTICS_TOKEN) |
pnpm docs:config |
Regenera las tablas de la guía de configuración |
Todos se ejecutan con mise exec -- pnpm ... para usar las versiones fijas.
apps/web/ → el sitio (Astro): features/ por funcionalidad, pages/, shared/ y content/
packages/core/ → @elvinlab/core: tokens, temas e i18n (solo presentación)
docs/ → guías, marca, diseño, plan y ADRs
tests/ → tests e2e (Playwright) y notas de prueba aisladas
.github/ → CI, Dependabot y plantilla de tareas
odd/tasks/ → seguimiento de cada funcionalidad
| Documento | Qué cuenta |
|---|---|
| Guía de configuración y actualización | Dónde se configura cada cosa y cómo se actualiza todo: ajustes, contenido, secretos, dependencias y releases |
| Tareas frecuentes | Recetas paso a paso: nota, aviso por correo, «Empieza aquí», ranking, «Ahora», proyectos, CV, changelog y release |
| Cómo crear una nota | Receta rápida de 6 comandos y, debajo, el paso a paso detallado del borrador a producción |
docs/PORTFOLIO.md |
Editar /me y agregar un experimento (también en inglés) |
docs/SUBSCRIPTION.md |
Suscripción por correo: cómo funciona, datos y privacidad, límites, cómo enviar una nota y problemas comunes (también en inglés) |
docs/PLAN.md |
Visión, alcance y reglas (en español) |
docs/BRAND.md |
Narrativa, voz y tokens de marca (en español) |
docs/DESIGN.md |
Dirección visual, páginas y accesibilidad |
docs/CONVENTIONS.md |
Arquitectura, código, estilos y flujo de git |
docs/TESTING.md |
Tests de navegador y presupuestos de calidad |
docs/adr/ |
Decisiones de arquitectura, una por documento |
Se trabaja en develop con push libre. El release es un push a main, que dispara el CI una sola vez (static, e2e y lighthouse → checks → deploy) y despliega a producción con una comprobación de humo y rollback automático. Una nota nueva no necesita ese recorrido completo: pnpm publish-note <slug> publica y el CI usa un carril rápido (static → notes-gate → deploy, unos 2 a 3 minutos; ADR 0016). Los pasos exactos, cómo revertir y qué revisar después están en la guía de configuración.
El sitio está pensado para que otra persona lo adopte reemplazando site.config.ts, el contenido y las imágenes de public/. La receta completa, con la lista de todo lo que sigue siendo del autor (textos de i18n, manifest, wrangler, CI) y su verificación, está en la guía.
El CI sigue tu configuración. Apagar funciones (o cambiar un nombre, un texto o la paleta) no exige editar pruebas: las pruebas de navegador leen qué está encendido y se saltan lo que está apagado. Cuatro escenarios de adopción (quiet, blog-only, me-only, bare) corren toda la suite con pnpm test:profiles, y un trabajo del CI los corre sin bloquear el release. Los textos tienen límites de longitud en ambos idiomas para que no desborden. Detalle en la guía y en TESTING.md.
En producción en elvinlab.dev desde el 1 de octubre de 2026. El avance está en el GitHub Project y el historial de cambios visibles, en el changelog.
El código se publica bajo la licencia MIT. Las notas (el contenido de content/notes) se publican bajo CC BY-NC-SA 4.0.
Elvin González · Costa Rica · elvinlab.dev · GitHub · LinkedIn · X


