Pinchas un enlace a un periódico local, y carga el titular, luego la foto, luego los tres primeros párrafos, y ya has empezado a leer antes de que pase nada más. Entonces la página se queda en blanco. O se convierte en un montón de enlaces sin estilo con el logo estirado a lo ancho de toda la pantalla. O sube un recuadro que te pide que aceptes el rastreo o pagues £2,99 al mes, sin tercer botón.

El artículo estaba en tu máquina. El servidor del editor ya lo había enviado entero, texto y hojas de estilo, y tu navegador ya lo había dibujado. Lo que se lo llevó fue código que el editor decidió ejecutar después, en tu ordenador, comprado a un proveedor comercial cuyo producto consiste en quitarte lo que se acaba de entregar.

En casa tengo un Pi-hole1, que bloquea los hosts de publicidad y rastreo para todos los dispositivos de la red. En una lista creciente de sitios de noticias británicos, eso bastaba para que la página quedara destruida. Así que el 22 de septiembre de 2026 construí una extensión de Chrome para impedirlo, llamada Keep The Page. El código está en GitHub en damo2929/browserplugin, con licencia MIT, y esto es lo que hace, lo que encontré dentro de esas páginas mientras la construía y los arreglos que lo empeoraron antes de que apareciera el bueno.

No es un saltador de muros de pago. Si el servidor nunca envió el artículo, nada de esto lo va a hacer aparecer. The Times siguió cerrado, y así debe ser. Lo que conserva es lo que ya te habían enviado.

El artículo llega, y luego desaparece

Vistos desde fuera, todos funcionan igual. El HTML llega completo, y luego un script, normalmente cargado desde un host de un proveedor y no del periódico, decide que no mereces verlo, y lo quita.

Qué quita, y cómo, depende del proveedor que compró el editor. Estos son los cuatro con los que me encontré, cada uno leído de una página real durante la construcción:

EditorQué llegaQué se hace luego la página a sí misma
Newsquest (269 cabeceras)2el artículo completolevanta un muro, ejecuta una carga eval, abre un diálogo confirm()
notebookcheck.netel artículo completoborra <body> a los 7 segundos más o menos, luego diálogos, luego un bucle de recarga
National World (The Scotsman, Yorkshire Post)3el artículo más unos 68KB de su propio CSSborra cada <link> y <style> cada 100ms, para siempre
Reach plc (Mirror, Daily Record, Manchester Evening News, Liverpool Echo y otros)4el artículo completolo tapa con «acepta el rastreo o paga £2,99/mes»

El 269 no sale de una nota de prensa, que habla de «más de 200 marcas». Es el número de dominios en los propios certificados TLS de Newsquest, la lista que la extensión necesitaba para saber dónde ejecutarse.

El caso de National World es el que más debería preocupar a los editores, porque rompe la página por razones que no tienen nada que ver con la publicidad. El decapador de hojas de estilo tiene un compañero que recupera el CSS desde el host del proveedor. Mi Pi-hole bloquea ese host. Así que el decapado se ejecutó, la restauración nunca llegó, y la maquetación del propio periódico quedó dependiendo para siempre de que un proveedor de publicidad fuera alcanzable. Eso no es un muro. Es un editor que entrega el aspecto de su propia web a un tercero sin darse cuenta.

¿Puedo preguntar por qué la hoja de estilo de un periódico tiene que esperar al servidor de una empresa de publicidad antes de poder quedarse en la página? No hay ninguna razón que sirva al lector. Ninguna.

Por qué lo llamo malware

Uso la palabra a propósito, como descripción de lo que hace el código, y no como conclusión jurídica sobre nadie. Las cargas anti-adblock cumplen el sentido corriente en cuatro puntos, y cada uno se observó directamente:

CriterioLo que vi
Se ejecuta sin consentimiento, contra tu interésnadie lo pidió, y su trabajo es quitarte contenido que ya tienes
Destruye datos que ya te han entregadoel artículo y su CSS llegan intactos, luego código dentro de la página los borra
Ofuscado para resistir la lecturatablas de cadenas permutadas como o[293 * (r + 450) % e], ejecutadas mediante eval
Esquiva el bloqueo y castiga la interferenciaCNAME cloaking para esquivar las listas de bloqueo DNS, comprobaciones antimanipulación que escalan a un diálogo o un bucle de recarga

La ofuscación no es minificación. El código minificado es pequeño. Esto es código dispuesto para que no puedas buscar con grep lo que hace, y la carga pasa luego por eval para que nada en el disco coincida con lo que se ejecuta.

