Lets create a plaintext RSS feed with 11ty
Before writing this blog post I had 3 types of RSS feeds for my blog posts:
In this post, I'm going to add a 4th type: Plaintext.
Origin
A few months ago I was browsing my contacts and past colleagues looking for people to follow for my blogroll page. In doing so I just so happened to remember Terence Eden who I worked with at Government Digital Service (GDS). Terence (or edent, as he is known on most social networks), is a prolific blogger! I honestly don't know where he gets the time to blog so much and work, eat, sleep, blink, and breathe! While looking at the source code of his blog for his RSS feed, I just so happened to notice he had a plaintext version of his RSS feed. At the time it was responding with a 404, so I let him know on Mastodon, and he fixed it in a matter of minutes. Once fixed, I could see how it was formatted, and I set it as a personal challenge to replicate this feature on this blog (once all other more important functionality had been added). Now you may be asking, why would you need a plaintext version? So, let's answer that question first!
Why add a plaintext RSS feed?
Other than a personal challenge, what are the reasons for adding a plaintext version of your RSS feed?
Greater syndication
Now I have no idea how people like to read my content (I'm just very glad they do!). So if there's any way I can make their lives easier by adding multiple formats of the same RSS feed, then I'm happy to do it. After all, I'm always looking for ways to make this blog more open and inclusive. Even if it's only for a handful of readers, then it's worth adding the functionality, as it really is "set it and forget it" once deployed!
Command-line feed readers
I had no idea until I started researching the topic that there are users who use their terminal window as an RSS feed reader. For this they use programs like:
- newsboat: "An RSS/Atom feed reader for text terminals" — It looks to have a very active repository on GitHub with 217 forks and 3,100 stars!
- Liferea: "Liferea (Linux Feed Reader), a newsreader for GTK/GNOME" — Again an active GitHub repository with 129 forks and 830+ stars. I also noticed the project is over 20 years old (looking at some of the commits)!
- RSS Guard: "Feed reader (podcast player and also Gemini protocol client) which supports RSS/ATOM/JSON and many web-based feed services." — another RSS client with an active GitHub repository with 130 forks and 1,800 stars. It's not surprising really considering the number of operating systems it supports!
So if you were in any doubt that plaintext RSS feed users don't exist, then I think the numbers above prove otherwise! (before you get angry, I do realise that not all these three programs users will be plaintext users!).
Privacy and Security Concerns
You only have to look at the Web Almanac 2024's chapters on Privacy and Security to realise how important these two topics are on the modern web. So it's completely understandable that RSS users are concerned about this when they subscribe to RSS feeds. Thankfully, this is where plaintext RSS feeds excel. By using plaintext RSS feeds, users can avoid loading external content such as images or scripts, thus reducing the risk of tracking and also enhancing their privacy.
Accessibility
I've seen accessibility brought up as a plus for plaintext RSS feeds, but honestly, I’m a little sceptical. Sure, a screen reader can read the content easily enough, but when it comes to navigation and overall user experience for someone with accessibility needs, navigating a plaintext feed seems like it’d be a step backwards. Wouldn’t something like HTML or XML, with its semantic markup and built-in navigation, be a way better option? That said, I’m totally open to being wrong here—if I’ve got this all incorrect, feel free to let me know.
Data Efficiency
This one really doesn't need much discussion, really. You can't get much more minimal in terms of bandwidth usage than a plaintext RSS feed. No images, no JavaScript, no markup, just pure content. It certainly makes for an excellent source of content for users with limited internet access or those operating in low-bandwidth environments.
Customisation and Integration
Tools like RSS Fulltext Proxy and Five Filters allow for text extraction from any website to convert partial feeds into full-text. This allows for integration into any feed reader, without plugins, or additional configuration. These are exceptional options if you want to integrate a particular feed into one of your workflows.
Reliability and Readability
You literally can't get a more robust format than plaintext. It is universally supported and small. And in terms of the resulting readability, there's no cookie banners, adverts, or newsletter signup forms that pop up halfway through an article. Thankfully, none of these annoyances are possible in plaintext, so you can consume all the content without interruption. It reminds me of how the World Wide Web used to be before it was commercialised beyond recognition! Take a read of the world's first website, and you get an idea of what Sir Tim Berners-Lee had in mind when he invented it in 1989 at CERN.
The web was originally conceived and developed to meet the demand for automated information-sharing between scientists in universities and institutes around the world.
The WorldWideWeb (W3) is a wide-area hypermedia information retrieval initiative aiming to give universal access to a large universe of documents.
Anyway, enough of the "why's" let's crack on with the 11ty implementation!
The code
eleventy.config.js
First, let's modify the 11ty config file:
Here we are telling 11ty to process .txt files as template files.
eleventyConfig.addTemplateFormats("txt");Next we need to configure 11ty and tell it how to handle .txt files with a custom handler.
eleventyConfig.addExtension("txt", {
outputFileExtension: "txt",
compile: async function (inputContent) {
return async (data) => inputContent;
},
});The code is fairly self-explanatory outputFileExtension is obvious, the resulting output will have a .txt extension. The compile function is a simple pass-through. It takes the txt input and returns the original content unchanged.
feed.txt.njk
Next up is the template for the plaintext feed output. I'm using Nunjucks as that's the main templating language I use across this blog, but I'm sure you'd be able to any other templating language 11ty supports too. This file sits in my /content/feed/ directory, so 11ty will process it as a njk file in the output.
---
permalink: feed/feed.txt
eleventyComputed:
layout: null
---
# {{ metadata.title }} - {{ metadata.author.name }} - {{ metadata.description }}
## {{ metadata.fulldescription }}
URL: {{ metadata.url }}
{% for post in collections.posts | reverse -%}
{% if loop.index0 < 10 -%}
--- Start: {{ post.data.title | safe }}
Published on: {{ post.date | readableDate }}
{{ metadata.url }}{{ post.url }}
Main Content:
{{ post.templateContent | striptags(true) | decodeHtmlEntities | safe }}
{% if not loop.last %}
--- End: {{ post.data.title | safe }}
{% endif -%}
{% endif -%}
{% endfor -%}
A Gist for this template is here, if you find that easier to read.
There's a fair amount going on in this template file, but it is essentially:
- Setting where I want the feed to sit in my final site output (
permalink) - Overriding the default layout dynamically during the build process (
layout: null) - Populating the top of the feed with my basic blog metadata.
- Looping through all by blog posts in reverse (newest first).
- Limiting the number of posts in the feed to 10.
- Outputting the parts of each post I want in the feed using Nunjucks and cleaning up the output using various filters, which I will go through next.
Code Notes:
- You may be looking at this template and wondering what is going on with all the space between the logic. Well, since this template is outputting plaintext, this is all intentional spacing to make the final output more readable.
- I'm using
loop.index0which is the current iteration of the loop but 0 indexed because it was the only way that seemed to work for me to limit the number of posts. I think I must have a weirdcollections.postssetup somewhere because regardless of what I tried, nothing worked. Likewise, I believe the nativeslice(0, 10)should do the same thing, but for me, the loop stopped working altogether. I tried debugging it for a couple of hours and eventually stuck with theloop.index0setup because it actually worked. However, if anyone has any ideas on why the nativeslice()function on acollections.postsdoesn't work, please let me know! I'd love to get to the bottom of what the issue is! - You may notice my use of the
-%}in the template, this is me telling Nunjucks to trim any whitespace (spaces, tabs, newlines) directly following the tag. This was required because it was adding numerous new lines in the resulting output. It's when I discover little intricacies in Nunjucks like this, that I realise what a versatile templating language it actually is!
filters
Most of the filters used in the above template are all pretty standard to the Nunjucks language:
I only added one custom 11ty filter for this functionality.
decodeHtmlEntities
For some reason, I had some very odd character encoding issues, and the only way I could resolve the issue was to create a custom 11ty filter to search and replace them throughout the content. I looked through the default Nunjucks filters for a solution, but filters like escape and forceescape didn't work. I could have probably used Nunjucks native replace filter, but that would have cluttered up the template, so decided to move it to an 11ty filter instead:
// clean up the HTML entities in the RSS feed text
eleventyConfig.addFilter("decodeHtmlEntities", function(text) {
return text.replace(/&([^;]+);/g, function(match, entity) {
const entities = {
'amp': '&',
'apos': "'",
'lt': '<',
'gt': '>',
'quot': '"',
'nbsp': ' '
};
// Handle numeric entities
if (entity[0] === '#') {
const code = entity[1] === 'x'
? parseInt(entity.slice(2), 16)
: parseInt(entity.slice(1), 10);
return String.fromCharCode(code);
}
return entities[entity] || match;
});
});It's basically just a fancy way to search and replace for multiple encoding issues it finds in the plaintext output.
Just to be clear, I wrote a really clunky version that did the same thing as this first. Basically, a chain of text.replaceAll('&amp;', '&').replaceAll('&apos;', "'")... and so on, I think you get the idea.
It worked fine, but then I asked ChatGPT to optimise it, and this is the result it came up with! It's certainly not as readable, but it still does the job, and it also handles numeric entities too! Off the back of this example, I think AI used correctly can be a fantastic teaching tool, especially when it comes to random little filter functions like this.
Adding the feed to your <head>
Once you have the feed up and running, it's time to make sure you have it visible to your users by adding it to your pages <head>. It's dead simple, just like your other feeds, only the type is set to text/plain.
<link rel="alternate" href="/feed/feed.txt" type="text/plain" title="Nooshu - Matt Hobbs blog post Plaintext Feed.">I also have links to all my feed formats in my footer so they are easy to find. So, finally, why not look at the result of the above code now by looking at my plaintext RSS feed.
Sample output
Here's an example of the output from the code above:
# Nooshu - Matt Hobbs - Frontend web developer, turned engineering manager.
## This is the website of Matt Hobbs, who is a Frontend Engineering Manager from Oxfordshire, UK.
URL: https://nooshu.com
--- Start: The Speed Trifecta: 11ty, Brotli 11, and CSS Fingerprinting
Published on: 23 January 2025
https://nooshu.com/blog/2025/01/23/the-speed-trifecta-11ty-brotli-11-and-css-fingerprinting/
Main Content:
So recently, I have written two 11ty related blog posts:
Using an 11ty Shortcode to craft a custom CSS pipeline
Cranking Brotli up to 11 with Cloudflare Pro and 11ty
...more blogpost content here...
Post changelog:
23/01/25: Initial post published.
--- End: The Speed Trifecta: 11ty, Brotli 11, and CSS FingerprintingSummary
Whew! Another blog post in the books, and another shiny new feature added—this time, a working plaintext RSS feed! (That’s one more thing off the to-do list—always a win!) Thanks for stopping by! I hope you found this post interesting, maybe even useful? As always, if you’ve got thoughts, or feedback, please let me know!
Post changelog:
- 29/01/25: Initial post published.