En la primera parte conté cómo está montado Link Checker Online: rastreo en el navegador, dos motores de fetch y una extensión de Chrome. Aquí va lo que más se aprende, que es lo que falló.

«Restablecer resultados» no hacía nada… solo en producción

En local funcionaba. En producción, pulsar Restablecer resultados en modo Proxy no hacía nada: ni error ni reinicio.

Para reiniciar se pedía un JWT nuevo (correcto: el anterior ya tenía consumido su contador de 100 URLs), pero se reutilizaba el mismo token de Turnstile del primer rastreo. Cloudflare invalida un token tras su primer siteverify exitoso, así que el segundo canje se rechazaba con un 403… y un catch lo tragaba sin decir nada. En desarrollo no se veía porque la sitekey de prueba de Turnstile («siempre pasa») no invalida los tokens reutilizados.

Dos lecciones. Primera: un catch que se come el error es una bomba de relojería. Segunda: el entorno de pruebas debe parecerse al real en lo que importa. El arreglo fue una función que rearma el widget en segundo plano para obtener un token fresco y, si falla, un diálogo que lo explica en lugar de callar.

Reanudar tras una pausa larga «terminaba» el rastreo

Pausabas, volvías al rato, reanudabas y el rastreo se marcaba como finalizado al instante aunque quedaran URLs en cola.

El JWT dura un tiempo de reloj, no de «tiempo activo», así que tras una pausa larga caducaba. La primera petición recibía un 401 y el rastreo paraba (comportamiento correcto y documentado), pero la interfaz lo trataba igual que un final normal. El bug real no era la caducidad, sino no avisar. Añadí un diálogo de «sesión caducada».

Y decidí no añadir refresco de token: un rastreo normal termina de sobra dentro de la ventana de validez, y menos piezas móviles son menos superficie de ataque.

Rastreo en curso al 16 %, con el botón Pausar activo y 51 URLs todavía en cola (interfaz en inglés)

Pestaña «Todos» de los resultados, con el rastreo en curso.

Lista virtual en blanco hasta mover la rueda

Los resultados se pintan con una lista virtualizada (solo las filas visibles). A veces, al cambiar de pestaña interna, el panel aparecía en blanco hasta que movías el scroll.

La lista solo se repintaba cuando llegaban datos o saltaba un evento scroll. Si los datos llegaban mientras el panel estaba oculto (display: none), no había medidas reales y el repintado quedaba desfasado. Arreglo: forzar un refresh() justo al activar la pestaña.

Resultados con el panel de filtros abierto y varias pestañas internas (Todos, Ok, Rotos, Redirecciones…), la vista que dejaba de pintarse (interfaz en inglés)

Filtrado de resultados.

Caracteres � en páginas que no son UTF-8

Páginas antiguas en ISO-8859-1 o Windows-1252 mostraban rombos en las URLs y en los textos de ancla. Implementé la detección de codificación por BOM y cabecera Content-Type, con UTF-8 por defecto, en ambos motores.

Lo que no hice, a propósito, fue leer <meta charset> del HTML. El estándar exige un prescan de los primeros 1024 bytes que es una máquina de estados nada trivial, y habría que duplicarla en el Worker y en la extensión. Lo documenté como limitación conocida en lugar de convertirla en deuda oculta.

Decisiones de dejar algo sin arreglar

A veces la decisión correcta es no arreglar y escribir por qué. Tres ejemplos:

  • Un tope «blando». El límite de enlaces del modo Proxy es aproximado por diseño: hacerlo exacto añadía más complejidad y riesgo que beneficio. Está documentado, con una prueba que fija el margen.
  • Dominios inexistentes con etiqueta distinta según el motor. Un enlace a un dominio sin registrar sale como Timeout en la extensión (el fetch falla con un error de red genérico, indistinguible de un timeout) y como Roto en el Proxy. Ambos lo marcan como problema, pero «Timeout» es una etiqueta engañosa. Arreglarlo bien implica tocar la extensión, así que queda anotado para el próximo envío a la tienda.
  • Rutas de pruebas cerradas por defecto. Había un sitio de fixtures para probar el rastreador (429, redirecciones, etc.). El build lo excluía de producción, pero el Worker seguía atendiendo esas rutas. Lo cerré con una variable de entorno apagada por defecto: sin activarla, 404. «Cerrado por defecto» es mejor que «acuérdate de cerrarlo».

Rastrear sin techo asusta

Cuando la extensión permitió rastreos sin límite apareció otro problema: ¿qué pasa con un sitio de cientos de miles de URLs? Las búsquedas lineales se volvían cuadráticas y la memoria de la pestaña crecía sin freno.

Moví las deduplicaciones y búsquedas a estructuras indexadas (mapas y conjuntos en lugar de recorrer listas) y añadí un guardián de memoria que vigila el uso del heap y, si se mantiene alto de forma sostenida, detiene el rastreo con un aviso y permite exportar lo hecho hasta ese momento. Mejor un final controlado que una pestaña congelada.

Ventana de exportación de resultados en Excel, CSV o JSON, por categorías (interfaz en inglés)

Exportación de resultados y de apariciones de enlaces.

Lo que haría igual y lo que haría distinto

Igual:

  • Abstraer el motor de fetch desde el primer día.
  • Mantener el backend lo más fino posible.
  • Documentar las limitaciones aceptadas con su porqué.
  • Tener pruebas automáticas específicas para el contrato entre Proxy y extensión.

Distinto:

  • Diseñar desde el principio una categoría propia para «error de red o DNS» en lugar de colapsarla en Timeout.
  • Probar antes el flujo de Turnstile con una sitekey real: varios bugs solo salían en producción.
  • Pensar antes en el impacto de la revisión de la Web Store sobre lo que metía en la extensión.

Lo que viene

Está en el roadmap un rastreo reproducible por URL (compartir un enlace que lanza el mismo análisis con los mismos parámetros) y, más adelante, una evaluación SEO básica.

Si quieres probarlo, está en linkcheckeronline.com. Y si te ha servido este repaso, escríbeme: me interesa saber qué te llevarías a tu propio proyecto.