El cloaking es una técnica conocida con su propia literatura de investigación5. El cargador se pide desde lo que parece un subdominio del periódico, y ese nombre apunta al proveedor:

a02342.<publisher-domain>   ->  cdn-52-x.privacy-mgmt.com      (Sourcepoint)
fb.html-load.com            ->  adshield-fallback-dev-wskxz.b-cdn.net

El subdominio es aleatorio para cada cabecera. Una lista de bloqueo que nombra hosts no puede seguirle el ritmo, y de eso se trata.

Y el código trata cualquier interferencia como prueba de culpa. Los mensajes de error descodificados en la carga que los mantenedores de listas de filtros atribuyen a Ad-Shield6 son literalmente Vital API blocked y Vital API blocked (eval). El cargador de Sourcepoint escribe un atributo, lo vuelve a leer al instante y lanza un error si el valor ha cambiado:

z.call(O,'src',G), O[x]('src') !== G && throw E
… catch (W) { try { await l(W) } catch (x) { o(W) } }   // o() raises the dialog

Sourcepoint ha vendido esto abiertamente. Su propia documentación decía «on average about 30% of messaged users will turn off their adblockers»7. Así que no describo un script rebelde que alguien coló. Es un producto, comprado y desplegado a propósito por el editor.

Los muros consent-or-pay son otra categoría, y a esos no los llamo malware. No destruyen lo entregado ni se esconden de las listas de bloqueo. Coaccionan de otra manera, y de eso trata la siguiente sección.

Acepta 1 467 socios o paga £2,99

El muro en las cabeceras de Reach es Quantcast Choice, ahora gestionado por InMobi8 y servido desde cmp.inmobi.com. Da dos opciones. Aceptar, o pagar £2,99 al mes. No hay un «no» gratuito.

Aceptar comparte tus datos con 1 467 socios listados y escribe una cookie euconsent-v2 que dura 13 meses. Nadie lee una lista de 1 467 empresas, nadie podría sopesar qué haría cada una con los datos aunque la leyera, y la cifra por sí sola te dice qué clase de consentimiento se está pidiendo.

El RGPD británico dice que el consentimiento tiene que darse libremente, y que al juzgarlo se mira si el servicio se ha condicionado a un consentimiento para un tratamiento que no necesita9. El ICO ha publicado orientaciones según las cuales el consent or pay puede ser lícito10, orientaciones que ahora dice que están en revisión, y el Comité Europeo de Protección de Datos ha dicho que para las grandes plataformas que solo ofrecen esas dos opciones, en la mayoría de los casos no lo será11. No voy a fingir que el regulador lo ha prohibido. No lo ha hecho.

Así que aquí es donde llego, sin rodeos. Elegí quitar el muro y rechazar el consentimiento. Eso significa que leo el artículo por la rama gratuita, sin pagar y sin entregar mis datos a 1 467 empresas. Es una decisión. La extensión lo dice en su propia página de ajustes, y no la disfrazo de algo neutral. Un consentimiento que no puedes rechazar es un precio, y no voy a pagar un precio disfrazado de pregunta.

Quitar el recuadro es el tercio fácil. Los otros dos tercios están en Responder bien a la pregunta del consentimiento, porque un banner que borras sin responder vuelve cada vez.

Un día, ocho commits

Todo se construyó en un día. Unas horas husmeando en páginas antes de hacer ningún commit, y luego ocho commits entre las 20:32 y las 22:50. Lo construí con Claude Code, y buena parte del trabajo duro de leer script inline ofuscado línea a línea lo hizo el agente mientras yo miraba lo que las páginas hacían en pantalla. Ese reparto funcionó bien, y dónde falló está más abajo, porque falló de una manera que conviene conocer.

HoraCommit
20:32primer commit: Newsquest, notebookcheck, National World, Reach, Page Six
20:46el proveedor detrás de cada mecanismo, anotado en el README
20:5650 enlaces de la portada de Google News, como muestra no elegida
21:02responder a la API de consentimiento con todas las finalidades denegadas
21:11enlaces sociales desactivados
22:50protecciones desactivables, MSN y Bing, rechazo del consentimiento con un clic, 77 tests

Es una extensión Manifest V3 con dos permisos, declarativeNetRequest y storage, sin permisos de host y sin acceso propio a la red, así que no puede descargar nada, reescribir ninguna respuesta ni hablar con ningún servidor en tu nombre. Esa restricción dio forma al diseño más que ninguna otra cosa, porque el trabajo interesante tiene que ocurrir dentro de la página.

