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 printf — EURO=$(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:
- Sin JavaScript la página quedaba prácticamente en blanco. Todo
.lx-rvarrancaba enopacity: 0y solo JS lo revelaba. - El build de Docker fallaba por una dependencia opcional de
firebase-adminquenpm ciomitía en silencio sobre Alpine, mientras el build local pasaba. - El reveal no llegaba a media página en español, por
keyderivadas 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.