Sustituir el XSLT del navegador: transformar XML en el servidor con PHP, Python o Node.js

Chrome dejará de aplicar las hojas XSL el 17 de noviembre de 2026. Si tu sitio sirve XML con una hoja de estilo XSL, la solución más duradera es ejecutar tú mismo el XSLT en el servidor y enviar HTML. Hemos ejecutado los ejemplos de Python y Node.js que verás más abajo; el de PHP está escrito según el manual de PHP y no lo hemos ejecutado.

¿Qué sustituye al XSLT del navegador en Chrome?

Hacer la transformación XSL en el servidor y enviar al navegador HTML normal. En PHP lo hace la clase XSLTProcessor de la extensión XSL; en Python, la clase XSLT de lxml; en Node.js, un paquete como saxon-js o xslt-processor. Todos reciben tu archivo XML y tu hoja XSL y devuelven una cadena HTML, que envías como text/html. Es la única opción en la que el resultado ya no depende de lo que admita el navegador.

XSLT deja de funcionar en Chrome 158, el 17 de noviembre de 2026. Los sitios inscritos en la prueba de origen (origin trial) y los navegadores gestionados con una política empresarial lo conservan hasta Chrome 176, el 17 de agosto de 2027. Nuestra página sobre el calendario de retirada de XSLT recoge qué páginas se ven afectadas y todas las opciones que tienen los visitantes.

El anuncio de Chrome describe estas opciones para quienes gestionan un sitio:

  1. Hacer el procesamiento XSLT en el servidor y enviar el HTML resultante.
  2. Añadir el script xslt-polyfill al documento XML para que la hoja de estilo vuelva a aplicarse en el navegador.
  3. Servir JSON en lugar de XML y mostrarlo con JavaScript.
  4. Ejecutar XSLT en la página con una biblioteca de JavaScript. Según el anuncio, la más grande con diferencia es la de Saxonica, que es totalmente compatible con XSLT 3.0.

El polyfill es el cambio más pequeño. Igual que una biblioteca de JavaScript, se ejecuta en el navegador de cada visitante. Transformar en el servidor exige más trabajo, pero el navegador recibe una página HTML normal, sin scripts ni extensiones. Este artículo se centra sobre todo en esa opción.

Si necesitas más tiempo, la prueba de origen de Chrome mantiene XSLT en tu sitio hasta Chrome 176, en agosto de 2027. Para ello, inscribe tu sitio en la prueba y añade su token a las respuestas de tus páginas. Como una página XML no tiene un head HTML donde poner una etiqueta meta, usa el encabezado de respuesta HTTP Origin-Trial, como se explica en la guía de Chrome sobre pruebas de origen. Si no puedes tocar nada del servidor, te queda el polyfill.

¿Debo convertir mi sitemap o mi feed RSS en HTML?

No. Los buscadores y los lectores de feeds los descargan de esa dirección y necesitan encontrar XML en ella.

La documentación de Google sobre sitemaps enumera los formatos que acepta: sitemaps XML, RSS, mRSS y Atom 1.0, y archivos de texto. El HTML no está entre ellos. Sustituir /sitemap.xml por HTML lo deja inservible para los buscadores a cambio de resolver un problema de visualización que solo afecta a las personas. Como explicamos en qué lee Google en un sitemap, el diseño no influye en la forma en que Google procesa un sitemap de WordPress.

Deja el XML donde está. Si alguien necesita una versión legible, publica el HTML en otra dirección y enlázala. Para los feeds hay además una opción más ligera: dar formato a feeds con CSS sigue funcionando en Chrome.

La transformación en el servidor tiene sentido para las páginas XML que solo leen personas, como un catálogo de productos con formato XSL.

¿Por qué mi XSLT devuelve una tabla vacía con un sitemap?

Porque los elementos de un sitemap pertenecen a un espacio de nombres, y la hoja tiene que referirse a ellos con un prefijo. Los ejemplos de este artículo usan dos archivos para generar una copia legible de un sitemap, que se sirve en su propia dirección (por ejemplo, /sitemap.html), mientras /sitemap.xml se queda como está. El XML es un sitemap corto:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc><lastmod>2026-10-14</lastmod></url>
  <url><loc>https://example.com/about/</loc></url>
</urlset>

La hoja de estilo, sitemap.xsl, lo convierte en una tabla HTML:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" exclude-result-prefixes="s"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9">
  <xsl:output method="html" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <html>
      <body>
        <h1>Sitemap</h1>
        <table>
          <xsl:for-each select="s:urlset/s:url">
            <tr>
              <td><xsl:value-of select="s:loc"/></td>
              <td><xsl:value-of select="s:lastmod"/></td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