Dos mundos, un atributo que baja y un evento que subeEl código de la página y los ajustes de la extensión viven en mundos distintosAlmacenamientochrome.storage.localproteccionescanales de trazadoajustes socialesúltimos 50 erroresleído y escrito porla página de ajustesMundo aisladobridge.jssocial.jsportal.jspuede leer chrome.storageno puede tocar lasfunciones de la páginaMundo de la página (MAIN)walls.jsguard.jsportal-early.jsenvuelve setTimeout, cookie,__tcfapi, window.adLightningún chrome.*Baja: un atributo en el elemento html, solo lo apagado<html data-ktp-off="cookies,dom">instalación por defecto: nada escritoSube: un evento ktp-reportdetail: una cadena JSONun objeto no cruza de forma fiable
Chrome ejecuta los scripts de extensión en dos mundos. El mundo de la página alcanza el propio JavaScript de la página pero no tiene APIs de extensión. El mundo aislado puede leer los ajustes pero no puede tocar las funciones de la página. Todo pasa entre ellos como un atributo que baja y un evento que sube.

El mundo MAIN de Chrome comparte el entorno JavaScript de la página12. Un script ahí puede sustituir setTimeout, envolver document.cookie o definir window.adLight antes que la página, y eso es justo lo que hace falta para pelear con estos muros. En la práctica no puede llamar a chrome.storage. El mundo aislado sí puede, pero no ve las funciones de la página. Así que un pequeño puente lee los ajustes y los escribe en <html>, y los errores suben como un CustomEvent cuyo detalle es una cadena JSON, porque un objeto no cruza esa frontera de forma fiable.

El atributo que baja solo lista lo que has desactivado. Una instalación por defecto no escribe nada en la página. Eso importa, porque una marca permanente en <html> es justo el tipo de cosa que buscan estos SDK, y porque así un fallo del almacenamiento falla hacia defender la página y no al revés.

Encuentra la puerta, no pelees con el muro

El arreglo de Newsquest son dos líneas de razonamiento, y es con el que se midió todo lo demás.

Todo su muro depende de una sola bandera en la página:

var adLight = false;        // line 1647
if (adLight !== true) { …}  // line 2020: loader, eval payload, confirm()

adLight es la bandera de suscriptor de «pocos anuncios». Si es verdadera, el muro no se levanta nunca. Así que la extensión define window.adLight como true en document_start, antes de que se ejecute el propio script de la página, con un setter que ignora lo que se escriba en él:

Object.defineProperty(window, 'adLight', {
  configurable: false,
  enumerable: true,
  get() { return true; },
  set() { /* ignore the page's "false" */ }
});

Un var al principio de un script no redefine una propiedad que el objeto global ya tiene. Solo le asigna un valor13. Así que el propio var adLight = false de la página se ejecuta, cae en el setter y no hace nada. El cargador del muro no arranca nunca, la carga eval no llega nunca y no hay ninguna comprobación antimanipulación que saltar, porque no se ha manipulado nada. La bandera simplemente dijo que sí.

Es la única propiedad de toda la extensión que no es configurable. Tiene que serlo para sobrevivir a la declaración. Todo lo demás es configurable: true, para que a ninguna página se le quite para siempre una de sus propias APIs.

Cubre 269 cabeceras y no se puede desactivar en los ajustes, que explican por qué: se ejecuta antes de que chrome.storage pueda responder, y una vez puesta no se puede deshacer. Una casilla sería decoración.

Cada arreglo que lo empeoró

Esta es la sección útil, porque cada error aquí es lo primero que a uno se le ocurre probar.

Lo que probéLo que pasó
Bloquear el host del cargadorel host es un CNAME first-party aleatorio por cabecera, y una petición fallida es en sí misma la señal de detección
Vigilar setAttribute en los scripts inyectadosdisparó la comprobación de relectura de Sourcepoint, que abrió el diálogo que se quería evitar
Responder al confirm() con Cancelaren este SDK, Cancelar significa recargar, así que provocó un bucle de recarga infinito
Guardar una copia de <body> en DOMContentLoaded para restaurarla despuésel muro vacía la página durante el análisis, así que la copia era de un body vacío
Vigilar remove() y removeChild()el muro limpia la página con un solo innerHTML = '', no nodo a nodo
Vigilarlo todo a la vez en notebookcheckel muro escaló a un diálogo y luego a un bucle de recarga, claramente peor que no hacer nada
Redirigir el host del proveedor a un stub localuna regla redirect con solo declarativeNetRequest invalidó todo el conjunto de reglas y mató en silencio la regla de Newsquest en 269 sitios
Ejecutar los vigilantes de página en todos los sitiosparcheó prototipos globales en cada página que visitaba, incluida la de mi banco

