Skip to main content

NGINX

NGINX XSLT Module: Transform XML Responses into HTML

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

You have an API that returns XML. Maybe a legacy SOAP service, maybe an RSS feed from a third-party vendor, maybe an internal system that was built a decade ago and nobody wants to touch. Whatever the source, the result is the same: your users see a wall of angle brackets instead of a readable web page. This is exactly the problem the NGINX XSLT module was designed to solve.

The obvious fix is to write application code that parses the XML, applies a template, and renders HTML. But that means your backend now owns presentation logic. Every time the XML structure changes, you update the app. Every new endpoint needs its own transformation code. You are coupling things that should be separate.

The NGINX XSLT module takes a different approach. It applies XSLT stylesheets directly at the web server level, transforming XML responses into HTML before they ever reach the client. Your backend keeps returning XML. NGINX handles the transformation. No application code changes, no new dependencies in your stack, no deployment cycles just to tweak a table layout.

This is particularly valuable when you cannot modify the XML source at all – a third-party API, a vendor feed, or a legacy service nobody has the credentials to rebuild. If you have used the NGINX substitutions filter module for simple text replacements, the NGINX XSLT module takes content transformation to the next level with full XML-aware, structure-preserving processing.

How the NGINX XSLT Module Works

The XSLT module operates as a filter in the NGINX request processing pipeline. When a response passes through NGINX with a matching content type (by default, text/xml), the module intercepts the response body, parses it as XML using libxml2, and applies one or more XSLT stylesheets using libxslt.

Here is the processing flow:

  1. Header filter – NGINX checks whether the response content type matches xslt_types (default: text/xml) and whether any stylesheets are configured. If both conditions are met, the module prepares to buffer the entire response body.
  2. Body filter – As response chunks arrive, the module feeds them into an incremental XML parser. Once the full document is received, it validates the XML structure.
  3. Transformation – The module applies each configured stylesheet in order. If you have multiple xslt_stylesheet directives, the output of one becomes the input for the next, forming a transformation pipeline.
  4. Output – The transformed result is sent to the client with updated Content-Type and Content-Length headers. The content type is determined by the stylesheet’s xsl:output element.

Because the module must parse the entire XML document before applying the transformation, it buffers the full response body in memory. Keep this in mind when working with large XML documents – you may need to tune proxy_buffer_size if you are proxying upstream XML responses.

Installing the NGINX XSLT Module

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the module from the GetPageSpeed RPM repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-xslt

Then load the module by adding this line to the top of /etc/nginx/nginx.conf, before any other configuration blocks:

load_module modules/ngx_http_xslt_filter_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-xslt

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Verify the module is loaded by adding an XSLT directive to your configuration and testing:

nginx -t

If nginx -t passes with XSLT directives present, the module is active and ready to use.

Configuration Directives

The NGINX XSLT module provides six directives. All of them can be used within location blocks; several also work at the server and http levels.

xslt_stylesheet

Syntax: xslt_stylesheet file [param=value ...]
Context: location

Defines the path to an XSLT stylesheet that will be applied to XML responses. You can specify multiple xslt_stylesheet directives in the same location – they will be applied sequentially, forming a transformation pipeline.

Inline parameters can be passed directly with the stylesheet using param=value syntax. Parameter values can include NGINX variables.

xslt_stylesheet /etc/nginx/xslt/products.xsl;

xslt_param

Syntax: xslt_param name value
Context: http, server, location

Passes a parameter to all XSLT stylesheets in the current context. The value is evaluated as an XPath expression, which means it can reference XPath functions, numbers, and node sets. NGINX variables are expanded before the XPath evaluation.

xslt_param show_count 2;

Because the value is an XPath expression, passing a literal string requires explicit quoting within the XPath:

xslt_param name "'static-value'";

For most cases involving plain string values, use xslt_string_param instead – it handles quoting automatically.

xslt_string_param

Syntax: xslt_string_param name value
Context: http, server, location

Passes a string parameter to all XSLT stylesheets. Unlike xslt_param, the value is always treated as a plain string, not an XPath expression. This is the safer choice when passing NGINX variables or literal text.

xslt_string_param title "Our Product Catalog";
xslt_string_param currency $arg_currency;

xslt_types

Syntax: xslt_types mime-type ...
Default: text/xml
Context: http, server, location

Specifies which MIME types trigger XSLT processing. By default, only text/xml responses are transformed. If your upstream returns application/xml or another XML-based content type, add it here:

