Create an Eleventy podcast feed
At the end of last year I converted my DJ Cruze website from Wordpress to Eleventy. Much like this website, it's easy to deploy using Netlify. Moving to a statically generated site made it much easier to update and avoided me having to maintain a server, PHP, Wordpress versions, and so on.
For many years I created the DJ Cruze podcast show so I wanted to move over the podcast RSS feed from Wordpress to Eleventy. Creating a custom RSS feed based on my podcast episodes took a bit of work so I thought I'd share how I did it.
Create the podcast data file
Permalink to "Create the podcast data file"Eleventy supports a wide range of data sources.
For my podcast I define a JSON file which contains the metadata needed for the podcast. All of this data will be used to populate the RSS feed. You may have this information in other data files in your Eleventy site but I wanted to keep most of the data for the podcast together.
It's stored in the data directory like this: /data/podcast.json
. This will expose a global data object called podcast
which matches the filename of the JSON file and can be used in your template files.
{
"title": "DJ Cruze House Music Podcast",
"description": "DJ Cruze is in the house! Spinning funky and chunky house music since 1988. Manchester is in the house!",
"category": "Music",
"author": "DJ Cruze",
"channelImage": "/images/podcasts/dj-cruze-podcast-logo-1400x1400.jpg",
"owner": {
"name": "Marc Littlemore",
"email": "info@djcruze.co.uk"
},
"feedPath": "/podcasts/feed.xml",
"episode": {
"defaultDescription": "The latest episode of the DJ Cruze podcast features new and old funky and chunky house music."
}
}
Most of the metadata properties should be obvious.
Both iTunes and Google Podcasts support the standard RSS schema but with additional tags. Google Podcasts is essentially the same as an iTunes feeds and uses the iTunes specific tags in its feed too. Make sure that your category
property matches the expected iTunes categories and be careful that you also match the correct case. If you don't, it may well be rejected. In my case I'm using the "Music" category. Note that you can use sub-categories too but I'm not in my example.
Set up a podcast post
Permalink to "Set up a podcast post"In Wordpress, each of my podcasts was a new post with a specific tag. I exported these as Markdown files and added some custom frontmatter to mark it up with per-podcast metadata.
---
media:
# The episode number
episode: 57
# The image file for this episode
# You can ignore this if you don't want per-episode artwork
image: '/images/podcasts/dj-cruze-podcast-episode-57-june-2011.jpg'
# Your MP3 file
content: '/podcasts/dj-cruze-podcast-episode-57-june-2011.mp3'
# The duration of the episode in seconds
duration: '4230'
# The filesize of the MP3 file in bytes
fileSize: '67973718'
# A per-episode description if you want it
description: 'Another DJ set of funky house music from DJ Cruze!'
# Additional layout data here...
---
I use Eleventy's directory specific data files to mark up all podcasts with the tag podcast
. The enables us to create an Eleventy collection of podcasts to iterate through.
{
"tags": ["podcast"]
}
Custom filters
Permalink to "Custom filters"Our podcast feed needs some custom filters adding to the Eleventy configuration.
RSS feeds expect some dates using the RFC822 date format. There's an npm
package we can install to do the conversion for us. Install it as a development dependency like this:
npm install --save-dev rfc822-date
We also need to escape some of our text to ensure any special characters are encoded correctly. I use the Lodash escape method to do this. Again, lets install this a development dependency as follows:
npm install --save-dev lodash.escape
Lastly, we need to know what the date of the last episode was so we can add a special LastBuildDate
tag. This tells any clients when the podcast was last modified. By adding this, any new podcasts will update to the latest date and allow new episodes to be downloaded.
Here are the additional filters we add to our .eleventy.js
configuration file. I've only added the filters we've created for the podcast feed and intentionally left out any other Eleventy configuration.
const escape = require('lodash.escape');
const rfc822Date = require('rfc822-date');
module.exports = (eleventyConfig) => {
// RSS
eleventyConfig.addLiquidFilter('rfc822Date', (dateValue) => {
return rfc822Date(dateValue);
});
// Escape characters for XML feed
eleventyConfig.addLiquidFilter('xmlEscape', (value) => {
return escape(value);
});
// Newest date in the collection
eleventyConfig.addFilter('collectionLastUpdatedDate', (collection) => {
if (!collection || !collection.length) {
throw new Error(
'Collection is empty in collectionLastUpdatedDate filter.'
);
}
return rfc822Date(
new Date(
Math.max(...collection.map((item) => {
return item.date;
}))
)
);
});
// Rest of Eleventy config goes here...
};
Template file
Permalink to "Template file"Now we can create a post with a custom template in it to define our podcast URL. In this template we can iterate over our podcast
collection and render each podcast episode correctly into the expected RSS XML format.
I created a podcast Liquid template file with a /podcast/feed.xml
permalink. I'm excluding this file from other collections and from my sitemap. You might want to do this too.
The podcast feed is split up into the intial channel metadata which is mostly created from our podcast.json
metadata. Ensure that any text is escaped correctly by passing it through the escape
filter.
As I'm attempting to redirect my old Wordpress theme from an old URL to a new URL, I've also added in the itunes:new-feed-url
tag as follows. You won't need this unless you are migration your podcast feed too.
<itunes:new-feed-url>{{site.url}}{{podcast.feedPath}}</itunes:new-feed-url>
The second part of the podcast feed iterates through our collections.podcast
feed in reverse order. In that way, we always get the latest episode first in the RSS feed. The metadata used for each episode comes from the Markdown frontmatter that we defined earlier.
The enclosure
tag defines the media file needed to play the podcast. As I host my podcasts on a different URL to the DJ Cruze site itself, I have an additional URL in my site.json
data file which defines the external server to load them from. This explains the site.mediaFilesUrl
URL which is prepended before the content path.
Here is the full RSS template.
---
permalink: "/podcasts/feed.xml"
eleventyExcludeFromCollections: true
sitemap:
exclude: yes
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>{{podcast.title | xmlEscape}}</title>
<link>{{site.url}}</link>
<language>en-us</language>
<copyright>© 2005 - {{"now" | date: "%Y"}} {{podcast.author}}</copyright>
<lastBuildDate>{{collections.podcast | collectionLastUpdatedDate}}</lastBuildDate>
<generator>Eleventy</generator>
<description>{{podcast.description | xmlEscape}}</description>
<itunes:author>{{podcast.author}} </itunes:author>
<itunes:explicit>false</itunes:explicit>
<itunes:type>episodic</itunes:type>
<itunes:image href="{{podcast.channelImage | prepend: site.url}}" />
<itunes:owner>
<itunes:name>{{podcast.owner.name}}</itunes:name>
<itunes:email>{{podcast.owner.email}}</itunes:email>
</itunes:owner>
<itunes:category text="{{podcast.category}}" />
<itunes:new-feed-url>{{site.url}}{{podcast.feedPath}}</itunes:new-feed-url>
{%- for podcastEpisode in collections.podcast reversed -%}
<item>
<pubDate>{{ podcastEpisode.date | rfc822Date }}</pubDate>
<link>{{ podcastEpisode.url | prepend: site.url }}</link>
{% if podcastEpisode.data.media.guid %}
<guid>{{ podcastEpisode.data.media.guid }}</guid>
{% else %}
<guid>{{ podcastEpisode.url | prepend: site.url }}</guid>
{% endif %}
{% if podcastEpisode.data.media.title %}
<title>{{podcastEpisode.data.media.title | xmlEscape}}</title>
{% else %}
<title>{{podcastEpisode.data.title}}</title>
{% endif %}
{% if podcastEpisode.data.media.description %}
<description>{{ podcastEpisode.data.media.description | xmlEscape }}</description>
{% else %}
<description>{{ podcast.episode.defaultDescription | xmlEscape }}</description>
{% endif %}
<enclosure
url="{{podcastEpisode.data.media.content | prepend: site.mediaFilesUrl}}"
length="{{podcastEpisode.data.media.fileSize}}"
type="audio/mpeg"
/>
{% comment %} iTunes specific tags {% endcomment %}
{% if podcastEpisode.data.media.episodeType %}
<itunes:episodeType>{{podcastEpisode.data.media.episodeType}}</itunes:episodeType>
{% else %}
<itunes:episodeType>full</itunes:episodeType>
{% endif %}
{% if podcastEpisode.data.media.image %}
<itunes:image href="{{podcastEpisode.data.media.image | prepend: site.url}}" />
{% endif %}
{% if podcastEpisode.data.media.episode %}
<itunes:episode>{{podcastEpisode.data.media.episode}}</itunes:episode>
{% endif %}
{% if podcastEpisode.data.media.season %}
<itunes:season>{{podcastEpisode.data.media.season}}</itunes:season>
{% endif %}
{% if podcastEpisode.data.media.duration %}
<itunes:duration>{{podcastEpisode.data.media.duration}}</itunes:duration>
{% endif %}
</item>
{%- endfor -%}
</channel>
</rss>
If you head to my DJ Cruze podcast feed, you can see all of the episodes rendered correctly.
Next steps
Permalink to "Next steps"There is some metadata which could be automatically generated from the media files. I add to the duration and file size properties to my Markdown frontmatter for each podcast episode. I do this by hand but it could easily be generated by reading the file when Eleventy builds the feed.
Here is the DJ Cruze website on GitHub. Feel free to take a look through the code and adapt it for your own use.
I hope you find this useful. Let me know if you have any questions or comments.
Useful resources
Permalink to "Useful resources"Want to read more?
Automate Your Static Site Rebuilds with Netlify Scheduled Functions
Effortlessly automate regular website updates for your static site using Netlify scheduled functions. Set it and forget it - never worry about manual updates again!
Easily Create Gravatar Images With Eleventy
If you're moving your Wordpress site to Eleventy, you will want to convert your Gravatar images too. Find out how easy it is using an Eleventy shortcode.
Streamline Your Workflow: Automate GitLab Releases with Semantic-Release
Revolutionise your GitLab workflow with automated releases. Discover how to use semantic-release for seamless deployment.
You've got this: become a confident developer
If you're early in your software development career, you might not realise how good you are yet. Here are a few ideas to boost your confidence.