La redirección merece una segunda mirada. Chrome da a una regla block acceso implícito y exige permiso de host para cualquier otra cosa14. Su documentación dice que las reglas estáticas no válidas se ignoran15. Lo que yo vi fue peor: se fue el archivo entero, sin ningún error en la página, y la regla 1 simplemente dejó de existir en 269 sitios. Por eso las dos reglas de MSN viven ahora en un conjunto propio, para que un mal cambio en uno no se lleve por delante al otro.

El bucle de recarga enseñó la otra regla dura. location.reload no se puede interceptar. El objeto Location es infalsificable en el estándar HTML16, así que sus métodos no son modificables ni configurables, e intentarlo da:

Object.defineProperty(location,'reload',...)
-> TypeError: Cannot redefine property: reload

Un muro cuyo camino de fallo es «recarga la página» ya no se puede detener una vez está en ese camino. Con nada. El único arreglo es asegurarse de que nunca llegue ahí. Es la misma lección que con adLight, aprendida por las malas: cada intento de pelear con un muro ya en marcha lo empeoró, y cada arreglo que funcionó impidió que el muro arrancara.

Un solo temporizador hizo el daño

notebookcheck no tiene adLight. El muro se ejecuta siempre. Con el trazado activado, la extensión mostró lo que programaba:

dropped setTimeout(7005ms) scheduled from eval    <- the body.remove()
dropped setTimeout(1251ms) / 105ms / 0ms x8
dropped setInterval(15000ms)

Uno de esos temporizadores hace todo el daño: el de los 7 segundos que quita <body>. Todo lo que viene después es reacción, porque la página vacía lanza un error, la excepción abre el diálogo, el diálogo recarga la página y todo vuelve a empezar desde arriba con una nueva tanda de temporizadores.

Así que el arreglo es una sola regla. Descarta un temporizador si se programó desde código pasado por eval, la página lleva la firma de este SDK y el manejador es una función. La pila dice de dónde vino una llamada, y eval deja su marca en ella.

before: 4 page loads, 3 confirms, reload loop
after:  1 page load,  0 confirms, alive 40,378ms, content intact
Un temporizador, y todo lo que viene detrásnotebookcheck: un temporizador hace el daño, el resto es reacciónTal como se sirveartículo y CSSllegan, dibujadoscargador desdehtml-load.comcarga evalfija temporizadores7,0s: se disparabody.remove()la página vacía falla,se abre confirm()la página recargay vuelve a empezar4 cargas, 3 diálogos, bucle de recargaCon la extensiónartículo y CSSllegan, dibujadoscargador desdehtml-load.comcarga evalfija temporizadorestemporizadores evaltodos descartados1 carga, 0 diálogos,viva a los 40 segundos
notebookcheck sin y con la extensión. Nada de lo que viene después del temporizador de 7 segundos necesita arreglo, porque nada de eso ocurre una vez descartado ese temporizador.

En ese momento había otros dos mecanismos en el build, una redirección del cargador a un stub y un elemento señuelo para absorber las escrituras del muro. Los dos funcionaban, y los dos trataban síntomas de ese único temporizador, cosa que solo quedó clara al probar la regla de temporizadores por separado y ver que bastaba por sí sola. Así que los dos se fueron, y con ellos un permiso y todos los permisos de host. Comprueba siempre si el último cambio basta por sí solo antes de conservar el andamiaje de alrededor.

La firma es el atributo data-sdk de la etiqueta del cargador, y encaja con una forma, l/<n>.<n>, en vez de con una versión. En un día aparecieron tres versiones. Se queda enganchada y nunca guarda un negativo, porque puede que la etiqueta del cargador todavía no se haya analizado cuando se programan los primeros temporizadores, y un «no» recordado la desarmaría para siempre en una página que sí lleva el muro.

La medición decía que bien. La pantalla, no.

The Scotsman y el Yorkshire Post se dieron por arreglados basándose en una medición que contaba texto. La página tenía 8 808 caracteres, 22 elementos bajo <body>, estables a los 2, 8 y 16 segundos. Según esa medida, estaba intacta.

