Chrome quita XSLT: qué se rompe en sitemaps, feeds y visores de CFDI
Tabla de Contenidos
Chrome 158 sale a estable el 17 de noviembre de 2026 y deja de ejecutar XSLT: se van XSLTProcessor y el procesamiento de <?xml-stylesheet type="text/xsl"?>. En la mayoría de los sitios eso cambia el adorno y nada más. El sitemap o el feed que un humano veía como tabla vuelve a verse como XML crudo, sin error y sin tocar el archivo. Donde sí se rompe código es en un rincón muy mexicano: los visores y validadores de CFDI que arman la cadena original en el navegador con la hoja XSLT del SAT. Ahí la función truena con ReferenceError: XSLTProcessor is not defined.
Las dos cosas las probé en Chrome 152 con el interruptor que apaga XSLT antes de tiempo. Para la parte del CFDI fui más lejos: generé la cadena original de un comprobante de prueba con cinco motores distintos y validé cada resultado contra el sello digital del propio XML. Una librería XSLT escrita en JavaScript da una cadena con la que el sello no verifica, el polyfill que recomienda Chrome falla si la hoja conserva sus includes, y la hoja del SAT tal como se publica ni siquiera corre en el navegador.
Las fechas
Todo sale del anuncio de Chrome for Developers y de la entrada en Chrome Platform Status, con las fechas de estable cotejadas en Chromium Dash:
| Hito | Versión | Fecha |
|---|---|---|
| Avisos en la consola | Chrome 142 | 28-oct-2025 |
| Deprecación oficial (consola y Lighthouse) | Chrome 143 | 2-dic-2025 |
| Arranca el Origin Trial para seguir usándolo | Chrome 152 | 25-ago-2026 |
| Beta de la versión que lo quita | Chrome 158 | 28-oct-2026 |
| XSLT deja de funcionar en estable | Chrome 158 | 17-nov-2026 |
Se apagan el Origin Trial y la política empresarial XSLTEnabled | Chrome 176 | 17-ago-2027 |
Hay un detalle que acorta el plazo más de lo que parece: desde Chrome 153 (8 de septiembre) sale una versión estable cada dos semanas, no cada cuatro. Entre hoy y la 158 hay nueve semanas y cinco versiones.
¿Y Firefox y Safari? Van en la misma dirección, sin fecha. Mozilla tomó postura positiva y en su bug de seguimiento ya cerró el aviso en consola (Firefox 147) y una política empresarial (Firefox 151), pero apagarlo por defecto sigue abierto. WebKit se declaró “cautelosamente a favor” y dijo que probablemente esperaría a que un motor lo quite del todo. El estándar HTML ya lo marcó como deprecado el 25 de agosto de 2026. En la práctica, lo que diga Chrome 158 es lo que verá la mayoría de tus usuarios en noviembre.
Qué se rompe, qué ves y cómo se arregla
| Qué usas | Qué pasa con Chrome 158 | Arreglo |
|---|---|---|
Sitemap XML con hoja XSL (WordPress core, Yoast, Rank Math, xslURL de @astrojs/sitemap) | El humano ve el visor de XML de Chrome en vez de la tabla. No hay error y el archivo servido no cambia | Nada, si nadie lo mira. Si importa, quita la instrucción o sirve una versión HTML aparte |
Feed RSS/Atom con hoja XSL (stylesheet con .xsl en @astrojs/rss) | Abierto en el navegador, se ve XML crudo. Los lectores de feeds no lo notan | <link rel="alternate" type="application/rss+xml"> en tu HTML, o el polyfill de una línea |
JavaScript que llama new XSLTProcessor() | ReferenceError: XSLTProcessor is not defined | Mover la transformación al servidor, o un motor XSLT en JS/WASM |
| Visor o validador de CFDI que arma la cadena original en el navegador | La cadena no se genera, así que tampoco se puede verificar el sello | Servidor con ext-xsl, xslt-polyfill con la hoja aplanada, o SaxonJS (detalle abajo) |
<?xml-stylesheet type="text/css"?> | Nada: sigue soportado | — |
| XSLT en tu servidor (PHP ext-xsl, Saxon, xsltproc) | Nada: esto solo afecta al navegador | — |
Sitemaps y feeds: se rompe el adorno, no el archivo
Abrí el índice de sitemaps de yoast.com en Chrome 152 dos veces. Con XSLT activo, Chrome aplica la hoja y pinta una página titulada “XML Sitemap” con su tabla. Con --disable-features=XSLT, el mismo URL cae en el visor de XML de Chrome: el árbol de <sitemap>, <loc> y <lastmod> con la instrucción xml-stylesheet a la vista, sin estilos y sin error. Según el anuncio, Chrome además muestra un aviso que enlaza a extensiones cuando XSLT está apagado; en modo headless no lo pude ver.
Esto le va a pasar a mucha gente, porque la hoja XSL viene de fábrica en casi todo el ecosistema:
- WordPress core agrega
<?xml-stylesheet type="text/xsl" href=".../wp-sitemap.xsl" ?>awp-sitemap.xmldesde la 5.5. El ticket #65593 para quitarla está en el milestone de la 7.2, con el PR #12448 abierto y otra propuesta (#12584) que renderiza la versión HTML en el servidor. Al 13 de septiembre ninguno está fusionado. - Yoast SEO la sigue emitiendo en su
trunk, que el changelog marca como 28.5 para el 15 de septiembre, y el sitemap del propioyoast.comla trae hoy. - Rank Math la sigue emitiendo en su
trunk, y el sitemap derankmath.comtambién la trae hoy. - Astro:
@astrojs/sitemaptiene la opciónxslURLy@astrojs/rssponetype="text/xsl"si le pasas unstylesheetque termina en.xsl. Ninguna viene activa por defecto.
¿Y Google?
No voy a escribir “Googlebot no se ve afectado” como si Google lo hubiera dicho: no encontré ninguna declaración de Google Search sobre XSLT y sitemaps. Lo que sí es verificable es que el cambio vive en el navegador. El servidor manda exactamente los mismos bytes antes y después del 17 de noviembre, y el anuncio de Chrome aclara que no retiran XML, solo XSLT. Un rastreador que ya leía tu sitemap sin ejecutar la hoja lo va a seguir recibiendo igual.
Qué hacer con el sitemap
Si ningún humano abre tu sitemap, no hagas nada. Si lo usas como página de consulta o te lo piden clientes, tienes tres caminos:
-
Quitar la instrucción. Te ahorras la petición al
.xsly los avisos de consola. En WordPress core hay filtros oficiales: si devuelven un valor falso, según el propio código, “se mostrará el XML crudo”:// En un mu-plugin o en functions.php add_filter( 'wp_sitemaps_stylesheet_url', '__return_false' ); add_filter( 'wp_sitemaps_stylesheet_index_url', '__return_false' ); // Yoast: el filtro se llama "url", pero recibe la declaración completa add_filter( 'wpseo_stylesheet_url', '__return_empty_string' );En Rank Math el filtro va por tipo de sitemap (
rank_math/sitemap/{tipo}_stylesheet_url). En Astro basta con quitarxslURLostylesheet. -
Servir HTML aparte. Es lo que propone el PR #12584 de WordPress: la misma transformación, pero hecha en el servidor, publicada en una URL
.html. El.xmlqueda para las máquinas. -
CSS en lugar de XSL.
<?xml-stylesheet type="text/css"?>sigue soportado, pero en el ticket de WordPress apuntaron el límite: con CSS le das estilo al XML, pero los<loc>no se vuelven enlaces en los que se pueda hacer clic.
Para feeds, la recomendación de Chrome es anunciar el feed con <link rel="alternate"> en el HTML en vez de enlazar el .xml a la vista, o agregar al feed una sola línea con el polyfill.
CFDI: donde sí truena código
Contexto para quien no factura en México: el CFDI es la factura electrónica del SAT, un XML firmado. La firma (el atributo Sello) no se calcula sobre el XML entero, sino sobre la cadena original: los valores del comprobante en un orden fijo, separados por |. Para verificar un sello necesitas esa cadena. Para mostrar la del timbre en una representación impresa, también.
El SAT publica la secuencia de esa cadena como hoja XSLT: en su página del Anexo 20, el documento “Secuencia de cadena original” enlaza a cadenaoriginal_4_0.xslt, con fecha de última modificación del 17 de abril de 2026. El timbre tiene la suya, cadenaoriginal_TFD_1_1.xslt. Tres detalles de ese archivo importan aquí:
- Declara
version="2.0", pero los navegadores solo implementan XSLT 1.0. Aun así funciona con procesadores 1.0: la documentación de CfdiUtils lo dice de PHP, que usa el mismo motor que Chrome (libxslt), y lo confirmé abajo en el navegador. - Trae 33
xsl:include(las utilerías y los complementos), todos con URL absoluta ahttp://www.sat.gob.mx. - La salida es texto plano (
method="text").
Por eso generar la cadena en el navegador era tentador: XSLTProcessor venía incluido y el XML nunca salía de la máquina del usuario. Busqué código real que lo hace y encontré dos patrones:
- Una librería npm de validadores y catálogos mexicanos, con versión publicada a finales de agosto de 2026, cuya función de cadena original usa
new XSLTProcessor()si detectawindow, y en Node recurre a un motor en JS. En Chrome 158 la detección sigue siendo verdadera (DOMParserno se va), así que entra por la rama del navegador y lanza elReferenceError. - Un repositorio público de firma electrónica en JavaScript que carga
cadenaoriginal_3_3.xsltdesde un campo de archivo, lo guarda enlocalStoragey arma la cadena conXSLTProcessor.transformToDocumentpara verificar el sello de un CFDI 3.3.
La hoja del SAT no corre tal cual en el navegador
Esto lo encontré al probar y no lo vi documentado en ningún lado. Si le pasas a XSLTProcessor la hoja oficial, Chrome se niega a cargar los 33 includes (el URL de abajo es mi página de prueba):
Unsafe attempt to load URL http://www.sat.gob.mx/sitio_internet/cfd/2/cadenaoriginal_2_0/utilerias.xslt
from frame with URL http://localhost:8765/native.html. Domains, protocols and ports must match.
El resultado es null. O sea: quien hoy arma la cadena en el navegador con XSLTProcessor tiene que servir la hoja y sus includes desde su propio origen, o aplanados en un solo archivo. Eso ayuda a detectarlo, porque esa copia vive en tu repositorio y basta buscar cadenaoriginal.
Por qué el visor de CFDI de este sitio no se rompe
El visor de XML de CFDI 4.0 de este sitio no usa XSLT, y no por suerte. Lee el XML con DOMParser, recorre Comprobante, Emisor, Receptor, Conceptos, Impuestos y el TimbreFiscalDigital, traduce los códigos del catálogo y comprueba la aritmética. No arma la cadena original ni valida el sello, y la herramienta lo dice en pantalla. DOMParser es una de las APIs que Chrome cita como el camino moderno para leer XML, así que no está en riesgo. Hice grep a todo el repositorio y no hay ni un XSLTProcessor ni un xml-stylesheet. Los sitemaps y el RSS del sitio tampoco llevan hoja. El servidor MCP mx-fiscal-mcp-server, que lee CFDI desde agentes de IA, corre en Node con @xmldom/xmldom y tampoco usa XSLT.
Las alternativas, probadas contra un sello real
Afirmar que “SaxonJS lo resuelve” es fácil. Para comprobarlo hice lo siguiente. Tomé cfdi40-valid.xml de la suite de pruebas de CfdiUtils, un CFDI 4.0 firmado con un certificado de pruebas del SAT. Generé la cadena con cada motor y la di por buena solo si el Sello del XML verifica contra ella con la llave pública de su propio Certificado:
# cert.pem = el atributo Certificado en base64, envuelto como PEM
# sello.bin = el atributo Sello decodificado de base64
openssl x509 -in cert.pem -pubkey -noout > pub.pem
openssl dgst -sha256 -verify pub.pem -signature sello.bin cadena.txt
# Verified OK → la cadena es la correcta
La referencia fue Saxon-HE 12.5 (Java, licencia MPL 2.0) con la hoja oficial: Verified OK. Lo demás:
| Motor | Dónde corre | Licencia | Resultado con la hoja del SAT |
|---|---|---|---|
XSLTProcessor nativo, hoja oficial | Navegador | — | Falla: bloquea los includes de otro dominio |
XSLTProcessor nativo, copia local | Navegador | — | Verified OK. Deja de existir en Chrome 158 |
| xslt-polyfill 1.0.28, hoja aplanada en un solo archivo | Navegador (WASM) | BSD-3-Clause | Verified OK con XSLT nativo apagado |
| xslt-polyfill 1.0.28, copia con includes | Navegador (WASM) | BSD-3-Clause | Falla: Error: Unknown mime type undefined |
SaxonJS 2.7 (saxon-js + xslt3), hoja compilada a SEF | Node; el SEF también corre en navegador según su documentación | Gratuita, no es open source | Verified OK en Node |
| xslt-processor 5.1.2 | JS puro | LGPL-3.0 | Cadena distinta y el sello no verifica |
La cadena de xslt-processor empieza con ||||| (cinco barras en vez de dos). Con la hoja aplanada, la salida se reduce a tres barras. No lo descarto para otros usos, pero para la cadena original del SAT no sirve tal cual.
El fallo del polyfill con includes no es un bug suelto, está en su README: resuelve xsl:include con fetch(), que es asíncrono, y los métodos de XSLTProcessor son síncronos, así que fallan cuando la hoja tiene includes. Aplanar la hoja lo resuelve: sustituyes cada <xsl:include> por el contenido de la hoja incluida, en el mismo orden. En la hoja 4.0 actual no hace falta agregar espacios de nombres, porque la raíz principal ya declara todos los que usan los includes.
Opción 1: en el servidor (la más aburrida, y por eso la mejor)
ext-xsl de PHP sigue en el manual de PHP, montada sobre libxslt. La retirada de Chrome no la toca. Con CfdiUtils (eclipxe/cfdiutils, MIT, versión 3.0.4 del 11 de septiembre de 2026):
use CfdiUtils\XmlResolver\XmlResolver;
use CfdiUtils\CadenaOrigen\DOMBuilder;
// XmlResolver descarga los XSLT del SAT y reescribe sus dependencias a copias locales
$resolver = new XmlResolver();
$location = $resolver->resolveCadenaOrigenLocation('4.0');
$cadena = (new DOMBuilder())->build($xmlContent, $location);
Un aviso sobre algo que circula: CfdiUtils no construye la cadena sin XSLT. DOMBuilder es un XSLTProcessor de ext-xsl; también trae GenkgoXslBuilder (XSLT 2.0 en PHP) y SaxonbCliBuilder. Su autor explica que armarla sin XSLT se puede, pero es “mucho código para fabricar y testear” que además cambia con cada complemento.
Desde el navegador queda en una petición:
const res = await fetch('/api/cfdi/cadena-original', {
method: 'POST',
headers: { 'Content-Type': 'application/xml' },
body: xmlText,
});
const cadena = await res.text();
El precio es de privacidad: si tu visor prometía “el XML no sale de tu navegador”, con esto deja de ser cierto y tienes que cambiar el texto.
Opción 2: seguir en el navegador con el polyfill
Si la promesa de procesar en local es el producto, xslt-polyfill reemplaza XSLTProcessor con libxslt compilado a WebAssembly. Pesa unos 1.4 MB minificado. Con la hoja aplanada, tu código existente no cambia:
<script src="/vendor/xslt-polyfill.min.js"></script>
<script>
const xsl = new DOMParser().parseFromString(hojaAplanada, 'application/xml');
const cfdi = new DOMParser().parseFromString(xmlText, 'application/xml');
const p = new XSLTProcessor(); // el del polyfill si el navegador ya no trae XSLT
p.importStylesheet(xsl);
const cadena = p.transformToFragment(cfdi, document).textContent;
</script>
Mientras el navegador todavía tenga XSLT nativo, el polyfill no se instala. Si quieres probarlo hoy en tu Chrome actual, pon window.xsltUsePolyfillAlways = true antes de cargar el script.
Opción 3: SaxonJS
SaxonJS implementa las partes obligatorias de XSLT 3.0, que es de sobra para una hoja que declara 2.0. El flujo documentado es compilar la hoja a un archivo SEF y ejecutar ese SEF:
npx xslt3 -xsl:cadenaoriginal_4_0_plana.xslt -export:cadena.sef.json -nogo
import SaxonJS from 'saxon-js';
const { principalResult: cadena } = SaxonJS.transform({
stylesheetFileName: 'cadena.sef.json',
sourceText: xmlText,
destination: 'serialized',
});
En mi prueba compilar tardó 2.4 s y el SEF pesó 3.65 MB. Ese tamaño cuenta si lo vas a mandar al navegador. Revisa la licencia antes de redistribuirlo: Saxonica la ofrece gratis, pero no es open source.
Cómo saber si dependes de XSLT
1. Busca en el código. Los aciertos en PHP, Java o .NET de servidor no te afectan; los de JavaScript que corre en el navegador sí:
grep -rnE "XSLTProcessor|transformToFragment|transformToDocument|importStylesheet|xml-stylesheet|cadenaoriginal" \
--include=*.{js,mjs,ts,tsx,jsx,vue,svelte,astro,html,xml,php} \
--exclude-dir={node_modules,vendor,.git} .
find . -name "*.xsl" -o -name "*.xslt" | grep -v node_modules
2. Busca en lo que compilaste. La librería npm del ejemplo esconde XSLTProcessor dentro de node_modules, así que el grep de arriba no la ve. Corre el mismo patrón sobre la carpeta de salida (dist/, build/, .next/, public/build/).
3. Revisa lo que sirves.
curl -s https://tusitio.com/wp-sitemap.xml | head -c 300 | grep -o 'xml-stylesheet[^?]*'
curl -s https://tusitio.com/feed/ | head -c 300 | grep -o 'xml-stylesheet[^?]*'
4. Pruébalo con XSLT apagado. No hace falta esperar a noviembre ni instalar Canary. En Chrome estable entra a chrome://flags/#xslt y ponlo en Disabled, o arranca Chrome con --disable-features=XSLT (lo verifiqué en Chrome 152). Para una comprobación sin interfaz:
chrome --headless=new --disable-features=XSLT --dump-dom \
'data:text/html,<script>document.write(typeof XSLTProcessor)</script>'
# undefined → así se verá tu app en Chrome 158
5. Deja que producción te avise. Chrome reporta la deprecación por la Reporting API con el id XSLT:
new ReportingObserver((reports) => {
for (const r of reports) {
if (r.body.id === 'XSLT') navigator.sendBeacon('/telemetria/xslt', r.body.sourceFile ?? '');
}
}, { types: ['deprecation'], buffered: true }).observe();
Y abre la consola: desde Chrome 143 cada uso imprime “XSLTProcessor and XSLT Processing Instructions have been deprecated by all browsers”.
Quién tiene que actuar de verdad
Tienes un visor, validador o generador de PDF de CFDI que arma la cadena original en el navegador: eres el público de este post. Tienes hasta el 17 de noviembre. Decide entre servidor (y cambia el aviso de privacidad) o polyfill con hoja aplanada, y valida contra un sello real como arriba antes de publicar.
Usas XSLTProcessor para cualquier otra cosa en el front: mismo plazo, mismas opciones. Si no puedes migrar a tiempo, el Origin Trial te da hasta agosto de 2027. Si lo que usa XSLT es un documento XML, no hay <head> donde poner la meta, así que el token va en la cabecera HTTP Origin-Trial.
Tienes un sitio WordPress, Yoast, Rank Math o Astro con sitemap estilizado: no se rompe nada que importe para indexar. Quita la instrucción si te molesta el aviso, o sirve HTML aparte si alguien consulta ese sitemap.
Tu empresa usa una app interna o un equipo que sirve XML con XSL y no puedes tocar: la política empresarial XSLTEnabled y la extensión que recomienda Chrome son para ti. La política caduca en agosto de 2027. La extensión aplica el polyfill, así que no depende del XSLT nativo.
Solo generas la cadena en PHP, Java o .NET en el servidor: no te toca. Esto es del navegador.
Para seguir leyendo:
- mx-fiscal-mcp-server: la ficha del servidor MCP open source para leer CFDI 4.0 y validar RFC, CURP y CLABE desde agentes de IA.
- Mejores servidores MCP para SEO en 2026: si auditas sitemaps con agentes, ahí está qué herramienta hace qué.
- SEO para WordPress en 2026: la guía completa de Yoast y Rank Math, donde vive el sitemap del que hablamos.
Preguntas frecuentes
¿Cuándo quita Chrome el soporte de XSLT?
En Chrome 158, que sale a estable el 17 de noviembre de 2026. Desde esa versión dejan de funcionar XSLTProcessor y el procesamiento de las instrucciones xml-stylesheet con type="text/xsl", salvo para sitios inscritos en el Origin Trial y equipos con la política empresarial XSLTEnabled. Ambas excepciones se apagan en Chrome 176, el 17 de agosto de 2027. La beta de Chrome 158 sale el 28 de octubre de 2026.
¿Se rompe mi sitemap XML con hoja XSL en Chrome 158?
El archivo no. Lo que cambia es cómo lo ve una persona en Chrome: en vez de la tabla generada por la hoja XSL aparece el visor de XML crudo del navegador, sin error. Lo probé en Chrome 152 con XSLT apagado sobre el índice de sitemaps de yoast.com. El servidor sigue mandando exactamente los mismos bytes. Google no ha publicado nada específico sobre XSLT y sitemaps, pero el cambio es solo del navegador y no del documento XML.
¿Cómo quito la hoja XSL del sitemap de WordPress?
En WordPress core, con los filtros wp_sitemaps_stylesheet_url y wp_sitemaps_stylesheet_index_url devolviendo false, por ejemplo con __return_false; el propio código dice que así se muestra el XML crudo. En Yoast, el filtro wpseo_stylesheet_url recibe la declaración completa y puedes devolver una cadena vacía. En Rank Math el filtro va por tipo: rank_math/sitemap/{tipo}_stylesheet_url. WordPress core tiene abierto el ticket 65593 para quitar la hoja en la versión 7.2.
¿Por qué se rompen los visores de CFDI que usan XSLT en el navegador?
Porque la cadena original del CFDI 4.0 la define el SAT con la hoja cadenaoriginal_4_0.xslt, y la forma cómoda de ejecutarla en el navegador era XSLTProcessor. En Chrome 158 esa clase deja de existir y el código lanza ReferenceError: XSLTProcessor is not defined. Sin cadena original no se puede verificar el sello digital del comprobante. Leer el XML con DOMParser, como hace el visor de CFDI de este sitio, sigue funcionando.
¿Qué alternativa genera bien la cadena original del SAT sin XSLT del navegador?
Probé varias contra el sello de un CFDI 4.0 de prueba. Dieron la cadena correcta Saxon-HE en Java, SaxonJS 2.7 con la hoja compilada a SEF y xslt-polyfill 1.0.28 en el navegador con la hoja aplanada en un solo archivo. xslt-polyfill falla si la hoja conserva sus 33 xsl:include, y xslt-processor 5.1.2 produjo una cadena distinta con la que el sello no verifica. En servidor, PHP con ext-xsl, que es lo que usa CfdiUtils, no se ve afectado.
¿CfdiUtils genera la cadena original sin XSLT?
No. Su DOMBuilder usa XSLTProcessor de la extensión ext-xsl de PHP con la hoja del SAT, y también ofrece GenkgoXslBuilder y SaxonbCliBuilder. Su documentación aclara que la hoja declara XSLT 2.0 pero la transformación con PHP da el resultado esperado. Como corre en el servidor, la retirada de XSLT en Chrome no le afecta.
¿Cómo pruebo hoy mi sitio como si ya fuera Chrome 158?
En Chrome estable, entra a chrome://flags/#xslt y ponlo en Disabled, o arranca el navegador con --disable-features=XSLT. Con eso typeof XSLTProcessor devuelve undefined y los XML con hoja XSL se muestran en crudo. Para detectar usos en producción, registra un ReportingObserver de tipo deprecation y filtra los reportes con id XSLT.