Guide
Chrome stops running XSLT on November 17, 2026: fix your styled sitemap and RSS feed
By Aman Singh · Published · 8 min read
Some sitemaps and RSS feeds look like a tidy web page when you open them in a browser. That page is produced by XSLT: the XML file points at a stylesheet, and the browser transforms it. Chrome is removing that ability. From Chrome 158, released on November 17, 2026, those files will show as raw XML for anyone who opens them, according to Chrome's deprecation notice.
This guide shows how to tell whether your site is affected, and two ways to keep a readable page using a small Node.js build step. The scripts were run in October 2026 on Node.js 20, against test files and against this site's own sitemap.
Want your sitemap and feeds fixed for you? Have it fixed →
What changes, and when
Two things stop working: the XSLTProcessor JavaScript class, and the processing instruction <?xml-stylesheet type="text/xsl" ...?> at the top of an XML file. An xml-stylesheet line with type="text/css" keeps working, and XML itself is not going anywhere. Key dates from Chrome's timeline:
| Chrome | Date | What happens |
|---|---|---|
| 143 | Dec 2, 2025 | Deprecated; warnings in the console and Lighthouse |
| 152 | Aug 25, 2026 | Origin trial available to keep XSLT running past removal |
| 158 | Nov 17, 2026 | XSLT stops working on Stable, except for origin trial and enterprise policy users |
| 176 | Aug 17, 2027 | Origin trial and enterprise policy end; XSLT is off for everyone |
Other browsers set their own schedules, so check their release notes if your audience relies on them. The fixes below work in every browser.
Are you affected?
Only if a file you serve has an XSL stylesheet attached. Some SEO plugins, site generators and feed templates add one so the sitemap or feed looks friendly when opened. If your sitemap already shows as plain XML today, nothing changes for you. Sitemaps generated by Next.js from app/sitemap.ts, like this site's, have no stylesheet.
The people who notice are humans: a client who opens the sitemap and sees a wall of tags, or a reader who clicks your feed link. Expect a "did the site break?" email if you do nothing.
Check your files
For one file, a quick look at the top is enough:
curl -sL https://example.com/sitemap.xml | head -c 600 | grep -i "xml-stylesheet"To check several files, or to run it in CI, this script reads the start of each file and reports whether it relies on XSLT. It accepts URLs and local paths.
// Finds XML files that depend on browser XSLT, which Chrome stops running
// on Stable in version 158 (Nov 17, 2026).
// Usage: node check-xslt.mjs https://example.com/sitemap.xml https://example.com/feed.xml
// Local files work too: node check-xslt.mjs public/sitemap.xml
import { readFile } from "node:fs/promises";
async function load(target) {
if (/^https?:\/\//.test(target)) {
const res = await fetch(target, { redirect: "follow" });
return { status: res.status, text: await res.text() };
}
return { status: 200, text: await readFile(target, "utf8") };
}
// Only the document prolog matters: processing instructions before the root element.
function stylesheets(xml) {
const prolog = xml.slice(0, xml.search(/<[A-Za-z]/));
const found = [];
for (const [, attrs] of prolog.matchAll(/<\?xml-stylesheet\s+([^?]*)\?>/g)) {
const type = /type\s*=\s*["']([^"']+)["']/.exec(attrs)?.[1] ?? "";
const href = /href\s*=\s*["']([^"']+)["']/.exec(attrs)?.[1] ?? "";
const isXslt = /xsl/i.test(type) || /\.xslt?$/i.test(href);
found.push({ type, href, isXslt });
}
return found;
}
let affected = 0;
for (const target of process.argv.slice(2)) {
try {
const { status, text } = await load(target);
if (status >= 400) {
console.log(`ERROR ${target}: HTTP ${status}`);
continue;
}
const sheets = stylesheets(text);
const xslt = sheets.filter((s) => s.isXslt);
if (xslt.length) {
affected++;
console.log(`AFFECTED ${target}: uses XSLT (${xslt.map((s) => s.href).join(", ")})`);
} else if (sheets.length) {
console.log(`OK ${target}: stylesheet is CSS (${sheets.map((s) => s.href).join(", ")}), not affected`);
} else {
console.log(`OK ${target}: no stylesheet, shows as raw XML already`);
}
} catch (err) {
console.log(`ERROR ${target}: ${err.message}`);
}
}
process.exitCode = affected ? 1 : 0;node check-xslt.mjs https://example.com/sitemap.xml https://example.com/feed.xmlHere it is against a set of test files served locally, plus this site's live sitemap. It exits with code 1 when anything is affected, so a CI job fails until you fix it.
AFFECTED http://localhost:4321/sitemap.xml: uses XSLT (/sitemap.xsl)
AFFECTED http://localhost:4321/atom.xml: uses XSLT (/pretty-feed.xsl)
OK http://localhost:4321/feed.xml: stylesheet is CSS (/feed.css), not affected
ERROR http://localhost:4321/missing.xml: HTTP 404
OK https://www.codewares.in/sitemap.xml: no stylesheet, shows as raw XML alreadyFix 1: run your XSL at build time
If you like the current look, keep the XSL file and run it yourself, once, during the build. The result is a normal HTML page that works in any browser. Crawlers keep using sitemap.xml.
// Runs the XSL stylesheet your sitemap already uses, at build time in Node,
// and writes the result as a plain HTML page. Crawlers keep reading
// sitemap.xml; people get sitemap.html, which needs no browser XSLT.
// Usage: node prerender-sitemap.mjs public/sitemap.xml public/sitemap.html
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { XmlParser, Xslt } from "xslt-processor";
const [input = "public/sitemap.xml", output = "public/sitemap.html"] = process.argv.slice(2);
const xml = await readFile(input, "utf8");
const href = /<\?xml-stylesheet[^?]*href\s*=\s*["']([^"']+)["']/.exec(xml)?.[1];
if (!href) throw new Error(`${input} has no xml-stylesheet to render`);
// Resolve "/sitemap.xsl" against the folder that holds the XML file.
const xslPath = path.join(path.dirname(input), href.replace(/^\//, ""));
const xsl = await readFile(xslPath, "utf8");
const parser = new XmlParser();
const html = await new Xslt().xsltProcess(parser.xmlParse(xml), parser.xmlParse(xsl));
await writeFile(output, `<!DOCTYPE html>\n${html}\n`);
console.log(`wrote ${output} (${html.length} chars) using ${xslPath}`);Show the test stylesheet (sitemap.xsl)
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>XML Sitemap</title>
<style>body{font-family:sans-serif;margin:2rem}td{padding:4px 12px}</style>
</head>
<body>
<h1>XML Sitemap</h1>
<p><xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> URLs</p>
<table>
<tr><th>URL</th><th>Last modified</th></tr>
<xsl:for-each select="sitemap:urlset/sitemap:url">
<tr>
<td><a href="{sitemap:loc}"><xsl:value-of select="sitemap:loc"/></a></td>
<td><xsl:value-of select="sitemap:lastmod"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>npm install xslt-processor
node prerender-sitemap.mjs public/sitemap.xml public/sitemap.htmlwrote public/sitemap.html (784 chars) using public\sitemap.xsl
<!DOCTYPE html>
<html>
<head>
<title>XML Sitemap</title>
...
<body>
<h1>XML Sitemap</h1>
<p>3 URLs</p>
<table>
...
<tr>
<td>
<a href="https://example.com/pricing">https://example.com/pricing</a>
</td>
<td>2026-10-12</td>
</tr>Then make it part of the build, after your site generates the sitemap:
{
"scripts": {
"build": "your-site-build && node prerender-sitemap.mjs dist/sitemap.xml dist/sitemap.html"
}
}- Link people to the HTML page. Put
/sitemap.htmlin your footer, and keepsitemap.xmlin robots.txt and Search Console. - Remove the stylesheet line from the XML once the HTML page is live, or leave it; in Chrome 158 it simply does nothing.
- Know the limits.
xslt-processoris a JavaScript implementation of XSLT 1.0. It ran the test stylesheet above correctly, but a large plugin stylesheet may use features it handles differently. If yours does, use Fix 2, or thexsltproccommand-line tool from libxslt in your build environment.
Fix 2: build the HTML from the data
If the XSL file is complicated or you do not care about the old look, skip XSLT entirely. Parse the sitemap and write a simple page:
// No stylesheet at all: read the sitemap's data and write a simple HTML page.
// Use this when your XSL file is too complex to run outside a browser.
// Usage: node sitemap-to-html.mjs public/sitemap.xml public/sitemap.html
import { readFile, writeFile } from "node:fs/promises";
import { XMLParser } from "fast-xml-parser";
const [input = "public/sitemap.xml", output = "public/sitemap.html"] = process.argv.slice(2);
const data = new XMLParser().parse(await readFile(input, "utf8"));
const urls = [data.urlset?.url ?? []].flat(); // one <url> parses as an object, not an array
const escape = (s) =>
String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
const rows = urls
.map((u) => `<tr><td><a href="${escape(u.loc)}">${escape(u.loc)}</a></td><td>${escape(u.lastmod)}</td></tr>`)
.join("\n");
await writeFile(
output,
`<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>Sitemap</title></head>
<body><h1>Sitemap</h1><p>${urls.length} URLs</p>
<table><tr><th>URL</th><th>Last modified</th></tr>
${rows}
</table></body></html>
`,
);
console.log(`wrote ${output} with ${urls.length} URLs`);npm install fast-xml-parser
node sitemap-to-html.mjs public/sitemap.xml public/sitemap.htmlRun against the test sitemap it wrote a page with 3 URLs, and against this site's live sitemap a page with all 27 URLs it listed at the time. Values are escaped, so an odd URL cannot break the page.
Fix 3: the polyfill
Chrome's notice also points to a WebAssembly polyfill that brings XSLTProcessor back, and that can transform an XML page when you add one script line to it. It is the least work if you have many XML pages, at the cost of shipping a script to every visitor. I have not tested it here; follow the instructions in Chrome's notice.
RSS and Atom feeds
A styled feed exists mainly to tell visitors "this is a feed, paste it into your reader". Feed readers never used the stylesheet. Instead of restyling the XML, give people a page that explains the feed, and make sure readers can find it from your site with a link tag in the HTML head, as Chrome recommends:
<link rel="alternate" type="application/rss+xml"
title="Example blog" href="https://example.com/feed.xml">Point your "RSS" button at a short /subscribe page with the feed URL and a copy button, rather than at the raw XML.
Common questions
Will removing XSLT hurt my SEO?+
Can I keep XSLT working in Chrome after November 17, 2026?+
Does a CSS stylesheet on my feed stop working too?+
How do I find XSLT used by JavaScript on my pages?+
Need it fixed for you?
Small, fixed-scope jobs like this are what bug fixing for Next.js, React and Node.js is for. For a bigger rebuild, see full-stack web app development.
Next steps
Keep reading
Need a hand with this?
Tell me what you are working on. Free scoping call, fixed quote within 48 hours.
Book a call