No lo estaba. Yo la estaba mirando, y todas las hojas de estilo habían desaparecido, los enlaces eran una lista pelada, el logo SVG llenaba la pantalla y había una barra de desplazamiento horizontal. Todas las palabras estaban ahí, y eso es todo lo que esa medición podía ver. Tuve que señalar la pantalla y decirlo.

Lo que midió la prueba, y lo que había en pantallaYorkshire Post antes del arreglo: la misma página, medida de dos manerasLo que midió la pruebainnerText.length8 808hijos de body22a los 2s, 8s, 16ssin cambiosVeredicto: intactaLo que había en pantalladocument.styleSheets.length0enlaces en lista pelada, logo a todo lo ancho,una barra de desplazamiento horizontalVeredicto: rotaCon el decapador descartado al programarse: 2 hojas de estilo, 532 reglas, la página se muestra
La misma página, medida de dos maneras. La longitud del texto decía que la página estaba sana. El número de hojas de estilo decía que estaba rota, y el número de hojas de estilo tenía razón.

La causa era el decapador de la primera tabla, y no encajaba con la regla de temporizadores, porque es un script inline normal y no eval. Así que su propio código fuente es la firma: una tarea repetida cuyo cuerpo llama a querySelectorAll('link,style') y luego a remove(). Nada legítimo hace eso. Se descarta al programarse, nunca se decapa nada y no hace falta restaurar nada. El Yorkshire Post pasó de 0 hojas de estilo a 2, con 532 reglas, y se mostró bien.

Encontrarlo necesitó una traza de pila, no una suposición. Parchea Element.prototype.remove para que registre la pila cada vez que quita un STYLE o un LINK, y señaló el script inline y el forEach al primer intento.

Desde entonces, document.styleSheets.length entró en cada comprobación. Una página sin hojas de estilo está rota, tenga el texto que tenga. Medir es más difícil de lo que parece, y estas son las trampas en las que cayó el build aquel día:

TrampaLo que decíaLo que era verdad
longitud del texto como prueba de renderizado«intacta»marcado sin estilo
una sola muestra tras la cargamuros de Reach «ausentes» en ocho cabecerasel muro vive unos 600ms y ya se había barrido
consola vacía tras navegar«el vigilante no se dispara»los mensajes de la carga no sobreviven a la navegación
muestras a 1s y 4snotebookcheck sanose vacía entre los 5 y los 8 segundos
cssRules contado entre orígenesESPN tenía 5 reglaslas hojas de otros orígenes lanzan error, así que la cuenta sale baja

El arreglo para el caso de los 600ms es consultar cada 100ms desde el momento en que carga la página. En Wales Online el muro apareció a los 425ms y había desaparecido a los 1 129ms.

Luego las comprobaciones se hicieron en serio. 52 artículos en 26 dominios, dos por sitio, elegidos para cubrir cada mecanismo: 52 de 52 con hojas de estilo, ningún muro en pantalla, ningún bucle de recarga. Luego 50 enlaces sacados directamente de la portada británica de Google News, no elegidos por mí: 45 se mostraron con normalidad, 2 eran hosts que mi Pi-hole bloquea a propósito, 1 era el muro de pago de verdad de The Times, y 2 eran el mismo artículo aterrizado dos veces porque Google reconstruye el orden de los enlaces en cada carga. Ninguno con cero hojas de estilo. Tres cabeceras de National World que yo nunca había probado aparecieron en esa ronda con el mismo SDK, y las tres se mostraron bien, que es justo para lo que sirve encajar con una forma en vez de con una lista de sitios.

Hay 77 tests unitarios, en el repositorio con todo lo demás. En esta máquina no hay Node, así que se ejecutan en gjs y cargan el walls.js real contra un DOM de sustitución, para que lo que se prueba sean los patrones de producción. Cada uno se comprobó rompiendo lo que vigila y viéndolo ponerse en rojo. Solo prueban decisiones. Que pasen no significa que una página se muestre, y el Scotsman es la razón de que esa frase esté en el README.

Responder bien a la pregunta del consentimiento

Borrar un banner de consentimiento deja la pregunta sin responder. Eso tiene dos consecuencias, y la primera es que el banner se reconstruye en cada carga de página. Y un editor que retiene su contenido hasta que la API de consentimiento responda se quedará colgado sin más, mientras que un proveedor que no recibe respuesta puede tratar la pregunta como si nunca se hubiera hecho. El silencio no es un rechazo.

