Having an RSS feed for your site’s content is one of the most important things you can do as a citizen of the open web. If you want people to tune into your writing, it just makes sense to have one. Or better yet, you could have many RSS feeds, as I do—each from their own unique collection or topic(s). This site is built on top of Jekyll, a static-site generator. Each of the individual RSS feeds on my site are comprised of a .xml file which sets the feeds metadata and programattically generates the feed records, and a .xsl file which (very optionally) gives the feed a web view.
In the years I’ve had this site, I’ve had to tune and tweak the .xml file quite a few times to continue to improve the way it works, the content it exposes and the metadata that is associated with the feed. Here I’ll explain some of those tweaks and the thought process behind them.
RSS XML File
My site’s RSS feed .xml file. I have substituted many values in the block below with placeholders such as [FEED NAME].
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="[LINK TO .XSL FILE]" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:shark="https://shellsharks.com/feeds/shark-namespace">
<channel>
<title>{{ site.title | strip_html | xml_escape }} [FEED NAME]</title>
<description>{{ site.description | xml_escape }}</description>
<link>{{ site.url }}</link>
<language>en-us</language>
<managingEditor>[EMAIL] ([ALIAS])</managingEditor>
<webMaster>[EMAIL] ([ALIAS])</webMaster>
<atom:link href="{{ "[RELATIVE .XML FILE LOCATION]" | prepend: site.baseurl | prepend: site.url }}" rel="self" type="application/rss+xml"/>
<pubDate>{{ site.time | date_to_rfc822 }}</pubDate>
<lastBuildDate>{{ site.time | date_to_rfc822 }}</lastBuildDate>
<image>
<title>{{ site.title | strip_html | xml_escape }} [FEED NAME]</title>
<url>{{ site.url }}{{ site.avatar_url }}</url>
<link>{{ site.url }}</link>
</image>
<generator>Jekyll v{{ jekyll.version }}</generator>
{% assign sorted_posts = site.posts | sort: 'date' | reverse %}
{% for post in sorted_posts limit:100 %}
<item>
<title>{{ post.title | strip_html | xml_escape }}</title>
<shark:summary>{{ post.excerpt }}</shark:summary>
<description>{{ post.content | replace: 'href="/', 'href="https://[DOMAIN]/' | xml_escape }}</description>
<pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
<link>{{ post.url | prepend: site.baseurl | prepend: site.url }}</link>
<guid isPermaLink="true">{{ post.url | prepend: site.baseurl | prepend: site.url }}</guid>
{% for tag in post.tags %}
<category>{{ tag | xml_escape }}</category>
{% endfor %}
{% for cat in post.categories %}
<category>{{ cat | xml_escape }}</category>
{% endfor %}
</item>
{% endfor %}
</channel>
</rss>
Some important things to note from the .xml file…
You’ll notice the
sharknamespace established in the initial<rss>element. Within the RSS<generator>elemtn you will see a tag from this namespace,<shark:summary>. I explain what these are for here.There’s a lot of html stripping (
strip_html) and xml escaping (xml_escape) being done. This is because RSS clients, and general RSS spec gets very cranky about certain characters being included in feeds.For any
date-related tags, be sure to use RFC822-compatible (date_to_rfc822) date strings. Otherwise things break.Remember to sort your RSS feed output using
sort: 'date' | reverse.You can limit the number of posts that are included in your generated RSS feed by using the
limit:[COUNT]directive in your Liquid for loop (as I did here{% for post in sorted_posts limit:100 %}).For a full-content feed, use the following syntax in your
<generator><description>tag ⬇️. This uses thecontentvariable to pull in the full content, fixes relative links withreplace: 'href="/', 'href="https://[DOMAIN]/'and then does the usual xml_escape operation.
{{ post.content \| replace: 'href="/', 'href="https://[DOMAIN]/' \| xml_escape }}
You can limit the source collections an RSS feed is generated from using something like this:
{% assign pagesandnotes = site.posts \| concat: site.notes \| concat: site.scrolls %}You can filter the feed records by front matter tags using syntax such as this:
{% if post.tags contains 'fediverse' or post.tags contains 'social' or post.tags contains 'indieweb' %}Create an individual .xml and .xsl file for each RSS feed you’re interested in having! A lot of the code is reusable with minor tweaks to customize for each of your feed usecases.
RSS XSL File
My site’s RSS feed .xsl file. I have substituted many values in the block below with placeholders such as [RSS FEED TITLE]. XSL, or eXtensible Stylesheet Lanugage is a styling language for XML. The syntax is somewhat arcane, but easy enough to use to create something simple like I have to spruce up your raw .xml RSS feeds (or other .xml files like a Sitemap).
I won’t spend time explaining the xsl-related syntax, as I believe you can kinda reverse-engineer how they work by looking at what is below, but I will note that you can use Jekyll includes to further stylize and bring these xml files under the umbrella of your site’s larger thematic design.
<?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" xmlns:shark="https://shellsharks.com/feeds/shark-namespace">
<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="/rss/channel/title"/> [RSS FEED TITLE]</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="[LINK TO CSS FILE]"/>
</head>
<body>
<header>
<div class="sitetitle">
<a href="/"><img src="[HEADER IMAGE]" alt="[ALT TITLE]" height="30" /></a>
</div>
<div class="head">
<div class="description">
<p><xsl:value-of select="/rss/channel/description"/></p>
</div>
</div>
<div class="aboutfeeds">
<p>This is a web feed that can be viewed in the browser. <strong>Subscribe for free</strong> by copying the URL <code><mark>[.XML URL]</mark></code> into your RSS reader. Learn more <a href="https://aboutfeeds.com">about feeds here</a>. A full listing of this site's feeds can be found <a href="[LINK TO FEEDS INDEX PAGE]">here</a>.</p>
</div>
</header>
<main>
<h2><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 455.731 455.731" xml:space="preserve"><path style="fill:#f78422" d="M0 0h455.731v455.731H0z"/><path style="fill:#fff" d="M296.208 159.16C234.445 97.397 152.266 63.382 64.81 63.382v64.348c70.268 0 136.288 27.321 185.898 76.931 49.609 49.61 76.931 115.63 76.931 185.898h64.348c-.001-87.456-34.016-169.636-95.779-231.399z"/><path style="fill:#fff" d="M64.143 172.273v64.348c84.881 0 153.938 69.056 153.938 153.939h64.348c0-120.364-97.922-218.287-218.286-218.287z"/><circle style="fill:#fff" cx="109.833" cy="346.26" r="46.088"/></svg> Latest Posts</h2>
<xsl:for-each select="/rss/channel/item">
<article>
<h3><a hreflang="en"><xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute><xsl:value-of select="title"/></a></h3>
<footer id="pubdate">Posted: <time><xsl:value-of select="pubDate" /></time></footer>
<p id="postdescription"><xsl:value-of select="shark:summary"/></p>
</article>
</xsl:for-each>
</main>
<footer>
{% include footer.html %}
</footer>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
One thing to note is the use of the shark:summary identifier in the article postdescription element. This is to bring in a more human readable form of the post description rather than the full-content, w/ HTML that is set by default. I describe what this is and why I’m doin’ it here.
“Shark” Namespace
Upon popular request, I recently upgraded all of my RSS feeds to be “full content”. This means that full-length articles, including much of the styling and images would be available directly within folks RSS readers, for those who enjoy consuming directly in-client, rather than clicking out to read within the confines of my site. I personally think reading on-site is the better way to enjoy mine and other people’s content, but to each their own!
This modification however had a negative downstream effect on the human-readability of my RSS feeds on the web. Last year I gave my .xml RSS feeds a makeover by giving each a .xslt file, which styled them and made them viewable via the browser. When I modified the feeds to be “full-content”, this changed the view of each post record in the stylized feed to be a single-line glob of HTML—not very readable! I wanted to fix this, but needed a way to do it that would keep my RSS .xml files in-spec enough to be handled by RSS clients.
The RSS 2.0 Specification states that “A RSS feed may contain elements and attributes not described on this page, only if those elements and attributes are defined in a namespace.”. Alrightey then, I needed to figure out how to establish a namespace and put custom elements in it. Enter the “shark” namespace. After much trial-and-error, I finally figured out how to establish the namespace, initialize a variable element in the .xml file and then reference it in the associated .xslt file. This ultimately allowed me to keep what shows up in the actual RSS feed as the full-content, HTML-laden blob and what shows up in the web view as a clean, human-readable, excerpt of each post record. Cool!
There’s a lot I still don’t understand about .xml namespaces, and XSLT in general. For now, I think I’ve figured out the bare minimum I need to, to solve this single issue. One notable, unexplained quirk that I encountered was that I needed to put my new <shark:summary> element before the standard RSS <description> element or it just didn’t work. I’m sure there’s an explanation for that but I couldn’t tell you what it is (yet).
Ongoing RSS Validation Issues
Full-content RSS feeds are great, for those who want that level of detail directly in their RSS clients. But having that much stuff jammed into the rather finicky RSS protocol is a recipe for well, validation issues. So, I’m going to create a running list of current validation issues for my RSS feeds here, and (hopefully) document how I ultimately address them. It’s worth pointing out that my feeds appear to be working fine in the RSS clients I have tried out, despite these issues/errors. But, I’d still like to fix them where possible.
According to the W3C Feed Validation Service I have a number of validation issues…
Missing namespace for p (84 occurrences)
<shark:summary><p>Here’s another <em>Blog Questions Challenge</em>. ...
I have a bunch of individual linstances ofo the following “invalid characther in a URI” error… Invalid character in a URI: ]/whats-a-home-page
</description>
Missing namespace for div
<shark:summary><div class="poem">
In addition, interoperability with the widest range of feed readers could be improved by implementing the following recommendations.
Use of unknown namespace: https://shellsharks.com—(LOL! They don’t like my sketchy shark namespace 🦈.)
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:shark="htt ...
description should not contain relative URL references: #silliest-souvenir (1330 occurrences)
</description>
Invalid HTML: Unexpected start tag (a) implies end tag (a). (3 occurrences)
<description><p>My site is over <a href="https://shell ...
style attribute contains potentially dangerous content: color:var(–shellsharks-color);text-decoration:underline!important; (22 occurrences)
</shark:summary>
Non-html tag: svg (4 occurrences)
</shark:summary>
description should not contain script tag (4 occurrences)
</shark:summary>
description should not contain style tag (3 occurrences)
</shark:summary>
description should not contain onkeyup attribute (3 occurrences)
</shark:summary>
description should not contain onclick attribute (2 occurrences)
</shark:summary>
All kinda interesting huh?… Well I’ll work through these as I have time. In the meantime, if you know how to address any of these, or you’re seeing actual impact on a feed in your client, please reach out so I can fix them ASAP!