Human Readable RSS Feeds With Pelican

Posted on Fri 21 October 2022 in Dispatches • 3 min read

A few months ago, I came across this post by Simone Silvestroni explaining how they implemented a styled and human-readable RSS feed for their Jekyll-powered blog. I really liked the idea and wanted to implement it on my own site for browsers that don’t automatically offer to subscribe to a given feed. Given that Silvestroni’s solution seemed to be straightforward enough, I set out to make the change for this Pelican-powered blog.

The problem I encountered almost immediately was that feed generation in Pelican is not done via templates, but rather through the feedgenerator module. As a result, the only way to alter the feed structure is to deploy a plugin. Unfortunately, I couldn’t find any such plugin already present within the ecosystem, so I set the project aside for the foreseeable future.

Until yesterday, when I decided to quickly write a plugin to do just that.

It’s called pelican-feed-stylesheet, and it enables you to specify a url for a XLS file that should be referenced as the XML stylesheet for your feed. It does this by following Pelican’s recommended approach of creating a new Writer class and using the get_writer signal to attach it to the publication process.

To use this plugin, first install it:

python -m pip install pelican-feed-stylesheet

Then add the following to the bottom of your pelicanconf.py or publishconf.py file:

FEED_STYLESHEET = "/YOUR_URL_HERE.xls"

PLUGINS.append("pelican_feed_stylesheet")

Now, of course, for this to work, you need to make your XLS file. This file is a presentation layer, so this work really belongs in your theme as opposed to the plugin itself, and you can use XPath queries to pull in relevant information from your feed so that you don’t have to duplicate data.

I used Silvestroni’s XLS file, as a starting point for mine, which is as good as any. My feed is structured slightly differently, so it required different XPath queries to retrieve information, and I customized the CSS so that the feed would be styled to match the rest of the site theme.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
        <title><xsl:value-of select="feed/title"/> RSS Feed</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
        <meta charset="UTF-8"/>
        <link rel="stylesheet" href="/theme/stylesheet/fontcss.css"/>
        <link rel="stylesheet" href="/theme/stylesheet/feed.css"/>
      </head>
      <body>
        <header>
          <h1><svg class="rss-logo" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 256 256"><defs><linearGradient x1=".085" y1=".085" x2=".915" y2=".915" id="a"><stop offset="0" stop-color="#E3702D"/><stop offset=".107" stop-color="#EA7D31"/><stop offset=".35" stop-color="#F69537"/><stop offset=".5" stop-color="#FB9E3A"/><stop offset=".702" stop-color="#EA7C31"/><stop offset=".887" stop-color="#DE642B"/><stop offset="1" stop-color="#D95B29"/></linearGradient></defs><rect width="256" height="256" rx="55" ry="55" fill="#CC5D15"/><rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/><rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#a)"/><circle cx="68" cy="189" r="24" fill="#FFF"/><path d="M160 213h-34a82 82 0 0 0-82-82V97a116 116 0 0 1 116 116z" fill="#FFF"/><path d="M184 213A140 140 0 0 0 44 73V38a175 175 0 0 1 175 175z" fill="#FFF"/></svg><xsl:value-of select="atom:feed/atom:title"/></h1>
          <div class="aboutfeeds">
            <p>This is a web feed, also known as an RSS feed. To know more read <a href="https://aboutfeeds.com/" target="_blank"><strong>About Feeds</strong> <small>↗︎</small></a> by Matt Webb.</p>
            <p><big>↘️ <strong>Subscribe</strong> by copying the URL into your RSS reader.</big></p>
          </div>
          <div class="head">
            <div class="avatar">
              <img src="/images/IMG_0002.jpg" height="100" width="100" alt="Avatar of Daniel Andrlik"/>
            </div>
            <div class="description">
              <p><xsl:value-of select="atom:feed/atom:subtitle"/></p>
              <p><a hreflang="en"><xsl:attribute name="href"><xsl:value-of select="atom:feed/atom:link/attribute::href"/></xsl:attribute>Visit Website &#x2192;</a></p>
            </div>
          </div>
        </header>
        <main>
        <p class="about"><img src="/favicon.png" alt="Ministry of Intrigue icon" width="24" height="24"/><a href="https://www.andrlik.org/about/">About Daniel Andrlik</a>.</p>
          <h2>📄 Recent Posts</h2>
          <xsl:for-each select="atom:feed/atom:entry">
            <article>
              <h3><a hreflang="en"><xsl:attribute name="href"><xsl:value-of select="atom:link/attribute::href"/></xsl:attribute><xsl:value-of select="atom:title"/></a></h3>
              <footer>Published: <time><xsl:value-of select="atom:published" /></time></footer>
            </article>
          </xsl:for-each>
        </main>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

You can also find a gist of both the XLS file and the CSS I’m using here.

The end result is that if someone clicks the link to my Atom feed and their browser doesn’t support immediately subscribing it gives them an easy to read HTML view as opposed to a whole page of XML for robots.

Screenshot showing a fully styled RSS feed

Screenshot of my current feed

There are some limitations to this approach. While the full text of an entry does appear in the feed for a feedreader, you can’t include the content or summary for each post in this listing as it will auto-escape any of the HTML entities in your text, which looks terrible.

I’m sure there’s a much more elegant way of doing this, but as a first attempt, it works well, and I’m happy with it.