Así que la extensión la responde, en este orden:

Rechazar, decir que no, no guardar nada, y solo entonces quitarA un banner de consentimiento se le responde, no solo se borraContenedor de un proveedor en pantalla#qc-cmp2-container #onetrust-consent-sdk sp_message_container1. Pulsar su rechazoReject all, Decline, Only essential,Continue without acceptingsolo etiqueta entera, 40 caracteres máx.coincide con Accept: falla el build2. Responder a la API: no__tcfapicada finalidad, función yproveedor denegadostcString "" tcloaded3. No guardar nunca el registroeuconsent-v2 addtl_consentOptanonConsent didomi_tokenescrituras de cookie ylocalStorage descartadas¿Sigue en pantalla en el siguiente tic?sí: quitarlo, desbloquear el desplazamientono: el rechazo se mantiene
Tres respuestas y un recurso final. Se pulsa el botón de rechazo si lo hay, a la API de consentimiento se le dice que no a todo, el registro nunca se guarda, y solo se quita un banner que siga en pie en el siguiente tic.

Primero pulsa su botón de rechazo. Solo botones cuya etiqueta entera sea un rechazo: «Reject all», «Decline», «Only essential», «Continue without accepting» y algunos más, cada uno anclado, y todo lo que pase de 40 caracteres se ignora. Un test unitario le da «I Accept», «Accept All», «Agree and close», «Allow all», «Got it», «Subscribe» y «Pay £2.99/mo» y hace fallar el build si alguno coincide. Pulsar el botón equivocado sería consentir en tu nombre, y eso es lo único que este proyecto no debe hacer nunca. En msn.com, pulsar «Reject All» hizo desaparecer el banner de Microsoft y no volvió al recargar, porque Microsoft guarda el rechazo en sus propios servidores.

Responde que no a la API de consentimiento. El marco del IAB da a cada página que pide consentimiento una función llamada __tcfapi para preguntar a qué has accedido17. Donde existe, la extensión la responde con cada finalidad, cada función especial y cada proveedor denegados, una cadena de consentimiento vacía y eventStatus: 'tcloaded', que significa que la respuesta es definitiva. Solo aparece donde ya hay un marco de consentimiento en la página, así que un sitio que nunca preguntó no la ve.

Nunca guarda el registro. document.cookie y localStorage descartan en silencio euconsent-v2, addtl_consent, OptanonConsent, didomi_token y el resto, para que un consentimiento que nunca diste no se escriba nunca ni se reproduzca ante 1 467 socios en la página siguiente. La ley británica ya exige consentimiento antes de guardar nada en tu dispositivo para esto18. La extensión hace cumplir el no.

Cada nombre de esa lista está anclado por los dos extremos, y detrás hay una historia. Las cookies de Sourcepoint empiezan por _sp_. Un patrón descuidado sp_ también pilla sp_dc y sp_t de Spotify, que son la sesión de inicio, y me habría desconectado de Spotify en cada página. Un test también fija eso.

Quitar es el recurso final. Solo para un banner que siga en pantalla después del clic, y solo para un banner que de verdad se esté mostrando. Varios proveedores dejan un envoltorio permanente en la página, haya banner o no. euronews mantiene un host de Didomi con altura cero, y arrancarlo rompe la página sin ganar nada. El de Reach estaba tres niveles por debajo de <body>, dentro de dos envoltorios que no son fijos, así que la comprobación tiene que bajar hasta la caja que sí lo es.

Botones de compartir, y un portal que ignora sus propios ajustes

Otras dos cosas de estas páginas existen para servir a alguien que no es el lector, y necesitaban un trato distinto al de los muros.

Botones de compartir y seguir. Cada enlace a Facebook, Instagram, X, TikTok o LinkedIn se reescribe a http://localhost/removeme, con el original aparcado en un atributo para que no se pierda nada. Luego una segunda pasada borra lo que es claramente mobiliario (un icono sin texto, «Share on X», todo lo que esté en un contenedor que se llame share o social) y oculta el resto. La marca está ahí para que un solo selector muestre todo lo que está a punto de irse, y un modo de solo marcar se detiene tras la primera pasada para que puedas mirar antes de fiarte de ella en un sitio.

La versión ingenua rompe páginas de tres maneras, y cada una es ahora un test:

