How to style a Hugo Atom feed with XSL | Andre FrancaHow to style a Hugo Atom feed with XSLHow to style a Hugo Atom feed with XSL
Andre Franca<br>May 3, 2026<br>4 min read<br>If you open my rss feed url in your browser, you’ll won’t see raw XML content anymore, but a styled HTML page with the same header and footer as the main site, and a list of recent posts in between. This won’t affect feed readers, which will still see the original XML content. The styling is applied only when a human visits the feed URL in a browser, to make it more readable.<br>⚠️<br>Heads up!<br>This post is over
old and may contain outdated information, opinions, or broken links.
If you open my rss feed url in your browser, you’ll won’t see raw XML content anymore, but a styled HTML page with the same header and footer as the main site, and a list of recent posts in between.<br>This won’t affect feed readers, which will still see the original XML content. The styling is applied only when a human visits the feed URL in a browser, to make it more readable.<br>I’ve achieved something similiar years ago with Jekyll, but never really got around to doing it in Hugo until recently.<br>The idea is simple:<br>Generate an Atom feed in Hugo1.<br>Add xml-stylesheet to the XML.<br>Create XSL files in static/xsl/ for a human-friendly browser view.<br>Apply the same approach to sitemaps.<br>Why Atom instead of Hugo’s native RSS?<br>In this project I use Atom, not Hugo’s default RSS, for one practical reason: Atom supports an explicit updated field per entry.<br>That lets feeds expose both publish and update timestamps (published and updated), which is useful for edited posts.<br>1. Disable default RSS and configure Atom output [optional if you want to keep the RSS format]<br>Define explicit outputs and do not include rss.<br>Add a custom Atom output format with baseName = "feed"2.<br>In hugo.toml:<br>[outputs]<br>home = ['html', 'atom', 'sitemap', 'postssitemap', 'pagessitemap', 'tagssitemap']<br>term = ['html'] # [optional] include 'atom' for tags/categories if you want
[outputFormats]<br>[outputFormats.atom]<br>mediatype = "application/atom+xml"<br>baseName = "feed"
[mediaTypes]<br>[mediaTypes."application/atom+xml"]<br>suffixes = ["xml"]
With this, Hugo generates /feed.xml as the canonical feed endpoint (instead of the default RSS endpoint behavior).<br>2. Create Atom templates<br>Use:<br>layouts/index.atom.xml for the home feed<br>[optional] layouts/_default/list.atom.xml for list/term feeds (for example, tags)<br>Important header snippet for both:<br>{{- printf "\n" | safeHTML -}}<br>{{- printf "\n" ("/xsl/atom.xsl" | absURL) | safeHTML -}}<br>feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ site.Language.LanguageCode }}">
That xml-stylesheet instruction tells the browser to apply the XSL.<br>See full atom template structure in my repo.<br>3. Create the feed XSL (static/xsl/atom.xsl)<br>Current implementation:
version="1.0"<br>xmlns:xsl="http://www.w3.org/1999/XSL/Transform"<br>xmlns:atom="http://www.w3.org/2005/Atom"<br>exclude-result-prefixes="atom">
method="html" encoding="UTF-8" indent="yes"/>
match="/"><br>lang="en">
charset="utf-8"/><br>name="viewport" content="width=device-width, initial-scale=1"/>
Feed:<br>select="atom:feed/atom:title"/>
select="atom:feed/atom:entry">
href="{atom:link[@rel='alternate']/@href}"><br>select="atom:title"/>
Published: select="atom:published"/><br>| Updated: select="atom:updated"/>
test="normalize-space(atom:summary) != ''"><br>select="atom:summary"/>
See the full XSL template in my repo.<br>In this version, the feed XSL now mirrors site header/footer content (title, menus, and badges) instead of using a simplified custom top/bottom layout.<br>4. Style sitemaps with the same approach (static/xsl/sitemap.xsl)<br>In your sitemap template, add:<br>{{ printf "" | safeHTML }}<br>{{ printf "" ("/xsl/sitemap.xsl" | absURL) | safeHTML }}
I’ve split my sitemaps into multiple files (home, posts, pages, and tags) for better organization, but you can also keep a single sitemap if you prefer. Just make sure to include the xml-stylesheet instruction in each sitemap template and disableKinds = ["sitemap"] to your hugo config to prevent Hugo from generating the default sitemap.xml, which would not have the XSL instruction.<br>See my sitemap templates structure in my repo:<br>For home sitemap, posts sitemap, pages sitemap, and tags sitemap.<br>Then create static/xsl/sitemap.xsl to render:<br>sitemapindex as a list of sitemap files<br>urlset as a URL table<br>the same site-level header/footer structure used in the Atom XSL<br>See the full sitemap XSL template in my repo.<br>5. Common issues<br>404 on old feed path (/index.xml):<br>if you changed hugo’s default baseName = "feed" as I did above, the correct endpoint is /feed.xml.
XSL not applied in browser:<br>verify href in xml-stylesheet and confirm the file exists in static/xsl.
Broken content in feed:<br>usually caused by double-escaping in content.
Header/footer drift from site template:<br>if your site header/footer changes later, update both XSL files to keep visual parity....