Chrome Removes XSLT: What Breaks in Sitemaps, Feeds and CFDI Invoice Tools
Table of Contents
Chrome 158 reaches Stable on 17 November 2026 and stops running XSLT: XSLTProcessor goes away, and so does processing of <?xml-stylesheet type="text/xsl"?>. For most sites that only changes the decoration. The sitemap or feed a human used to see as a table goes back to raw XML, with no error and no change to the file. Where real code breaks is a very Mexican corner of the web: invoice viewers and validators that build a CFDI’s original string in the browser with the tax authority’s XSLT stylesheet. There, the function throws ReferenceError: XSLTProcessor is not defined.
I tested both in Chrome 152 with the switch that turns XSLT off early. For the invoice part I went further: I generated the original string of a test invoice with five different engines and checked every result against the invoice’s own digital seal. A JavaScript XSLT library produces a string the seal does not verify against, the polyfill Chrome recommends fails if the stylesheet keeps its includes, and the official stylesheet as published does not even run in the browser.
The dates
Everything comes from the Chrome for Developers announcement and the Chrome Platform Status entry, with Stable dates checked against Chromium Dash:
| Milestone | Version | Date |
|---|---|---|
| Console warnings | Chrome 142 | 28 Oct 2025 |
| Official deprecation (console and Lighthouse) | Chrome 143 | 2 Dec 2025 |
| Origin Trial to keep using it starts | Chrome 152 | 25 Aug 2026 |
| Beta of the removing version | Chrome 158 | 28 Oct 2026 |
| XSLT stops working on Stable | Chrome 158 | 17 Nov 2026 |
Origin Trial and the XSLTEnabled enterprise policy end | Chrome 176 | 17 Aug 2027 |
One detail shortens the runway more than it looks: since Chrome 153 (8 September) a new Stable version ships every two weeks, not every four. Between today and 158 there are nine weeks and five releases.
What about Firefox and Safari? Same direction, no date. Mozilla took a positive position, and its tracking bug has already closed the console warning (Firefox 147) and an enterprise policy (Firefox 151), but disabling it by default is still open. WebKit said it is “cautiously supportive” and would probably wait for one engine to remove it fully. The HTML standard marked XSLT deprecated on 25 August 2026. In practice, what Chrome 158 does is what most of your users will see in November.
What breaks, what you see, how to fix it
| What you use | What happens in Chrome 158 | Fix |
|---|---|---|
XML sitemap with an XSL stylesheet (WordPress core, Yoast, Rank Math, xslURL in @astrojs/sitemap) | A human sees Chrome’s XML viewer instead of the table. No error, and the served file does not change | Nothing, if nobody looks at it. If it matters, drop the instruction or serve a separate HTML version |
RSS/Atom feed with an XSL stylesheet (a .xsl stylesheet in @astrojs/rss) | Opened in the browser, it shows raw XML. Feed readers do not notice | <link rel="alternate" type="application/rss+xml"> in your HTML, or the one-line polyfill |
JavaScript that calls new XSLTProcessor() | ReferenceError: XSLTProcessor is not defined | Move the transform to the server, or use a JS/WASM XSLT engine |
| CFDI viewer or validator that builds the original string in the browser | The string is never produced, so the seal cannot be verified | Server-side ext-xsl, xslt-polyfill with a flattened stylesheet, or SaxonJS (details below) |
<?xml-stylesheet type="text/css"?> | Nothing: still supported | — |
| XSLT on your server (PHP ext-xsl, Saxon, xsltproc) | Nothing: this only affects the browser | — |
Sitemaps and feeds: the decoration breaks, not the file
I opened the yoast.com sitemap index in Chrome 152 twice. With XSLT on, Chrome applies the stylesheet and renders a page titled “XML Sitemap” with its table. With --disable-features=XSLT, the same URL lands in Chrome’s XML viewer: the tree of <sitemap>, <loc> and <lastmod> with the xml-stylesheet instruction visible, no styling and no error. According to the announcement, Chrome also shows a banner linking to extensions when XSLT is off; in headless mode I could not see it.
This will hit a lot of people, because the XSL stylesheet ships by default across most of the ecosystem:
- WordPress core adds
<?xml-stylesheet type="text/xsl" href=".../wp-sitemap.xsl" ?>towp-sitemap.xmlsince 5.5. Ticket #65593 to remove it sits in the 7.2 milestone, with PR #12448 open and an alternative (#12584) that renders an HTML version on the server. As of 13 September neither is merged. - Yoast SEO still emits it in
trunk, which the changelog labels 28.5 for 15 September, and theyoast.comsitemap carries it today. - Rank Math still emits it in
trunk, and therankmath.comsitemap carries it today too. - Astro:
@astrojs/sitemaphas anxslURLoption, and@astrojs/rsssetstype="text/xsl"when you pass astylesheetending in.xsl. Neither is on by default.
What about Google?
I am not going to write “Googlebot is unaffected” as if Google had said so: I found no Google Search statement about XSLT and sitemaps. What is verifiable is that the change lives in the browser. Your server sends exactly the same bytes before and after 17 November, and Chrome’s announcement says it is not removing XML, only XSLT. A crawler that already read your sitemap without running the stylesheet keeps receiving the same file.
What to do with the sitemap
If no human ever opens your sitemap, do nothing. If people use it as a page, you have three paths:
-
Drop the instruction. It saves the request for the
.xsland the console warnings. WordPress core has official filters: per its own source, returning a falsy value means “the raw XML of the sitemap will be displayed”:// In an mu-plugin or functions.php add_filter( 'wp_sitemaps_stylesheet_url', '__return_false' ); add_filter( 'wp_sitemaps_stylesheet_index_url', '__return_false' ); // Yoast: the filter says "url", but it receives the full declaration add_filter( 'wpseo_stylesheet_url', '__return_empty_string' );In Rank Math the filter is per sitemap type (
rank_math/sitemap/{type}_stylesheet_url). In Astro, just removexslURLorstylesheet. -
Serve HTML separately. That is what WordPress PR #12584 proposes: the same transform, done on the server, published at a
.htmlURL. The.xmlstays for machines. -
CSS instead of XSL.
<?xml-stylesheet type="text/css"?>is still supported, but the WordPress ticket pointed out the limit: CSS can style the XML, but the<loc>entries do not become clickable links.
For feeds, Chrome recommends advertising the feed with <link rel="alternate"> in your HTML instead of a visible link to the .xml, or adding a single polyfill line to the feed.
CFDI invoices: where code actually breaks
Context for anyone who does not invoice in Mexico: a CFDI is the electronic invoice format mandated by Mexico’s tax authority, the SAT, and every one is a signed XML file. The signature (the Sello attribute) is not computed over the whole XML but over the original string (cadena original): the invoice’s values in a fixed order, separated by |. To verify a seal you need that string. To print the tax stamp’s own string on a PDF representation, you need it too.
The SAT publishes that sequence as an XSLT stylesheet: on its Annex 20 page, the “Secuencia de cadena original” document links to cadenaoriginal_4_0.xslt, last modified on 17 April 2026. The tax stamp has its own, cadenaoriginal_TFD_1_1.xslt. Three details of that file matter here:
- It declares
version="2.0", but browsers only implement XSLT 1.0. It still works with 1.0 processors: the CfdiUtils docs say so for PHP, which uses the same engine as Chrome (libxslt), and I confirmed it in the browser below. - It pulls in 33
xsl:includefiles (utilities and complements), all by absolute URL tohttp://www.sat.gob.mx. - The output is plain text (
method="text").
That is why building the string in the browser was tempting: XSLTProcessor came built in, and the XML never left the user’s machine. I looked for real code doing it and found two patterns:
- An npm library of Mexican validators and catalogues, with a release published at the end of August 2026, whose original-string function uses
new XSLTProcessor()when it detectswindowand falls back to a JavaScript engine in Node. In Chrome 158 the detection is still true (DOMParseris not going anywhere), so it takes the browser branch and throws theReferenceError. - A public JavaScript e-signature repository that loads
cadenaoriginal_3_3.xsltfrom a file input, stores it inlocalStorageand builds the string withXSLTProcessor.transformToDocumentto verify the seal of a CFDI 3.3 invoice.
The SAT stylesheet does not run as-is in the browser
I found this while testing and have not seen it documented anywhere. Hand XSLTProcessor the official stylesheet and Chrome refuses to load the 33 includes (the URL below is my test page):
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.
The result is null. So anyone building the string in the browser with XSLTProcessor today has to serve the stylesheet and its includes from their own origin, or flattened into a single file. That helps detection: the copy lives in your repository, so searching for cadenaoriginal finds it.
Why this site’s CFDI viewer does not break
The CFDI 4.0 XML viewer on this site does not use XSLT, and not by luck. It reads the XML with DOMParser, walks Comprobante, Emisor, Receptor, Conceptos, Impuestos and the TimbreFiscalDigital, translates the catalogue codes and checks the arithmetic. It does not build the original string or validate the seal, and the tool says so on screen. DOMParser is one of the APIs Chrome cites as the modern way to read XML, so it is not at risk. I grepped the whole repository: not a single XSLTProcessor or xml-stylesheet. The site’s sitemaps and RSS feed carry no stylesheet either. The open-source mx-fiscal-mcp-server, which reads CFDI invoices for AI agents, runs in Node on @xmldom/xmldom and does not use XSLT either.
The alternatives, tested against a real seal
Claiming “SaxonJS solves it” is easy. To check it, I took cfdi40-valid.xml from the CfdiUtils test suite, a CFDI 4.0 invoice signed with a SAT test certificate. I generated the string with each engine and accepted it only if the XML’s Sello verifies against it with the public key of its own Certificado:
# cert.pem = the base64 Certificado attribute, wrapped as PEM
# sello.bin = the Sello attribute, base64-decoded
openssl x509 -in cert.pem -pubkey -noout > pub.pem
openssl dgst -sha256 -verify pub.pem -signature sello.bin cadena.txt
# Verified OK → the string is correct
The reference was Saxon-HE 12.5 (Java, MPL 2.0) with the official stylesheet: Verified OK. The rest:
| Engine | Runs in | License | Result with the SAT stylesheet |
|---|---|---|---|
Native XSLTProcessor, official stylesheet | Browser | — | Fails: blocks cross-origin includes |
Native XSLTProcessor, local copy | Browser | — | Verified OK. Gone in Chrome 158 |
| xslt-polyfill 1.0.28, stylesheet flattened into one file | Browser (WASM) | BSD-3-Clause | Verified OK with native XSLT turned off |
| xslt-polyfill 1.0.28, copy with includes | Browser (WASM) | BSD-3-Clause | Fails: Error: Unknown mime type undefined |
SaxonJS 2.7 (saxon-js + xslt3), stylesheet compiled to SEF | Node; per its docs the SEF also runs in the browser | Free of charge, not open source | Verified OK in Node |
| xslt-processor 5.1.2 | Pure JS | LGPL-3.0 | Different string, seal does not verify |
The xslt-processor string starts with ||||| (five pipes instead of two). With the flattened stylesheet, the output shrinks to three pipes. It may be fine for other jobs, but it does not produce the SAT’s original string as-is.
The polyfill’s failure with includes is not a random bug; it is in its README. It resolves xsl:include with fetch(), which is asynchronous, while XSLTProcessor’s methods are synchronous, so they fail when the stylesheet has includes. Flattening fixes it: replace each <xsl:include> with the contents of the included stylesheet, in the same order. With the current 4.0 stylesheet you do not need to add namespaces, because the main root already declares every one the includes use.
Option 1: on the server (the boring one, which is why it wins)
PHP’s ext-xsl is still in the PHP manual, built on libxslt, and Chrome’s removal does not touch it. With CfdiUtils (eclipxe/cfdiutils, MIT, 3.0.4 from 11 September 2026):
use CfdiUtils\XmlResolver\XmlResolver;
use CfdiUtils\CadenaOrigen\DOMBuilder;
// XmlResolver downloads the SAT stylesheets and rewrites their dependencies to local copies
$resolver = new XmlResolver();
$location = $resolver->resolveCadenaOrigenLocation('4.0');
$cadena = (new DOMBuilder())->build($xmlContent, $location);
A correction to something that gets repeated: CfdiUtils does not build the string without XSLT. DOMBuilder is an ext-xsl XSLTProcessor; it also ships GenkgoXslBuilder (XSLT 2.0 in PHP) and SaxonbCliBuilder. Its author explains that building the string without XSLT is possible, but it means a lot of code to write and test, and that code has to change along with every complement’s specification.
From the browser, it becomes one request:
const res = await fetch('/api/cfdi/original-string', {
method: 'POST',
headers: { 'Content-Type': 'application/xml' },
body: xmlText,
});
const cadena = await res.text();
The cost is privacy: if your viewer promised “the XML never leaves your browser”, that stops being true and the copy has to change.
Option 2: stay in the browser with the polyfill
If local processing is the product, xslt-polyfill replaces XSLTProcessor with libxslt compiled to WebAssembly. It weighs about 1.4 MB minified. With a flattened stylesheet, your existing code does not change:
<script src="/vendor/xslt-polyfill.min.js"></script>
<script>
const xsl = new DOMParser().parseFromString(flattenedStylesheet, 'application/xml');
const cfdi = new DOMParser().parseFromString(xmlText, 'application/xml');
const p = new XSLTProcessor(); // the polyfill's, once the browser has no XSLT
p.importStylesheet(xsl);
const cadena = p.transformToFragment(cfdi, document).textContent;
</script>
While the browser still has native XSLT, the polyfill does not install itself. To try it in today’s Chrome, set window.xsltUsePolyfillAlways = true before loading the script.
Option 3: SaxonJS
SaxonJS implements the mandatory parts of XSLT 3.0, which is more than enough for a stylesheet declaring 2.0. The documented flow is to compile the stylesheet to a SEF file and run that:
npx xslt3 -xsl:cadenaoriginal_4_0_flat.xslt -export:cadena.sef.json -nogo
import SaxonJS from 'saxon-js';
const { principalResult: cadena } = SaxonJS.transform({
stylesheetFileName: 'cadena.sef.json',
sourceText: xmlText,
destination: 'serialized',
});
In my test, compiling took 2.4 s and the SEF weighed 3.65 MB, which matters if you ship it to the browser. Check the license before redistributing: Saxonica offers it free of charge, but it is not open source.
How to find out if you depend on XSLT
1. Search the code. Server-side hits in PHP, Java or .NET do not affect you; JavaScript that runs in the browser does:
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. Search what you built. The npm library above hides XSLTProcessor inside node_modules, so the grep above misses it. Run the same pattern over your build output (dist/, build/, .next/, public/build/).
3. Check what you serve.
curl -s https://yoursite.com/wp-sitemap.xml | head -c 300 | grep -o 'xml-stylesheet[^?]*'
curl -s https://yoursite.com/feed/ | head -c 300 | grep -o 'xml-stylesheet[^?]*'
4. Test with XSLT off. No need to wait for November or install Canary. In Stable Chrome, go to chrome://flags/#xslt and set it to Disabled, or start Chrome with --disable-features=XSLT (I verified it on Chrome 152). For a headless check:
chrome --headless=new --disable-features=XSLT --dump-dom \
'data:text/html,<script>document.write(typeof XSLTProcessor)</script>'
# undefined → this is how your app will look in Chrome 158
5. Let production tell you. Chrome reports the deprecation through the Reporting API with the id XSLT:
new ReportingObserver((reports) => {
for (const r of reports) {
if (r.body.id === 'XSLT') navigator.sendBeacon('/telemetry/xslt', r.body.sourceFile ?? '');
}
}, { types: ['deprecation'], buffered: true }).observe();
And open the console: every use prints “XSLTProcessor and XSLT Processing Instructions have been deprecated by all browsers”.
Who actually needs to act
You run a CFDI viewer, validator or PDF generator that builds the original string in the browser: you are the audience for this post. You have until 17 November. Pick server-side (and update your privacy copy) or the polyfill with a flattened stylesheet, and validate against a real seal as above before shipping.
You use XSLTProcessor for anything else on the front end: same deadline, same options. If you cannot migrate in time, the Origin Trial buys you until August 2027. If what uses XSLT is an XML document, there is no <head> for the meta tag, so the token goes in the Origin-Trial HTTP header.
You have a WordPress, Yoast, Rank Math or Astro site with a styled sitemap: nothing that matters for indexing breaks. Drop the instruction if the warning bothers you, or serve HTML separately if people read that sitemap.
Your company runs an internal app or device that serves XML with XSL and you cannot change it: the XSLTEnabled enterprise policy and the extension Chrome recommends are for you. The policy expires in August 2027. The extension applies the polyfill, so it does not depend on native XSLT.
You only build the string in PHP, Java or .NET on the server: not your problem. This is a browser change.
Further reading:
- Validate Mexican RFC, CURP and CFDI invoices from Claude: the open-source MCP server that reads CFDI 4.0 and validates RFC, CURP and CLABE, and the six details a regex gets wrong.
- Best MCP servers for SEO in 2026: if you audit sitemaps with agents, which tool does what.
- What breaks upgrading to Node 26: the same format for the runtime.
Frequently asked questions
When does Chrome remove XSLT support?
In Chrome 158, which reaches Stable on 17 November 2026. From that version XSLTProcessor and processing of xml-stylesheet instructions with type text/xsl stop working, except for sites enrolled in the Origin Trial and machines with the XSLTEnabled enterprise policy. Both exceptions end in Chrome 176, on 17 August 2027. The Chrome 158 beta ships on 28 October 2026.
Does my XML sitemap with an XSL stylesheet break in Chrome 158?
The file does not. What changes is how a person sees it in Chrome: instead of the table generated by the XSL stylesheet, the browser shows its raw XML viewer, with no error. I tested it in Chrome 152 with XSLT disabled on the yoast.com sitemap index. The server keeps sending exactly the same bytes. Google has not published anything specific about XSLT and sitemaps, but the change is in the browser, not in the XML document.
How do I remove the XSL stylesheet from a WordPress sitemap?
In WordPress core, return false from the wp_sitemaps_stylesheet_url and wp_sitemaps_stylesheet_index_url filters, for example with __return_false; the core source says that displays the raw XML. In Yoast, the wpseo_stylesheet_url filter receives the full declaration and you can return an empty string. In Rank Math the filter is per type: rank_math/sitemap/{type}_stylesheet_url. WordPress core has ticket 65593 open to remove the stylesheet in version 7.2.
Why do CFDI invoice viewers that use XSLT in the browser break?
Because the original string of a CFDI 4.0 invoice is defined by the SAT through the cadenaoriginal_4_0.xslt stylesheet, and the convenient way to run it in the browser was XSLTProcessor. In Chrome 158 that class no longer exists and the code throws ReferenceError: XSLTProcessor is not defined. Without the original string you cannot verify the invoice's digital seal. Reading the XML with DOMParser, as this site's CFDI viewer does, keeps working.
Which alternative correctly builds the SAT original string without browser XSLT?
I tested several against the seal of a CFDI 4.0 test invoice. Saxon-HE in Java, SaxonJS 2.7 with the stylesheet compiled to SEF, and xslt-polyfill 1.0.28 in the browser with the stylesheet flattened into one file all produced the correct string. xslt-polyfill fails if the stylesheet keeps its 33 xsl:include files, and xslt-processor 5.1.2 produced a different string that the seal does not verify against. On the server, PHP with ext-xsl, which is what CfdiUtils uses, is unaffected.
Does CfdiUtils build the CFDI original string without XSLT?
No. Its DOMBuilder uses XSLTProcessor from PHP's ext-xsl extension with the SAT stylesheet, and it also offers GenkgoXslBuilder and SaxonbCliBuilder. Its documentation notes that the stylesheet declares XSLT 2.0 but the PHP transform produces the expected result. Because it runs on the server, Chrome's XSLT removal does not affect it.
How can I test my site today as if it were Chrome 158?
In Stable Chrome, go to chrome://flags/#xslt and set it to Disabled, or start the browser with --disable-features=XSLT. With that, typeof XSLTProcessor returns undefined and XML files with an XSL stylesheet render raw. To catch uses in production, register a ReportingObserver for deprecation reports and filter the ones with id XSLT.