One of my favourite features of WordPress is how quick and easy it is to categorise posts, then even better, grab an RSS feed for that category. It’s what powers Set Studio’s newsletter, for example.
I’ve been doing some recent updates to this site and although the category system has been in place for a number of years, I wanted to add the ability for readers to subscribe to specific categories via RSS too.
In this post, what I’m going to do is first, show you how to implement a very low-tech category system and then, expand that into an RSS feed for each category. I’m also going to focus solely on JavaScript, HTML and Nunjucks so you can apply whatever front-end stuff you want with no dramas.
Setuppermalink
All we need to do here is install some dependencies:
- Code language
- text
npm i @11ty/eleventy @11ty/eleventy-plugin-rss
Eleventy configpermalink
Next, create an .eleventy.js
file and add the following to it:
- Code language
- js
const rssPlugin = require('@11ty/eleventy-plugin-rss'); module.exports = config => { config.addPlugin(rssPlugin); config.addCollection('posts', collection => { return [...collection.getFilteredByGlob('./src/posts/*.md')].reverse(); }); // Returns an array of tag names config.addCollection('categories', collection => { const gatheredTags = []; // Go through every piece of content and grab the tags collection.getAll().forEach(item => { if (item.data.tags) { if (typeof item.data.tags === 'string') { gatheredTags.push(item.data.tags); } else { item.data.tags.forEach(tag => gatheredTags.push(tag)); } } }); return [...new Set(gatheredTags)]; }); return { dir: { input: 'src', output: 'dist' } }; };
The first thing we’re doing is rigging up Eleventy’s official RSS plugin. This does all the heavy lifting for us with dates etc.
Next, we create a posts
collection by grabbing all the markdown files in the src/posts
directory. Pretty straightforward stuff.
Lastly, this is the magic. Each post item in this demo has front matter like this:
- Code language
- text
--- title: '10 Tips for Website Redesign in 2024' date: 2024-01-02 tags: ['Tips And Tricks'] ---
Even though this post is about categories, we’re using Eleventy’s existing, and rather powerful tag capabilities to power them. I like the tags setup because it creates a collection for each tag
found in content like the above. Sure, you could roll out a whole custom category operation, but this is the path of least resistance.
Let me just remind you of the snippet of JavaScript we’re focusing our attention on. Don’t copy this one into your project because you already have it.
- Code language
- js
config.addCollection('categories', collection => { const gatheredTags = []; // Go through every piece of content and grab the tags collection.getAll().forEach(item => { if (item.data.tags) { if (typeof item.data.tags === 'string') { gatheredTags.push(item.data.tags); } else { item.data.tags.forEach(tag => gatheredTags.push(tag)); } } }); return [...new Set(gatheredTags)]; });
The aim of the game with this collection is to return back an array of unique tags that are 100% guaranteed to have content attached to them because we extract them from content that references them.
The first thing we do is create an empty array to store strings in. Then — using Eleventy’s very handy “all” collection — we loop every item in the system and push the tags into this array. If the tags
value is a string, it’s pushed straight in, but if it’s an array, a quick loop and push is in order.
Using a Set
, all the duplicates are stripped out, then using the array spread operator, we turn the Set
back into an array and return it.
That’s all that needs to be done with the Eleventy config.
Templatespermalink
Everything that the user can see is using the following layout as a base, which is created on the following path: src/_includes/base.njk
. Here’s the code:
- Code language
- html
<!doctype html> <html> <head> <title>{{ title }}</title> </head> <main> {% if page.url != '/' %} <header> <a href="/">Back home</a> </header> {% endif %} {% block content %}{% endblock %} </main> </html>
There’s nothing much to cover here. I just added a link back to home if we’re not on the homepage to make the demo easier to navigate.
The only other layout is the post layout, which extends the base.njk
one. It’s created on the following path: src/_includes/post.njk
. Here’s the code:
- Code language
- html
{% extends "base.njk" %} {% block content %} <article> <h1>{{ title }}</h1> <time>{{ date }}</time> <h2>Categories</h2> <ul> {% for tag in tags %} <li> <a href="/category/{{ tag | slugify }}">{{ tag }}</a> </li> {% endfor %} </ul> {{ content | safe }} </article> {% endblock %}
Again, not much to cover here except the little list of tags. Let’s focus on that.
A tag is stored as a single string, or array of strings. Weirdly, the single tags appear to be loop-able in this context too (don’t ask me how; I haven’t got a clue 😅). All we’re doing is looping this data then using the built in slugify
filter to make a nice URL slug.
Ok, that’s the basics in place. Let’s generate these category listings and the RSS feeds. Create the following file: src/categories.njk
.
- Code language
- html
--- title: 'Category Archive' pagination: data: collections.categories size: 1 alias: category permalink: '/category/{{ category | slugify }}/index.html' --- {% extends "base.njk" %} {% set posts = collections[category] %} {% block content %} <article> <h1>{{ category }}</h1> <p> <a href="/category/{{ category | slugify }}.xml">Subscribe with RSS</a> </p> <ol reversed> {% for item in posts %} <li> <a href="{{ item.url }}">{{ item.data.title }}</a> </li> {% endfor %} </ol> </article> {% endblock %}
Using the Eleventy pagination system, we’re looping over each individual category that was generated by the categories
collection. Because it is a collection of strings that represent each category, we can:
- Use that same pattern from earlier to generate a slug, which in turn is used to generate a permalink
- Use that category string to filter down the Eleventy collections, returning posts that fall into that category
With the posts
variable that we {% set %}
, it’s a case of looping over them and generating a list of links. Job done!
Ok, last file now. Create src/category-rss.njk
. This will be the template for each category’s RSS feed.
- Code language
- html
--- title: 'Category Archive' pagination: data: collections.categories size: 1 alias: category permalink: '/category/{{ category | slugify }}.xml' siteUrl: 'https://example.com' authorName: 'Example author' authorEmail: '[email protected]' --- <?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>Low-tech Eleventy Categories - {{ category }}</title> <subtitle>Content filed under “{{ category }}”</subtitle> <link href="{{ siteUrl }}{{ permalink }}" rel="self"/> <link href="{{ siteUrl }}/"/> <updated>{{ collections.blog | rssLastUpdatedDate }}</updated> <id>{{ siteUrl }}</id> <author> <name>{{ authorName }}</name> <email>{{ authorEmail }}</email> </author> {% for post in collections[category].reverse() %} {% set absolutePostUrl %}{{ siteUrl }}{{ post.url | url }}{% endset %} <entry> <title>{{ post.data.title }}</title> <link href="{{ absolutePostUrl }}"/> <updated>{{ post.date | rssDate }}</updated> <id>{{ absolutePostUrl }}</id> <content type="html"> <![CDATA[{{ post.templateContent | safe }}]]> </content> </entry> {% endfor %} </feed>
The pagination mechanism is exactly the same as the previous template. The only difference this time is we’re looping the posts in each category to generate an <entry>
for the RSS feed.
This is also where the Eleventy RSS plugin shines because it both works out when our feed was last updated with rssLastUpdatedDate
and converts each post’s date into an RSS-friendly date with rssDate
.
I personally like to render the entire post content with post.templateContent
in the RSS feed. I think people should be able to read a post in their reader of choice (even with ads on this site). It’s also how I like to read posts in Feedbin.
The last thing to cover is I added some extra front matter: siteUrl
, authorName
and authorEmail
. I tend to set them in a more global configuration, but again, I’m keeping this post simple.
Wrapping uppermalink
I hope you can see how quickly you can get powerful functionality together if you choose the path of least resistance and using the tools available to you, with a light touch of forward thinking.
I’d love to see others using this sort of setup on their static sites too! We’re currently in the process of moving this site to Astro in the studio, so I imagine I’ll do a follow up of this setup on that platform in the future.