Cómo crear una extensión de Chrome con Modern Web Guidance
Quería un color picker. Uno de esos que pinchas en la barra de Chrome, señalas cualquier píxel de la pantalla y te copia el valor al portapapeles.
Podría haberlo pedido a pelo. “Hazme una extensión de Chrome que sea un cuentagotas.”
Lo que salga, saldrá.
En su lugar hice otra cosa: instalé las skills oficiales del equipo de Chrome en el proyecto y dejé que fueran ellas las que dictaran cómo se escribe ese código. El resultado está publicado: modern-web-guidance-picker, con un docs/journal.md dentro donde queda registrada cada decisión.
En este post te cuento el proceso completo:
- Qué se instala exactamente cuando ejecutas el instalador (y qué se queda fuera si no lo pides).
- El paso obligatorio que la skill impone antes de escribir la primera línea.
- Los fragmentos concretos donde el código cambió respecto a lo que habría salido por defecto.
- Los tres problemas que aparecieron y cómo se resolvieron.
- Cómo replicarlo en tu proyecto en menos de cinco minutos.
Las skills le dictan a tu agente el cómo. SDD le dicta el qué
Si prefieres no dejar nada al azar antes de que el agente escriba, aquí recorres el ciclo completo con OpenSpec sobre un proyecto de juguete: proposal, spec, diseño, tareas, aplicar y archivar.
Entra en el curso gratis →Qué es Modern Web Guidance en dos párrafos ¶
Si nunca lo has tocado, Modern Web Guidance es el paquete de skills que mantiene el equipo de Chrome para que tu agente de IA deje de escribir la web de 2019. La idea de fondo: los pesos de entrenamiento de cualquier modelo contienen patrones obsoletos, y la plataforma web se mueve más rápido de lo que se reentrena un LLM.
No es documentación que el agente “consulta si quiere”. Es una herramienta con dos comandos, search y retrieve, que devuelven guías cortas y calibradas directamente al contexto. El repositorio trae 140 guías repartidas en catorce categorías: css, accessibility, performance, forms, ui-behaviors, ui-components, visual-design, security, webmcp, built-in-ai y unas cuantas más.
Si te suena a chino todo esto de las skills, la guía completa de Agent Skills te pone al día del estándar, y en las diferencias entre skills, prompts, MCP y subagentes desmenuzo por qué una skill pesa cien tokens y un MCP se come media ventana de contexto.
Lo que se instaló de verdad en el proyecto ¶
Aquí está el primer detalle que casi se me escapa, y es el más importante de todo el artículo.
El instalador oficial se lanza así:
# Instalación local, en este proyecto y solo en este proyecto
npx modern-web-guidance@latest install
Detecta el agente (en mi caso Claude Code), decide que no está en modo interactivo y escribe en el proyecto por defecto. Perfecto. Pero solo instaló una de las dos skills que necesitaba.
El repositorio de Google trae dos paquetes distintos: modern-web-guidance (las 140 guías de plataforma web) y chrome-extensions, que es opt-in porque su contexto es muy específico. Cargar las reglas de Manifest V3 en un proyecto de React sería tirar tokens a la basura, así que Google la deja apagada.
Adivina cuál de las dos era imprescindible para construir una extensión de Chrome.
# La segunda skill hay que pedirla por su nombre
npx -y skills add https://github.com/GoogleChrome/modern-web-guidance \
--skill chrome-extensions
🔑 Si le pides a tu agente que instale “Modern Web Guidance” y te vas a por café, te va a faltar exactamente la skill que resuelve tu caso. Las opt-in no se instalan solas. Revisa siempre qué ha entrado antes de dejar que empiece a escribir código.
El árbol que quedó en el proyecto:
.agents/skills/modern-web-guidance/ ← los ficheros reales
.agents/skills/chrome-extensions/
.claude/skills/* ← symlinks que lee Claude Code
skills-lock.json ← lock con el hash del origen
Ese skills-lock.json merece un párrafo, porque me hizo cambiar de opinión a mitad del proyecto. Este es su contenido:
{
"version": 1,
"skills": {
"chrome-extensions": {
"source": "GoogleChrome/modern-web-guidance",
"sourceType": "github",
"skillPath": "skills/chrome-extensions/SKILL.md",
"computedHash": "1e8525bb49647008dcd3a6b7e5d3c3207345f086e7a64d7e6d5bcc67dbba7a71"
}
}
}
Mi primera reacción fue tratarlo como un package-lock.json: si el lock fija el origen, las skills no hacen falta en el repo. Son 165 ficheros y 1,4 MB de contenido de Google que no pintan nada en tu historial de git, ¿no?
Mira otra vez el JSON.
No hay commit. No hay tag. No hay versión. Solo un hash del contenido. Eso te permite detectar que lo que tienes difiere de lo que había, pero no te permite restaurar lo que se usó: si vuelves a ejecutar npx … install, te bajas HEAD. Es una huella dactilar, no un pin. Y un lockfile que no reproduce una versión no está haciendo el trabajo de un lockfile.
Así que la decisión se revirtió y las skills sí se versionan en el repo, con tres motivos:
- El peso era una excusa. 1,4 MB no es nada para un repositorio.
- La licencia lo permite. El origen es Apache-2.0, que autoriza redistribuir con atribución. El impedimento legal que yo suponía no existía.
- El journal cita reglas concretas de esas guías. Sin ellas versionadas, quien clone dentro de seis meses instala otra versión y esas citas dejan de poder comprobarse.
La versión exacta contra la que se construyó todo, 2026_05_16-c5e78707, está anotada en el README junto a la atribución de licencia. Porque el lock, ya lo hemos visto, no la guarda.
🔑 Un lockfile con hash de contenido y sin referencia de versión no reproduce nada. Si tu documentación cita el contenido de una dependencia externa, esa dependencia viaja contigo en el repo.
Efecto secundario agradable: al clonar modern-web-guidance-picker te llevas las 140 guías dentro. Puedes leerlas sin instalar nada.
Un apunte más, que a mí me hizo levantar la ceja: el instalador escribe en .agents/skills pensando en unos treinta agentes distintos (Cursor, Codex, Copilot…), y .claude/skills queda como symlinks relativos apuntando ahí. Es ruido inofensivo y tiene una ventaja: si mañana abres el proyecto con otro agente de los que soportan el estándar, las skills siguen ahí y siguen funcionando tras clonar.
Saber qué instala de verdad tu agente, qué se versiona y qué no es de las cosas que solo se aprenden usándolo. Cada domingo lo compartimos con +6.700 developers. Gratis desde 2018.
Suscríbete gratis →El paso que la skill te obliga a dar antes de escribir código ¶
Esta es la parte que diferencia una skill de un fichero de instrucciones que nadie lee.
El SKILL.md de modern-web-guidance lleva escrito en mayúsculas que su ejecución es obligatoria y previa a cualquier tarea de HTML, CSS o JavaScript de cliente. No es una sugerencia. El agente busca primero, recupera después y solo entonces escribe.
El flujo son dos comandos:
# 1. Buscar por lo que quieres conseguir, no por el nombre de la API
npx -y modern-web-guidance@latest search "show a toast notification confirming an action"
# 2. Recuperar la guía completa por su id (acepta varios separados por comas)
npx -y modern-web-guidance@latest retrieve "persistent-toast-notifications"
La búsqueda es semántica, se ejecuta en local con un modelo de TensorFlow.js y no hace ni una llamada de red. Devuelve JSON con el id, la descripción, la categoría, las features usadas y hasta el tokenCount de cada guía.
Estas fueron las consultas reales de la sesión y lo que devolvieron:
| Consulta | Guía recuperada |
|---|---|
| “pick a color from the screen with an eyedropper” | sin match directo — no hay guía de EyeDropper |
| “style modern interactive states for buttons and focus rings” | css |
| “show a toast notification confirming an action” | persistent-toast-notifications |
| “keyboard accessible list of items with delete action” | accessibility |
| “dark mode” | sección Dark mode de css |
Fíjate en la primera fila. No hay guía de EyeDropper, y la skill lo dice sin maquillaje en lugar de devolver el resultado menos malo. Eso es exactamente lo que quieres de una herramienta así: que te diga cuándo no tiene nada que aportar, para que tú vayas a la especificación y no te quedes con una respuesta aproximada.
Cuando la búsqueda no afina, hay una válvula de escape:
# Lista completa de las 140 guías con su categoría y su id
npx -y modern-web-guidance@latest list
Un apunte sobre el montaje: además de las dos skills, en la sesión estuvo conectado Chrome DevTools MCP. Sirvió el popup en localhost y lo abrió en un Chrome real, con chrome.storage.local sustituido por un stub en memoria. Eso permitió comprobar el CSS renderizado, las conversiones de color y el ciclo completo de copiado en lugar de darlos por buenos sobre el papel. Las skills dicen qué escribir. El MCP dice si lo escrito se comporta como esperabas.
Lo que cambió en el código de verdad ¶
Aquí es donde esto deja de ser teoría. Estos son los sitios concretos donde el CSS y el HTML salieron distintos a lo que habría escrito un agente sin las guías cargadas.
Tokens de color sin duplicar bloques de media query ¶
La guía de dark mode manda usar color-scheme y la función light-dark(). Nada de duplicar reglas dentro de @media (prefers-color-scheme: dark):
:root {
color-scheme: light dark;
/* Un token, dos valores. Sin media queries redundantes */
--ink: light-dark(#18181b, #f4f4f5);
--ink-muted: light-dark(#71717a, #a1a1aa);
--surface: light-dark(#ffffff, #131316);
--surface-raised: light-dark(#fafafa, #1d1d21);
--border: light-dark(#e4e4e7, #2e2e35);
}
El popup se ve bien en claro y en oscuro sin una sola regla repetida.
Capas de cascada en lugar de BEM ¶
La guía de css prohíbe explícitamente meter BEM para gestionar especificidad. Manda declarar el orden de las capas por delante y usar :where() para bajar el peso de los selectores.
Y hay una regla que me sorprendió, porque va contra lo que llevo años escribiendo: nada de resets globales sobre *. El motivo es sólido: un reset en * no se puede sobrescribir desde una capa inferior ni desde un web component sin recurrir a !important.
@layer reset, base, components;
@layer reset {
/* Sin reset en `*`: solo los elementos que este popup usa de verdad */
:where(body, h1, h2, p, ul, li, figure) {
margin: 0;
padding: 0;
}
:where(button) {
font: inherit;
color: inherit;
background: none;
border: 0;
cursor: pointer;
}
}
Objetivos táctiles con min-block-size, no con height ¶
La guía cita el criterio 2.5.8 de WCAG: 24×24 píxeles CSS como mínimo para cualquier elemento interactivo. Pero añade el matiz que marca la diferencia: hay que forzarlo con min-block-size y no con height, para que el contenido pueda hacer crecer el objetivo pero nunca encogerlo.
.pick {
/* min-block-size, no height: el contenido puede hacerlo crecer */
min-block-size: 2.5rem;
padding-inline: 0.75rem;
border-radius: var(--radius);
}
/* Un solo tratamiento de foco para todo. `outline`, no `box-shadow`,
porque box-shadow desaparece en modo de alto contraste */
:where(button):focus-visible {
outline: 2px solid var(--ink);
outline-offset: 2px;
}
El caso literal del color picker en la guía de accesibilidad ¶
Este me pareció una casualidad demasiado bonita. La guía de css tiene una línea sobre el modo de alto contraste de Windows que dice, palabra por palabra, que uses forced-color-adjust: none donde el color sea información esencial, y pone dos ejemplos: un resaltador de sintaxis y la muestra de un color picker.
Justo lo que estaba construyendo.
.swatch {
block-size: 4.5rem;
border: 1px solid var(--border-strong);
/* La muestra ES la información: en alto contraste no se puede repintar */
forced-color-adjust: none;
}
Y viene con su contrapartida, que es igual de importante: la misma guía prohíbe usar forced-color-adjust: none solo por preservar la estética. En el resto de la interfaz no aparece ni una vez. Lo que sí aparece es un bloque que devuelve los bordes que ese modo se lleva por delante:
/* En alto contraste se eliminan box-shadow y background-image.
Todo lo que comunicaba estructura necesita borde u outline */
@media (forced-colors: active) {
.pick { border: 1px solid ButtonText; }
.formats, .format { border-color: CanvasText; }
.swatch, .recent-swatch { border-color: CanvasText; }
}
El toast, con popover="manual" obligatorio ¶
La guía persistent-toast-notifications es tajante: un toast debe usar popover="manual" porque ese estado carece de light-dismiss. Si usas popover="auto", el siguiente clic del usuario se lleva por delante la confirmación que acabas de mostrar.
<div id="toast" popover="manual" role="status" class="toast"></div>
.toast {
opacity: 0;
translate: 0 0.5rem;
transition:
opacity 0.18s var(--ease),
translate 0.18s var(--ease),
overlay 0.18s allow-discrete,
display 0.18s allow-discrete;
}
/* :is() para que un navegador sin :popover-open no descarte la regla entera */
.toast:is(:popover-open) {
opacity: 1;
translate: 0 0;
}
@starting-style {
.toast:is(:popover-open) { opacity: 0; translate: 0 0.5rem; }
}
El role="list" que parece redundante y no lo es ¶
La guía de accesibilidad dice que no metas roles ARIA redundantes: nada de <nav role="navigation">. Y a continuación pone la excepción exacta que aplicaba aquí: Safari le quita la semántica de lista a un <ul> cuando le aplicas list-style: none o display: flex / grid. En ese caso el role="list" explícito es obligatorio para recuperarla.
La lista de colores recientes es una rejilla. Lleva su role="list".
💡 Ninguna de estas seis decisiones aparece en un prompt. Son cosas que sabes o no sabes. La skill convierte el “no lo sabía” en una línea de código correcta sin que tengas que pedirlo.
De usar skills a diseñarlas
Has visto lo que una skill ajena hace con tu código. Ahora escribe la tuya
Las de Google saben de plataforma web. Ninguna sabe cómo se llaman tus carpetas ni qué revisa tu equipo. Te llevas la anatomía del SKILL.md, la carga progresiva para no gastar contexto y el mismo archivo funcionando en 25+ agentes.
Destripar el método →Incluye el selector para elegir tu primera skill · Acceso con Web Reactiva Premium
Las veinte reglas de la skill chrome-extensions ¶
La segunda skill funciona distinto. No es un buscador semántico: es un SKILL.md con veinte reglas obligatorias que arrancan con una frase seca — incumplir cualquiera de ellas produce una extensión rota.
Las que tocaron este proyecto:
- Iconos: solo referencias ficheros que existan de verdad. Nada de apuntar el mismo PNG a los tres tamaños. Cada tamaño, su fichero, con sus dimensiones exactas. O los omites del manifest y Chrome pone el suyo.
- Siempre
async/await, nunca cadenas de.then(). Todas las APIschrome.*de MV3 devuelven promesas. - Nada de
eval,new Function()ni<script>en línea. La CSP de extensión los bloquea en todas las páginas. - Los service workers son efímeros: jamás guardes estado en variables.
chrome.actionexige la claveactionen el manifest, y el popup se cierra cuando pierde el foco.
Esa cuarta regla llevó a la decisión de arquitectura más limpia del proyecto: no hay service worker. No había nada que hacer en segundo plano, y no tenerlo elimina la clase entera de bugs de la que avisa la skill. Tampoco hay content scripts, ni página de opciones, ni host_permissions.
El manifest completo cabe en una pantalla:
{
"manifest_version": 3,
"name": "Picker",
"version": "1.0.0",
"description": "Pick any color on your screen and copy it as HEX, RGB, HSL or OKLCH.",
"author": "Dani Primo",
"homepage_url": "https://webreactiva.com",
"permissions": ["storage", "clipboardWrite"],
"action": {
"default_popup": "popup/popup.html",
"default_title": "Picker — pick a color",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
}
Dos permisos. Cero avisos de privacidad al instalar. El motivo es que EyeDropper es una API del navegador, no de la página: lee píxeles de toda la pantalla sin inyectar nada en ningún sitio. El enfoque clásico habría sido captureVisibleTab con un canvas y una lupa propia: cientos de líneas, permiso sobre todas las URLs y solo funcionaría dentro de la pestaña.
La decisión técnica que ahorró cuarenta líneas ¶
De los cuatro formatos que muestra el popup (HEX, RGB, HSL y OKLCH), el interesante es el último.
Convertir sRGB a OKLCH a mano son unas cuarenta líneas de matrices y constantes que es facilísimo teclear mal. Un fallo en un decimal y el color sale desviado sin que nada explote.
El navegador ya sabe hacerlo. Se llama relative color syntax:
// El navegador convierte por nosotros. Cero matemáticas a mano
probe.style.color = `oklch(from ${hex} l c h)`;
getComputedStyle(probe).color; // → "oklch(0.596 0.234 12.2)"
Funciona porque los espacios de color no legacy (oklch, lab, lch) se serializan en su propio espacio al calcularse.
Y aquí está el matiz que explica por qué HSL sí va a mano en el código:
// `hsl(from ...)` no serviría: HSL y RGB son el mismo espacio en CSS Color 4,
// y el valor calculado se serializa siempre como `rgb()`.
// El truco solo vale para espacios no legacy.
function toOklch(hex) {
probe.style.color = '';
probe.style.color = `oklch(from ${hex} l c h)`;
if (!probe.style.color) return null; // sintaxis no soportada en este navegador
// ...
}
Ese if no es paranoia decorativa. Si el navegador no acepta la sintaxis, la asignación al estilo se queda vacía, la función devuelve null y el formato no se pinta. Ni error en consola, ni valor inventado, ni fila con un guion misterioso. Simplemente no aparece. Es la degradación honesta que pide la guía.
La plataforma web se mueve más rápido de lo que da tiempo a leer, y por eso cada domingo seleccionamos 12 recursos sobre IA, herramientas y oficio de developer. Los suscriptores también aportan los suyos.
Suscríbete gratis →La trampa del popup, y cómo se esquiva con una línea ¶
Regla de la skill: el popup de una extensión se cierra al perder el foco. Está escrito en references/extensions/popup-ui.md y aplica siempre.
Ahora piensa qué significa eso en un cuentagotas. El usuario abre el popup, pulsa el botón, se abre el selector de color del sistema operativo, señala un píxel… y si el popup se ha muerto por el camino, el color elegido se ha perdido.
La mitigación cuesta reordenar dos bloques:
async function pickColor() {
const { sRGBHex } = await new EyeDropper().open();
// Persistir ANTES de tocar la UI: el popup se cierra al perder el foco,
// y un color que solo viviera en el DOM se perdería.
const { recent } = await readState();
const next = [sRGBHex, ...recent.filter((c) => c !== sRGBHex)].slice(0, MAX_RECENT);
await writeState({ current: sRGBHex, recent: next });
// Pintar va después
renderCurrent(sRGBHex);
renderRecent(next);
}
Si el popup muere, el color ya está en chrome.storage.local y aparece al reabrir. Una línea de reordenar contra un bug reproducible solo a veces.
⚠️ Este es el tipo de fallo que no sale en ningún test. Ocurre en la máquina de un usuario, con una versión concreta del sistema operativo, y llega como “a veces se me pierde el color”. Prevenirlo cuesta treinta segundos si sabes que existe.
Los problemas que aparecieron (y se resolvieron) ¶
Ninguna sesión de este tipo sale limpia a la primera. Estos tres se atascaron de verdad y los tres tienen una causa concreta que merece la pena que conozcas.
La carpeta que Chrome no cargaba ¶
Síntoma: cargar sin empaquetar y que no aparezca ninguna tarjeta en chrome://extensions. Sin error. Sin nada.
Causa: el manifest.json estaba en la raíz del proyecto, que es también donde viven .agents/, .claude/ con sus symlinks, docs/, tools/ y el skills-lock.json. La carpeta que cargas en Chrome debe contener solo la extensión.
Arreglo: mover todo a extension/. La estructura final separa lo que Chrome carga de lo que es andamiaje del proyecto:
extension/ ← esto y solo esto es lo que carga Chrome
manifest.json
popup/ popup.html · popup.css · popup.js
icons/ 16 / 48 / 128 px
tools/make-icons.py genera los tres PNG
docs/journal.md cada decisión, con su motivo
Como bonus, apareció una forma de validar el manifest sin instalar nada. El empaquetador de Chrome sigue funcionando y es igual de estricto:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--pack-extension="$PWD/extension" --no-message-box
Salida limpia significa que Chrome acepta el manifest y todos los ficheros que referencia. Genera un .crx y un .pem; borra el .pem acto seguido, que es una clave privada de firma.
El damero que tapaba el color elegido ¶
El estado vacío de la muestra era un conic-gradient de cuadros, el clásico patrón de transparencia. Al pintar el color con style.backgroundColor, el color salía a cuadritos.
La causa es de manual y la ves en cuanto la lees: background-image se pinta por encima de background-color. Siempre.
Se arregló quitando el damero, y por un motivo que va más allá del bug: EyeDropper nunca devuelve canal alfa, así que insinuar transparencia era además una mentira.
Este fallo no salió leyendo el código. Salió mirando una captura de pantalla del popup renderizado.
El hidePopover() que lanzaba excepción ¶
El temporizador del toast llamaba a hidePopover() a ciegas. Si el popover ya estaba cerrado, el método lanza excepción.
let toastTimer;
function toast(message) {
els.toast.textContent = message;
if (!els.toast.matches(':popover-open')) els.toast.showPopover();
clearTimeout(toastTimer);
// hidePopover() lanza excepción si ya está cerrado
toastTimer = setTimeout(() => {
if (els.toast.matches(':popover-open')) els.toast.hidePopover();
}, TOAST_MS);
}
Cómo replicar esto en tu proyecto ¶
El proceso completo, sin las cicatrices:
- Instala la skill base en el proyecto.
npx modern-web-guidance@latest install. Elige el ámbito de proyecto, no el global: elskills-lock.jsonqueda versionable. - Añade las skills opt-in que te toquen. Para extensiones,
npx -y skills add https://github.com/GoogleChrome/modern-web-guidance --skill chrome-extensions. Comprueba el lock después. - Pide explícitamente que use las guías. Algo tan simple como “usa las skills de modern-web-guidance para esto” activa el flujo de
searchantes de escribir. - Exige un journal. “Guarda el registro de todo lo que haces en un
docs/journal.mdapuntando todas las decisiones.” Es la instrucción con mejor relación coste-beneficio de todo el proceso. - Pide que verifique en un navegador real, no en teoría. Y que te diga qué no ha podido comprobar.
- Versiona las skills y deja fuera solo lo peligroso. En el
.gitignore,*.crxy*.pem. El.pemque genera el empaquetador es una clave privada de firma.
Ese cuarto punto merece un párrafo aparte. El journal no es documentación de cortesía: es donde quedan escritas las decisiones de recorte, que son las que nunca se documentan. Por qué no hay service worker. Por qué se descartó el side panel. Por qué se tiró el selector de formato con radios y label:has(:checked) que la propia guía de CSS recomienda, y se dejaron los cuatro valores a la vista con un botón de copiar cada uno.
Menos código, menos estado y un clic en lugar de dos.
Qué me llevo de todo esto ¶
Que la diferencia entre una extensión que funciona y una extensión bien construida no está en el prompt. Está en el conocimiento que el agente tiene cargado cuando empieza a escribir.
Sin las skills habría salido una extensión funcional. Con @media (prefers-color-scheme) duplicando bloques, con un reset en *, con outline: none en el foco, sin forced-color-adjust en la muestra de color, con el toast en popover="auto" cerrándose al primer clic y con cuarenta líneas de matrices sRGB a OKLab escritas a mano.
Nada de eso habría dado error. Habría funcionado. Peor, pero funcionando.
Y esa es justo la clase de deuda que no se ve hasta que alguien abre tu extensión en modo de alto contraste.
El código completo está en el repositorio, y dentro tienes docs/journal.md con cada decisión, incluidas las que se revirtieron. Clónalo, instala las dos skills con los comandos del README y pídele a tu agente que construya otra cosa encima.
¿Cuál va a ser tu extensión?
Fuentes ¶
- GoogleChrome, “modern-web-guidance” — repositorio oficial con las skills y las 140 guías
- Chrome for Developers, “Modern Web Guidance” — documentación oficial e instalación por agente
- Chrome for Developers, “Get started with Modern Web Guidance” — instrucciones específicas para cada agente
- Dani Primo, “modern-web-guidance-picker” — el código completo de la extensión
- Dani Primo, “Journal de decisiones del proyecto” — cada decisión con su motivo, incluidas las revertidas
- MDN, “EyeDropper API” — referencia de la API del cuentagotas
- ChromeDevTools, “chrome-devtools-mcp” — el servidor MCP usado para verificar el popup en un navegador real
🧨 Última oprtunidad para recibir la dinamita que mereces sobre programación con IA el próximo domingo: Suscríbete gratis a Web Reactiva en https://webreactiva.com/newsletter
12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.