XSLT 1.0 usa las expresiones de ruta de la recomendación XML Path Language (XPath) 1.0 del W3C, según la cual un nombre sin prefijo solo coincide con elementos sin espacio de nombres: el espacio de nombres por defecto del documento no cuenta. Por eso la hoja declara el prefijo s y escribe s:urlset/s:url. Con urlset/url, la tabla saldría vacía.

El atributo exclude-result-prefixes="s" evita que esa declaración de espacio de nombres acabe en el HTML. Sin él, en nuestra prueba con lxml, el resultado empezaba por <html xmlns:s="...">.

Con libxslt, la biblioteca que usan los ejemplos de PHP y Python, el resultado no lleva DOCTYPE: en nuestra prueba con lxml empezaba directamente por <html><body>. MDN explica que una página necesita un doctype como <!doctype html> para mostrarse en modo estándar y no en modo quirks. Añade a xsl:output el atributo doctype-system="about:legacy-compat", con la cadena heredada que el estándar HTML prevé para generadores como este. Con él, lxml escribió <!DOCTYPE html>. El atributo indent="yes", en cambio, solo permite al procesador añadir espacios en blanco, como haría un formateador XML: libxslt añadió saltos de línea entre la mayoría de los elementos, pero ninguna sangría.

¿Cómo ejecutar XSLT en PHP, Python o Node.js?

La extensión XSL de PHP y lxml en Python se apoyan en libxslt, que, según el anuncio de Chrome, es la misma biblioteca XSLT que usan los navegadores Chromium. Node.js necesita un paquete.

El anuncio de Chrome justifica la retirada por motivos de seguridad: bibliotecas como libxslt son código C/C++ complejo y antiguo, propenso a errores de seguridad de memoria. En tu servidor, tú decides qué se procesa: aplica solo tus hojas a tu XML, nunca una hoja enviada por un usuario, y mantén libxslt al día instalando las actualizaciones del sistema.

PHP

Usa la clase XSLTProcessor de la extensión XSL de PHP. Según el manual, la extensión viene con PHP y se activa con la opción --with-xsl al compilarlo, así que comprueba primero que php -m muestra xsl. En el PHP 8.3 que teníamos a mano, el de la imagen oficial de Docker de WordPress, no era así, y faltaba XSLTProcessor.

<?php
$xml = new DOMDocument();
$xml->load('sitemap.xml');

$xsl = new DOMDocument();
$xsl->load('sitemap.xsl');

$proc = new XSLTProcessor();
$proc->importStylesheet($xsl);

$html = $proc->transformToXml($xml);
if ($html === false) {
    http_response_code(500);
    exit;
}
header('Content-Type: text/html; charset=utf-8');
echo $html;

transformToXml devuelve el resultado como cadena, o false si hay un error, así que el código lo comprueba antes de enviar ningún encabezado. Como no teníamos la extensión XSL, este ejemplo se basa en el manual y no lo hemos ejecutado.

Python

Instala lxml con pip install lxml. Según su documentación, lxml admite XSLT 1.0 mediante libxml2 y libxslt.

from lxml import etree

doc = etree.parse("sitemap.xml")
transform = etree.XSLT(etree.parse("sitemap.xsl"))
result = transform(doc)
html = str(result)

El resultado se serializa como indica el elemento xsl:output de la hoja. Según la documentación de lxml, para obtener una cadena de Python con str(result) ese elemento debe declarar UTF-8, como hace el nuestro. Lo ejecutamos con Python 3.14.3 y lxml 6.0.2: obtuvimos la tabla HTML, con dos filas y una celda de fecha vacía para la segunda página.

Node.js

Node.js no incluye un procesador XSLT entre sus módulos integrados. Probamos dos paquetes.

SaxonJS. El paquete saxon-js, de Saxonica, es un procesador XSLT 3.0 para Node.js. Esa versión define un modo de compatibilidad para las hojas marcadas con version="1.0", y nuestra hoja funcionó sin cambios. SaxonJS.transform() necesita la hoja ya compilada en un archivo SEF, así que compila la tuya una vez con el comando xslt3:

npm install saxon-js xslt3
npx xslt3 -xsl:sitemap.xsl -export:sitemap.sef.json -nogo

Después, lanza la transformación desde un módulo ES, por ejemplo un archivo llamado transform.mjs:

import SaxonJS from 'saxon-js';

const output = await SaxonJS.transform({
  stylesheetFileName: 'sitemap.sef.json',
  sourceFileName: 'sitemap.xml',
  destination: 'serialized',
}, 'async');
const html = output.principalResult;

El README de saxon-js presenta SaxonJS como gratuito, aunque su licencia no es de código abierto.

xslt-processor. Este paquete está escrito en JavaScript puro, sin dependencias nativas. Su documentación muestra la llamada: se analizan los dos archivos con su clase XmlParser y se pasan al método xsltProcess de un objeto Xslt, que devuelve una promesa con el resultado. El README advierte de que faltan algunas funciones que exigen los estándares, y de que el paquete reconoce los elementos XSLT por su prefijo xsl: e ignora las declaraciones de espacios de nombres. Nuestro ejemplo funcionó, pero prueba tus propias hojas antes de confiar en este paquete.

