La herramienta tenía razón. Ese era el problema.
Acababa de añadir una entrada de changelog al sitio de marketing de sh0 y de corregir una página de documentación. Antes de hacer push, ejecuté el verificador responsive que mantenemos exactamente para esto: un pequeño script que controla un Chrome headless, mide el desbordamiento horizontal a 390, 768 y 1280 píxeles, y sale con código distinto de cero si alguna página se desplaza lateralmente. Informó de un único fallo:
[DÉBORDE] /fr/docs/databases/backups-restore @390px
→ overflow=130px · coupable=a.flex.items-center [right=520]Preciso. Reproducible. Con el elemento culpable nombrado (la herramienta habla francés; coupable es «culpable»). Todo lo que un buen mensaje de error debe ser, y invitaba exactamente al arreglo equivocado.
La reparación de cinco minutos que no hice
La lectura obvia: algún enlace del pie de página es demasiado ancho a la anchura de un teléfono. Probablemente la navegación anterior/siguiente, donde una etiqueta francesa más larga se niega a partirse. Añadir min-w-0, añadir truncate, relanzar, verde, push. Cinco minutos.
Dos cosas me detuvieron. La versión inglesa de la misma página medía cero. Y nuestra propia regla de la casa, escrita tras un incidente anterior, dice que hay que mirar las capturas en lugar de fiarse de la cifra.
La captura medía 520 píxeles de ancho. La ventana medía 390.
Eso no es una página con un elemento demasiado ancho dentro. Es una página sin hoja de estilos. Chrome había maquetado HTML crudo a su anchura natural, y el «desbordamiento» que informaba la herramienta era la diferencia entre un documento sin estilos y la ventana que yo había pedido. La medición era exacta al píxel. Lo que medía no era un defecto de maquetación: era su sombra.
Qué estaba realmente roto
SvelteKit emite los enlaces a los assets de forma relativa a la profundidad de la propia página. La página inglesa vive en /docs/databases/backups-restore, tres segmentos de profundidad, así que su enlace a la hoja de estilos es:
html<link href="../../_app/immutable/assets/0.B9O4bhzJ.css">Desde /docs/databases/, ../../ aterriza en la raíz. Correcto.
Ese mismo archivo prerrenderizado se estaba sirviendo en /fr/docs/databases/backups-restore, cuatro segmentos de profundidad. Ahora ../../ aterriza en /fr/, el navegador pide /fr/_app/immutable/assets/0.B9O4bhzJ.css, recibe un 404 y renderiza el documento desnudo. Lo mismo con cada etiqueta de script. La página estaba además, por supuesto, en inglés: era el archivo inglés.
Dos llamadas a curl lo confirmaron, y una de ellas es la historia completa en una sola línea:
$ curl -s https://sh0.dev/fr/docs/databases/backups-restore | grep -o '<html lang="[a-z]*"'
<html lang="en"Por qué el archivo francés no existía
El sitio se prerrenderiza. Las páginas de marketing tienen entradas explícitas en svelte.config.js: un array mantenido a mano de 57 rutas, expandido sobre cuatro locales, porque esas páginas están detrás de menús desplegables al pasar el cursor que el crawler de prerrenderizado no sabe seguir. Las páginas de documentación nunca se añadieron a ese array. Se dejaron al crawler.
El crawler sigue los enlaces que aparecen en el HTML renderizado. /fr/docs enlaza once subpáginas. Siguiéndolas, y lo que ellas enlazan a su vez, se llega a veinticinco. Las veintitrés restantes solo son accesibles a través de una barra lateral que el rastreo nunca despliega. Así que veintiséis páginas de documentación en francés estaban prerrenderizadas (el índice más las veinticinco alcanzables desde él), y veintitrés no. Para esas veintitrés, la reescritura que retira el locale encontraba la ruta prerrenderizada en inglés, decidía que tenía un archivo que servir, y lo servía.
Veintitrés páginas, cuatro locales. Comprobé las 196 URLs de documentación localizadas en producción:
checked=196
mismatch=92Noventa y dos URLs, en vivo, sirviendo inglés sin estilos. No era una regresión de mi cambio: llevaba en producción desde que la documentación estaba traducida.
El arreglo son dos líneas de spread y la lista que cubren: un array docsPages con las cuarenta y nueve rutas, construido exactamente como ya lo estaba el array pages de cincuenta y siete entradas, expandido sobre los mismos cuatro locales. Veintiocho líneas de configuración, de las cuales veintiséis son los nombres de las rutas. El mecanismo existía desde siempre. Nadie lo había extendido al segundo directorio.
Dos pistas falsas, y la prueba que cuesta treinta segundos
El 500. En local, esa misma ruta devolvía un 500 con TypeError: fetch failed. Tentador de perseguir: es ruidoso, está justo en la página sospechosa. Era un artefacto del servidor de preview: adapter-node arrancado sin ORIGIN, y el fetch que SvelteKit se hace a sí mismo para una ruta prerrenderizada reescrita no tiene adónde ir. Producción nunca lo tuvo. Definir ORIGIN lo hizo desaparecer.
«Esto lo he roto yo». La pista más cara, porque lleva a revertir trabajo bueno. Yo había tocado esa página minutos antes. La prueba que lo zanja cabe en un comando: lanzar la misma verificación contra /fr/docs/databases/connection-strings, una página que nunca había abierto. Firma idéntica: 130 píxeles, el mismo culpable nombrado, el mismo right=520. Un defecto que se reproduce en un archivo que no has tocado es un defecto que has revelado, no uno que has introducido. Esa distinción vale treinta segundos siempre.
La parte que se generaliza
Es la segunda vez que una herramienta de medición informa en silencio de lo que no es en un sitio de ZeroSuite, y los dos fallos son la misma forma invertida.
El primero: una ejecución responsive autenticada cuya sesión expiró a mitad de la serie. Cada página posterior renderizaba la home pública de marketing en lugar de la página bajo prueba. Cero píxeles de desbordamiento, dieciséis renderizados, éxito. Las capturas no estaban en blanco: estaban llenas, eran atractivas y plausibles. Nada en el informe las distinguía de un éxito real. Lo corregimos haciendo que la herramienta falle cuando falta un selector aseverado: no haber medido nada debe fallar, no tener éxito.
El segundo es este. Una cifra roja, correcta en su aritmética, apuntando a un elemento inocente.
Ambos casos tienen la misma raíz: una herramienta informa de lo que midió, y lo que midió no es necesariamente lo que crees haber pedido. El verde no significa que aquello que te importa esté bien. El rojo no significa que aquello que nombra sea culpable. La cifra es un puntero hacia el artefacto, y es en el artefacto donde vive la respuesta.
Si hubiera arreglado el enlace, el verificador se habría puesto verde. La página habría seguido sin estilos, seguiría en inglés, y noventa y dos URLs seguirían rotas, ahora con una prueba en verde montando guardia sobre ellas.
Qué se publicó
Un array y dos líneas de spread. Los cinco locales prerrenderizan ahora las cuarenta y nueve páginas de documentación; el francés tenía veintiséis. Vuelto a medir en producción tras el despliegue: 196 URLs comprobadas, cero discrepancias. La suite responsive pasa en local a 390 y 768 píxeles sobre siete rutas en tres idiomas, y de nuevo en producción a 390 sobre cuatro locales, y las capturas, que miré, miden 390 píxeles de ancho.
El desbordamiento ha desaparecido. Nunca fue el fallo.