xslt_types text/xml application/xml application/rss+xml;

xslt_last_modified

Syntax: xslt_last_modified on | off
Default: off
Context: http, server, location

Controls whether the Last-Modified and ETag response headers are preserved after transformation. By default, the module removes both headers because the transformation changes the response content, making the original modification time inaccurate.

When enabled, the original Last-Modified header is kept and a weak ETag is generated. This is useful when the source XML changes infrequently, and you want browsers and proxies to cache the transformed result:

xslt_last_modified on;

xml_entities

Syntax: xml_entities file
Context: http, server, location

Specifies a DTD file that defines character entities used in the XML being processed. This is needed when your XML documents reference custom entities beyond the standard XML set (&, <, >, ", ').

xml_entities /etc/nginx/xslt/entities.dtd;

Practical Examples

Example 1: Transforming a Product Catalog

This is the most straightforward use case for the NGINX XSLT module: serving a static XML file as a formatted HTML page.

The XML data (/var/www/data/products.xml):

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
  <product id="1">
    <name>Widget Pro</name>
    <price currency="USD">29.99</price>
    <description>A professional-grade widget.</description>
  </product>
  <product id="2">
    <name>Gadget Plus</name>
    <price currency="USD">49.99</price>
    <description>Enhanced gadget with premium features.</description>
  </product>
</catalog>

The XSLT stylesheet (/etc/nginx/xslt/catalog.xsl):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="UTF-8" indent="yes"
              media-type="text/html"/>
  <xsl:template match="/catalog">
    <html>
      <head><title>Product Catalog</title></head>
      <body>
        <h1>Product Catalog</h1>
        <table border="1" cellpadding="8">
          <tr><th>ID</th><th>Name</th><th>Price</th><th>Description</th></tr>
          <xsl:for-each select="product">
            <tr>
              <td><xsl:value-of select="@id"/></td>
              <td><xsl:value-of select="name"/></td>
              <td>
                <xsl:value-of select="price"/>
                <xsl:text> </xsl:text>
                <xsl:value-of select="price/@currency"/>
              </td>
              <td><xsl:value-of select="description"/></td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

The NGINX configuration:

location = /catalog {
    default_type text/xml;
    xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
    alias /var/www/data/products.xml;
}

A request to /catalog returns a fully rendered HTML table instead of raw XML. The default_type text/xml is important here: without it, NGINX would serve the file with a generic MIME type, and the XSLT filter would not activate.

Example 2: Rendering RSS Feeds as HTML Pages

RSS and Atom feeds are XML documents. You can use the NGINX XSLT module to transform them into styled web pages so visitors can browse your feed content without a dedicated RSS reader.

The stylesheet (/etc/nginx/xslt/rss2html.xsl):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="UTF-8" indent="yes"
              media-type="text/html"/>
  <xsl:template match="/rss/channel">
    <html>
      <head><title><xsl:value-of select="title"/></title></head>
      <body>
        <h1><xsl:value-of select="title"/></h1>
        <p><xsl:value-of select="description"/></p>
        <xsl:for-each select="item">
          <article>
            <h2><a href="{link}"><xsl:value-of select="title"/></a></h2>
            <p><xsl:value-of select="pubDate"/></p>
            <p><xsl:value-of select="description"/></p>
          </article>
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

The NGINX configuration:

location = /feed {
    default_type text/xml;
    xslt_stylesheet /etc/nginx/xslt/rss2html.xsl;
    alias /var/www/data/feed.xml;
}

Example 3: Transforming an Upstream XML API

One of the most powerful use cases for the NGINX XSLT module is placing NGINX as a reverse proxy in front of an XML-based backend service and transforming the response before it reaches the client:

location /products {
    proxy_pass http://backend-api/api/products;
    xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
}

The upstream service returns raw XML with content type text/xml. NGINX applies the stylesheet and serves HTML to the client. The backend does not need to know about the presentation layer at all.

If the upstream returns application/xml instead of text/xml, add the xslt_types directive:

location /products {
    proxy_pass http://backend-api/api/products;
    xslt_types text/xml application/xml;
    xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
}

Example 4: Dynamic Filtering with Parameters

Parameters let you customize the transformation based on request data. This example filters products by currency using a query string parameter:

The stylesheet (/etc/nginx/xslt/catalog-filter.xsl):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="UTF-8" indent="yes"
              media-type="text/html"/>
  <xsl:param name="title" select="'Product Catalog'"/>
  <xsl:param name="currency_filter"/>
  <xsl:template match="/catalog">
    <html>
      <head><title><xsl:value-of select="$title"/></title></head>
      <body>
        <h1><xsl:value-of select="$title"/></h1>
        <table border="1" cellpadding="8">
          <tr><th>Name</th><th>Price</th></tr>
          <xsl:choose>
            <xsl:when test="$currency_filter != ''">
              <xsl:for-each select="product[price/@currency=$currency_filter]">
                <tr>
                  <td><xsl:value-of select="name"/></td>
                  <td>
                    <xsl:value-of select="price"/>
                    <xsl:text> </xsl:text>
                    <xsl:value-of select="price/@currency"/>
                  </td>
                </tr>
              </xsl:for-each>
            </xsl:when>
            <xsl:otherwise>
              <xsl:for-each select="product">
                <tr>
                  <td><xsl:value-of select="name"/></td>
                  <td>
                    <xsl:value-of select="price"/>
                    <xsl:text> </xsl:text>
                    <xsl:value-of select="price/@currency"/>
                  </td>
                </tr>
              </xsl:for-each>
            </xsl:otherwise>
          </xsl:choose>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

The NGINX configuration:

location = /catalog {
    default_type text/xml;
    xslt_stylesheet /etc/nginx/xslt/catalog-filter.xsl;
    xslt_string_param title "Our Premium Products";
    xslt_string_param currency_filter $arg_currency;
    alias /var/www/data/products.xml;
}

Now you can filter by currency using query parameters:

  • /catalog – shows all products
  • /catalog?currency=USD – shows only USD products
  • /catalog?currency=EUR – shows only EUR products

Use xslt_string_param when passing NGINX variables or literal text. Use xslt_param only when you need XPath expression evaluation, such as passing numeric values or XPath functions.

Example 5: Chained Stylesheets (Transformation Pipeline)

The NGINX XSLT module supports applying multiple stylesheets in sequence. The output of one becomes the input for the next. This is useful for separating concerns – for example, one stylesheet normalizes the XML structure, and another renders it as HTML:

location = /catalog {
    default_type text/xml;
    xslt_stylesheet /etc/nginx/xslt/normalize.xsl;
    xslt_stylesheet /etc/nginx/xslt/render.xsl;
    alias /var/www/data/products.xml;
}

The first stylesheet transforms the raw XML into an intermediate format. The second stylesheet converts that intermediate format into the final HTML output. This pattern keeps each stylesheet focused and reusable.

Testing Your Configuration

After configuring the NGINX XSLT module, verify everything works:

1. Check the configuration syntax:

nginx -t

If you see unknown directive "xslt_stylesheet", the module is not loaded. Make sure load_module modules/ngx_http_xslt_filter_module.so; appears at the top of nginx.conf (RHEL-based systems).

2. Reload NGINX:

sudo systemctl reload nginx

3. Test the transformation with curl:

curl -D- http://localhost/catalog

You should see:
Content-Type: text/html; charset=UTF-8 (not text/xml)
– HTML content in the response body (not raw XML)

If you still see raw XML, check these common issues:
– The source content type does not match xslt_types (default: text/xml)
– The stylesheet path is incorrect or the file is not readable by the NGINX worker process
– The XML document is malformed

Performance Considerations

The NGINX XSLT module processes every matching response on every request. Here are some points to keep in mind:

Memory usage: The module buffers the entire XML response in memory before parsing. For large XML documents (10+ MB), this can significantly increase memory consumption per request. If you regularly serve large XML files, consider splitting them or transforming them offline.

CPU overhead: XSLT transformation is CPU-intensive, especially with complex stylesheets or large documents. For high-traffic endpoints, consider caching the transformed output using NGINX’s proxy_cache or fastcgi_cache:

proxy_cache_path /var/cache/nginx/xslt levels=1:2
                 keys_zone=xslt_cache:10m max_size=100m;

server {
    location /products {
        proxy_cache xslt_cache;
        proxy_cache_valid 200 5m;
        proxy_pass http://backend-api/api/products;
        xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
    }
}

Stylesheet caching: NGINX parses and compiles XSLT stylesheets at configuration load time, not on every request. This means the stylesheet itself is compiled once and reused across all requests. There is no per-request overhead for stylesheet parsing.

DTD caching: Similarly, DTD files specified with xml_entities are loaded and cached at startup.

Last-Modified and caching: Enable xslt_last_modified on when the source XML changes infrequently. This allows browsers and CDNs to cache the transformed response and use conditional requests (If-Modified-Since), reducing unnecessary re-transformations.

Security Best Practices

Restrict Stylesheet Access

Store your XSLT stylesheets outside the web root to prevent direct download:

# Good: stylesheets in a separate directory
/etc/nginx/xslt/catalog.xsl

# Bad: stylesheets inside the web root
/var/www/html/templates/catalog.xsl

Set restrictive file permissions:

chmod 640 /etc/nginx/xslt/*.xsl
chown root:nginx /etc/nginx/xslt/*.xsl

Validate Input XML

The NGINX XSLT module returns HTTP 500 if the XML document is malformed. In a proxy setup, a misbehaving upstream could trigger 500 errors by returning invalid XML. Consider adding error handling:

location /products {
    proxy_pass http://backend-api/api/products;
    xslt_stylesheet /etc/nginx/xslt/catalog.xsl;
    proxy_intercept_errors on;
    error_page 500 /fallback.html;
}

Avoid User-Controlled Stylesheet Paths

Never construct stylesheet paths from user input. The stylesheet path must be a static value in the NGINX configuration:

# DANGEROUS - never do this
# xslt_stylesheet /var/www/templates/$arg_template;

# SAFE - use a fixed path
xslt_stylesheet /etc/nginx/xslt/catalog.xsl;

Be Cautious with xslt_param

When using xslt_param (XPath expression mode), be aware that user-supplied values are evaluated as XPath. While XSLT 1.0 has limited capabilities compared to XSLT 2.0+, prefer xslt_string_param for any values derived from user input to avoid unexpected XPath evaluation.

Troubleshooting

“unknown directive xslt_stylesheet”

The module is not loaded. Add the load_module directive at the top of nginx.conf:

load_module modules/ngx_http_xslt_filter_module.so;

XML response passes through without transformation

Check three things:

  1. Content type mismatch: The response content type must match xslt_types. If your response is application/xml, add:
    xslt_types text/xml application/xml;
    
  2. No stylesheet configured: Ensure xslt_stylesheet is present in the matching location block.

  3. Static file without type: When serving static XML files, NGINX determines the content type from the file extension. If you use alias or try_files to serve a file without an .xml extension, set default_type text/xml; in the location.

HTTP 500 on transformation

This usually means the XML document is malformed. Check the NGINX error log:

tail -f /var/log/nginx/error.log

Look for messages like xsltApplyStylesheet() failed or XML parser errors. Common causes:

  • The upstream returned invalid XML (unclosed tags, encoding issues)
  • The stylesheet references nodes that do not exist in the document
  • An xslt_param value is not a valid XPath expression (use xslt_string_param instead)

Incorrect output encoding

The output encoding is determined by the encoding attribute in the stylesheet’s xsl:output element:

<xsl:output method="html" encoding="UTF-8"/>

If you omit this, the default encoding depends on the output method. Always specify the encoding explicitly in your stylesheet.

When to Use the NGINX XSLT Module

The NGINX XSLT module is the right choice when:

  • You need to transform XML API responses to HTML at the reverse proxy layer
  • You want to render RSS/Atom feeds as styled web pages
  • You have legacy SOAP services and want to present their data in a modern format
  • You need a transformation pipeline that keeps presentation logic out of your application code
  • You want to apply the same transformation to multiple upstream services without duplicating code

For simpler text-based modifications (replacing strings, injecting headers), the NGINX substitutions filter module may be a better fit. The NGINX XSLT module is specifically designed for structured XML-to-XML or XML-to-HTML transformations.

For new applications that control both the backend and frontend, generating HTML or JSON directly in the application is usually simpler. The NGINX XSLT module excels in scenarios where you cannot or do not want to modify the XML source.

Conclusion

The NGINX XSLT module brings server-side XML transformation directly into your web server configuration. Whether you are rendering RSS feeds, transforming API responses, or bridging legacy XML services to modern HTML frontends, XSLT processing at the NGINX level keeps your backend clean and your transformation logic centralized.

The module is available as a prebuilt dynamic module from the GetPageSpeed repository for RHEL-based distributions. Install it with dnf install nginx-module-xslt, write your stylesheets, and let NGINX handle the rest.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.