Versión ingenuaLo que rompeLo que se hace en su lugar
a[href*="x.com"]también pilla netflix.com, linux.com, phoenix.comanalizar el host y comparar etiquetas enteras
quitar cada enlace social«Continue with Facebook» desaparece y la gente se queda fueraenlaces de inicio de sesión, OAuth, legales y de desarrolladores intactos
quitar un enlace dentro de una fraselas palabras se van con élocultar por defecto; una opción lo desenvuelve y conserva las palabras

Esa última es una concesión que hice a sabiendas. Una despedida de la BBC queda así: «follow BBC Manchester on , , and .». Lo miré y aun así elegí ocultar. La opción de conservar las palabras está ahí para quien no esté de acuerdo.

MSN y Bing. Los dos funcionan con los mismos web components, unas 160 shadow roots en la portada. Una consulta simple al documento encontró 1 enlace. Recorrer las shadow roots encontró 73, incluidos los mosaicos de Facebook y X. Lo que no las recorre no ve casi nada.

MSN sí tiene sus propios ajustes de contenido. Se guardan en los servidores de Microsoft, ligados a un identificador anónimo, no en una cookie, así que desaparecen tras borrar las cookies, en un perfil nuevo y en modo incógnito. A Bing no llegan en absoluto. Y con todos desactivados y 11 700 píxeles de desplazamiento, esto era lo que seguía en el feed:

Interruptor propio de MSN, apagadoSigue en la página
Casual Gamesel mosaico de Juegos
Shoppingmosaicos publicitarios de Booking, Temu y eBay
Comments501 enlaces de comentarios
Weather, Finance, Sportsdesaparecen, hasta el próximo borrado de cookies

Un interruptor llamado Comments que deja 501 enlaces de comentarios en la página es una afirmación hecha al usuario, y una afirmación falsa. Así que la extensión los quita ella misma, apuntando a los nombres de componente estables de MSN en lugar de a sus nombres de clase, que cambian con cada build.

Una cosa que aprendí ahí y que vale mucho más allá de MSN: quitar una imagen de la página no impide que se cargue. Chrome empieza la descarga en cuanto se asigna src, incluso para una imagen que nunca se mete en la página19. Para cuando un content script ve una tarjeta, su miniatura ya va por el cable. Así que los datos del feed se bloquean en los dos puntos de acceso que los sirven, y nunca los hosts de imágenes, porque th.bing.com también sirve la búsqueda de imágenes de Bing.

Y el feed de MSN todavía parpadea un momento en pantalla antes de desaparecer. Cuatro intentos de ocultarlo antes se midieron todos como eficaces y ninguno paró el parpadeo, lo que significa que lo que se pinta no es lo que la extensión oculta. La página de ajustes lo dice claramente. Prefiero que lo diga a que insinúe una carga limpia que no entrega.

Lo que no hace

No va aPorque
abrir un muro de pago de verdadsi el servidor retiene el artículo, sigue retenido
adivinar con un sitioun dominio solo entra después de ver su muro ahí; dos sitios de News Corp que se suponían iguales a Page Six no lo eran, y volvieron a salir
tocar un sitio sin firmaen la BBC cada gancho está instalado y no se registra nada, ningún temporizador descartado, ningún nodo tocado
llamar a casasin permisos de host, sin acceso a la red, sin telemetría; el registro de errores se queda en tu navegador
fingirel parpadeo de MSN no está resuelto, y una variante de Ad-Shield, wp-ls/…, todavía no encaja; su página se mostraba bien, así que queda anotado en vez de arreglado a ciegas

Una página que te han enviado es tuya

Una vez que un servidor ha enviado una página a tu navegador, esa copia está en tu máquina. Ejecutar código sobre ella después para quitártela no es un modelo de negocio que yo reconozca. Es el viejo truco de venderte algo y seguir con la mano encima.

Los periódicos eran algo que comprabas y luego poseías: alguien en la esquina tenía un montón, dabas el dinero, y se venía a casa contigo y era tuyo, para leerlo, doblarlo, prestarlo o encender la chimenea con él. Nadie pasaba por casa una hora después a recortar la segunda página porque te habías saltado los anuncios. La versión web regaló el periódico y luego vendió al lector. Primero a los anunciantes, luego a los 1 467 socios, y ahora a un proveedor cuyo producto entero consiste en decidir si te has portado lo bastante bien como para quedarte con lo que te dieron.

Lo que no consigo tragarme es la hoja de estilo. Un periódico que deja que el servidor de una empresa de publicidad decida si su propia maquetación sobrevive ha regalado su propia portada. No lo habrá sabido, porque nadie dentro bloqueaba el host, así que el primero en enterarse fue un lector con un Pi-hole, delante de una página llena de enlaces pelados, al que un cuadro de diálogo le decía que la culpa era suya. No lo era.