¿Cuándo basta con el script xslt-polyfill?

Cuando puedes editar los archivos XML, pero no el código que los sirve. El polyfill aplica la hoja de estilo en el navegador, con JavaScript: el XSL no cambia y al XML solo hay que añadirle una línea de script (la encontrarás en nuestra página sobre el calendario de retirada de XSLT). El README de xslt-polyfill aporta tres detalles prácticos:

  • el script está publicado en npm como xslt-polyfill, y el archivo se sirve desde tu propio sitio;
  • solo aplica una hoja que el documento ya enlaza con una línea xml-stylesheet;
  • mientras el XSLT nativo está activo, el navegador transforma el documento antes de ejecutar cualquier script, así que prueba el polyfill con XSLT desactivado en chrome://flags/#xslt.

También importa el tipo de contenido. En nuestra prueba con XSLT desactivado, un elemento script colocado en un feed RSS se ejecutó cuando se enviaba como application/xml, pero no como application/rss+xml, porque en ese caso Chrome lo mostraba como texto plano. El anuncio de Chrome indica que añadir la línea a un feed no debería impedir que los lectores de feeds analicen el XML. La documentación de Google sobre sitemaps no dice nada al respecto, así que, si añades la línea a un sitemap, revisa después el informe correspondiente de Search Console.

La contrapartida es que cada visitante ejecuta un script para ver la página, mientras que con la transformación en el servidor recibe el HTML ya listo.

¿Qué Content-Type debe usar el HTML generado?

text/html, el tipo MIME que MDN indica para los documentos HTML. Envía Content-Type: text/html; charset=utf-8, como en el ejemplo de PHP. Así, la página funciona en cualquier navegador, con o sin XSLT.

Quien llegue a una página sin transformar puede usar una extensión: en Chrome Web Store están XSLT Polyfill, la oficial, y XMLNest, un visor XML para Chrome. En Por qué creé XMLNest cuento la historia de la extensión.

Fuentes

  1. Chrome for Developers: Removing XSLT for a more secure browser (consultada el 14 de octubre de 2026)
  2. Chrome for Developers: Get started with origin trials (consultada el 14 de octubre de 2026)
  3. XSLT Polyfill, README de mfreed7/xslt_polyfill en GitHub (consultada el 14 de octubre de 2026)
  4. W3C: XSL Transformations (XSLT) Version 1.0 (consultada el 14 de octubre de 2026)
  5. W3C: XML Path Language Version 1.0, Node Tests (consultada el 14 de octubre de 2026)
  6. W3C: XSL Transformations (XSLT) Version 3.0, XSLT 1.0 Compatibility Mode (consultada el 14 de octubre de 2026)
  7. HTML Standard: The DOCTYPE (consultada el 14 de octubre de 2026)
  8. MDN: Modo quirks y modo estándar (consultada el 14 de octubre de 2026)
  9. Manual de PHP: XSLTProcessor::transformToXml (consultada el 14 de octubre de 2026)
  10. Manual de PHP: extensión XSL, instalación (consultada el 14 de octubre de 2026)
  11. Manual de PHP: opciones de la línea de comandos (consultada el 14 de octubre de 2026)
  12. lxml: XSLT (consultada el 14 de octubre de 2026)
  13. Documentación de Node.js: índice de la API (consultada el 14 de octubre de 2026)
  14. npm: saxon-js (consultada el 14 de octubre de 2026)
  15. npm: xslt-processor (consultada el 14 de octubre de 2026)
  16. Google Search Central: Crear y enviar un sitemap (consultada el 14 de octubre de 2026)
  17. MDN: Tipos MIME comunes (consultada el 14 de octubre de 2026)
  18. Prueba de XMLNest : Pruebas realizadas por XMLNest el 14 de octubre de 2026 con los archivos de ejemplo de este artículo: Python 3.14.3 con lxml 6.0.2 (libxslt 1.1.39) produjo la tabla HTML de dos filas, que empezaba por <html><body> sin DOCTYPE, y por <!DOCTYPE html> al añadir doctype-system="about:legacy-compat"; Node.js 24.20.0 con saxon-js 2.7.0 y xslt3 2.7.0, y después con xslt-processor 5.1.2, produjo la misma tabla. En Chromium 156.0.8078.4 con la función XSLT desactivada, un elemento script del espacio de nombres XHTML colocado en un feed RSS se ejecutó cuando el feed se enviaba como application/xml, y no como application/rss+xml. PHP 8.3.35 de la imagen oficial de Docker wordpress:7.1.3-php8.3-apache no tenía la extensión XSL, así que el ejemplo de PHP sigue el manual de PHP y no se ejecutó (consultada el 14 de octubre de 2026)