Hecho con IA. Dirigido por personas.

Las reglas del método

Escritas mientras construíamos la landing de LEXA y esta web, y publicadas tal como se escribieron. Los códigos entre paréntesis, como (5a), son los pasos de aquel trabajo, y algunos ejemplos, como el modo en que la landing renderiza en el servidor, describen ese proyecto y no esta web.

Ocho reglas de verificación, una de diseño y dos de entorno. Ninguna es teórica: cada una nació de un error real —las nueve primeras durante el porting de la v5, la 10 y la 11 en la web corporativa—, y todas se adoptaron después de que ese error produjera un daño o una conclusión falsa. Se anota el error de origen porque una regla sin su historia se salta a la primera prisa.

Quien vuelva a tocar esta landing hereda esto antes que el código.


1 · Una captura en blanco no es evidencia de página en blanco

will-change mantiene elementos en capas del compositor que captureVisibleTab no incluye. Una sección perfectamente pintada se fotografía vacía.

Antes de creerte una captura vacía: comprueba estilos computados y fuerza un repintado (el.style.willChange = 'auto' y un scroll de 1 px).

Origen (5a): reporté un bug inexistente en el hero. Lo cerró abrir el mockup original, que salía igual de vacío con opacity: 1 y filter: blur(0px).

2 · scroll-behavior: smooth cancela scrollIntoView()

Si le sigues un scrollBy, el desplazamiento suave se interrumpe y la sección nunca entra en viewport. Parece que el observer está roto.

Usa behavior: 'instant' en cualquier scroll de verificación.

Origen (5b): di por roto el IntersectionObserver de «How it teaches». Funcionaba.

3 · Mide la carga de una imagen después, no antes

naturalWidth leído antes de que termine la petición vale 0, y complete vale false.

Origen (5b): reporté que las imágenes del bento no cargaban. Cargaban, con 200.

4 · Un cero sin control no prueba nada

grep no interpreta \xe2\x82\xac como bytes: busca esa cadena literal, que nunca existe. Devuelve cero siempre, y ese cero se firma con toda la confianza del mundo.

Todo patrón no-ASCII se construye con printfEURO=$(printf '\xe2\x82\xac')y va acompañado de un control previo que demuestre que matchea algo conocido. El control queda en el log, no solo el resultado.

Origen (5d): iba a certificar «cero precios en la landing» con un patrón incapaz de encontrar nada.

5 · Las cadenas prohibidas se grepean sin distinguir mayúsculas, y ancladas

grep -i siempre. Y cada patrón con contexto suficiente: partner aparece dentro de Mediapartners-Google, /mes dentro de nextjs.org/docs/messages/, iOS dentro de usuarios y Ejercicios. Un patrón de un solo número no sirve para nada: 30 devolvió 867 coincidencias de código minificado.

Acota siempre la búsqueda a los ficheros que contienen la landing, no a todos los chunks que carga la página.

Origen (5e): «Seleccionado por Google for Startups» seguía publicado y solo apareció con -i. En minúsculas puras daba 0.

6 · Antes de llamar bug a algo visual, mide dos veces separadas en el tiempo

Tres de mis «bugs» eran mediciones tomadas mientras una transición seguía corriendo.

Origen (5e): las barras de benchmark, «a scaleX(0)», estaban a mitad de animación.

7 · Toda validación se hace en los dos idiomas, y con cambio en caliente

El SSR renderiza siempre en inglés. Si el usuario acaba en inglés no hay re-render, no hay remontaje y los fallos ligados al cambio de idioma no existen. Probar solo en el idioma por defecto no prueba nada sobre la mitad de los usuarios.

Origen (incidente de producción): la mitad de la página era invisible solo en español. Mis comprobaciones en inglés pasaban de verdad.

8 · Ninguna prueba puede tocar la clase que está probando

Añadir .in a mano para sortear la regla 1 fabricó exactamente el falso positivo que enmascaraba el bug del reveal durante todo el desarrollo.

Si el artefacto de captura impide medir, se mide con conteo y estilos computados:

document.querySelectorAll('.lx-rv').length   //  56
document.querySelectorAll('.lx-rv.in').length //  56  <- tienen que coincidir

Sin scroll y tras 8 s, que es lo que verifica el fallback duro.

Origen (incidente de producción): el bug más grave de toda la rama sobrevivió a decenas de comprobaciones por esto.


9 · No añadas una regla defensiva sin medir antes si el caso existe

Antes de cualquier @media, white-space, aspect-ratio o similar que no esté en el mockup: mide a 400 px y a 980 px y anota la cifra. Si no desborda, no se añade.

Origen (5b y 5c): dos desviaciones inventadas en dos incrementos seguidos, ambas resolviendo problemas que no existían. Al medirlas, una daba las mismas dimensiones que en escritorio y la otra ocupaba 110 px donde había 345.


10 · Un worktree que construye no enlaza node_modules

Con output: 'standalone', un build dentro de un worktree cuyo node_modules es un enlace al del repositorio deja .next/standalone/node_modules apuntando al directorio real. El siguiente next build en ese worktree limpia .next con recursive-delete, que desciende por los enlaces a directorios, y vacía el node_modules del repositorio principal.

En un worktree que construye, npm ci propio. Si ya existe un enlace, se deshace con unlink antes de construir o de borrar nada: find <worktree> -type l -path '*node_modules*'.

Origen (corporativa, worktree de Clash Display): a las 03:23 un segundo build vació el node_modules del repositorio y el siguiente falló con Cannot find module …/jest-worker/processChild.js. Se restauró con npm ci desde el lockfile sin cambios.

11 · Al parar en un punto de espera, ningún proceso propio queda vivo

Un servidor de preview o un navegador headless solo vive mientras se mide contra él. Antes de detenerse a esperar una decisión se paran todos, y se comprueba: ps sin next-server, node server.js ni Chromium propios, y ss -ltnp sin sus puertos. pgrep -f cuenta también la shell que lo ejecuta, porque su línea de comandos contiene el patrón: se filtra por el ejecutable, no por la línea completa.

Origen (corporativa, espera del GO): dos servidores de preview (:9200 y :9301) siguieron levantados más de dos horas mientras se esperaba el GO, y quedaban dos next-server de una sesión anterior con cinco horas de vida. Ramón lo vio como «dos procesos colgados». Al comprobar la limpieza, pgrep -fc next-server devolvió 1: era la propia shell.


Lo que estas reglas encontraron

Tres hallazgos de severidad «bloquea», los tres camino de producción:

  1. Sin JavaScript la página quedaba prácticamente en blanco. Todo .lx-rv arrancaba en opacity: 0 y solo JS lo revelaba.
  2. El build de Docker fallaba por una dependencia opcional de firebase-admin que npm ci omitía en silencio sobre Alpine, mientras el build local pasaba.
  3. El reveal no llegaba a media página en español, por key derivadas del texto traducido. Este llegó a producción y duró menos de una hora.

Y dos que un usuario habría notado antes que nadie: la especificidad CSS que dejaba las barras de benchmark sin animar, y el <html lang> que no seguía al idioma seleccionado.