El periodismo tiene un precio, y no tengo ningún problema en pagarlo. Lo que no voy a hacer es dejar que un script decida lo que ya tengo. Esa raya se traza en mi navegador, no en el suyo.


  1. Pi-hole documentation — «The Pi-hole® is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software.» ↩︎

  2. Newsquest — About us — «We are the leading local news publisher in the UK with a portfolio of more than 200 brands.» Las marcas no son dominios, de ahí la cifra más alta sacada de los certificados. ↩︎

  3. Daily Business, 18 de diciembre de 2024 — «National World, owner of The Scotsman and Yorkshire Post, has reached agreement on a £65.1 million takeover by Irish publisher Media Concierge.» ↩︎

  4. Reach plc — About us, archivado el 5 de septiembre de 2026 — «120+ brands, from household names like the Mirror, Express, Daily Record and Daily Star, to local titles like MyLondon, BelfastLive and the Manchester Evening News». ↩︎

  5. Dimova et al., «The CNAME of the Game: Large-scale Analysis of DNS-based Tracking Evasion», PETS 2021 — el CNAME cloaking «effectively bypasses antitracking measures that rely on fixed hostname-based block lists.» ↩︎

  6. uBlockOrigin/uAssets, incidencia #30988, clasificada por los mantenedores bajo la etiqueta «Ad-Shield», con el mensaje servido desde error-report.com: «Failed to load website properly since html-load.com is blocked.» Véase también Jacob Desforges, «Ad-Shield ad reinsertion», 12 de abril de 2026. La atribución es de la comunidad de listas de filtros; el proveedor no revela nada. ↩︎

  7. Sourcepoint — Anti-adblock FAQs, archivado el 25 de mayo de 2022 — «Historical experience shows on average about 30% of messaged users will turn off their adblockers.» La misma página pregunta si «a CNAME applied to a 1st-party subdomain» impediría que se bloquee el script de detección. ↩︎

  8. AdExchanger, 16 de agosto de 2023 — «InMobi acquired Quantcast’s consent management platform, called Quantcast Choice». ↩︎

  9. UK GDPR, Article 7 — «utmost account shall be taken of whether, inter alia, the performance of a contract, including the provision of a service, is conditional on consent». Considerando 42: el consentimiento no se da libremente «if the data subject has no genuine or free choice». ↩︎

  10. ICO — Consent or pay, publicado el 23 de enero de 2025 — «“Consent or pay” models can be compliant with data protection law if you can demonstrate that people can freely give their consent». La página dice ahora que la orientación está en revisión tras la Data (Use and Access) Act. ↩︎

  11. EDPB Opinion 08/2024, adoptado el 17 de abril de 2024 — «In most cases, it will not be possible for large online platforms to comply with the requirements for valid consent if they confront users only with a binary choice». Trata de las grandes plataformas en línea, no de los periódicos regionales. ↩︎

  12. Chrome for Developers — content_scripts manifest key — «Choosing the “MAIN” world means the script will share the execution environment with the host page’s JavaScript.» ↩︎

  13. ECMAScript — CreateGlobalVarBinding — «If a binding already exists, it is reused and assumed to be initialized.» ↩︎

  14. Chrome for Developers — declarativeNetRequest — «provides implicit access to allow , allowAllRequests and block rules», y si no «you must request host permissions before you can perform any action on a host.» ↩︎

  15. Chrome for Developers — declarativeNetRequest — «Errors and warnings about invalid static rules are only displayed for unpacked extensions. Invalid static rules in packed extensions are ignored.» ↩︎

  16. HTML Standard — the Location interface marca sus miembros como [LegacyUnforgeable], lo que en Web IDL significa «the property will be non-configurable and will exist as an own property on the object itself». ↩︎

  17. IAB Tech Lab — TCF v2 CMP API — «Every consent manager MUST provide the following API function: __tcfapi(command, version, callback, parameter)». ↩︎

  18. PECR, regulation 6 — «a person must not store information, or gain access to information stored, in the terminal equipment of a subscriber or user», con el consentimiento como condición en Schedule A1, modificado por la Data (Use and Access) Act 2025. ↩︎

  19. HTML Standard — update the image data — se ejecuta «whenever that element is created or has experienced relevant mutations», también cuando se asigna su src; estar en el documento no es una condición. ↩︎