How to style a Hugo Atom feed with XSL

blenderob1 pts0 comments

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&rsquo;ll won&rsquo;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&rsquo;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&rsquo;ll won&rsquo;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&rsquo;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&rsquo;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&rsquo;s native RSS?<br>In this project I use Atom, not Hugo&rsquo;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&rsquo;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&rsquo;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....

atom feed sitemap hugo rsquo content

Related Articles