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:
- 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. - 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.
- Transformation – The module applies each configured stylesheet in order. If you have multiple
xslt_stylesheetdirectives, the output of one becomes the input for the next, forming a transformation pipeline. - Output – The transformed result is sent to the client with updated
Content-TypeandContent-Lengthheaders. The content type is determined by the stylesheet’sxsl:outputelement.
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_moduledirective 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:
- Content type mismatch: The response content type must match
xslt_types. If your response isapplication/xml, add:xslt_types text/xml application/xml; - No stylesheet configured: Ensure
xslt_stylesheetis present in the matching location block. -
Static file without type: When serving static XML files, NGINX determines the content type from the file extension. If you use
aliasortry_filesto serve a file without an.xmlextension, setdefault_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_paramvalue is not a valid XPath expression (usexslt_string_paraminstead)
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.
