Apart from the infamous one about 3D printers (I was right), and the one that featured on the orange site, my most popular post is a fairly technical one about GitHub webhooks. It seems to be especially popular with Russians coming from Yandex, so before I start this post that is also about webhooks, here's a message for my Russian friends:
Пожалуйста, постарайтесь остановить бесчеловечную войну вашей страны против народа Украины.
With that out of the way, this is a post about some things I learned recently about WordPress RSS feeds, and Discord webhooks. If you came here for the library content, this one might not be for you.
The goal
newCardigan wanted to automatically post to a particular channel when we release a new podcast episode, and to a different forum channel when we publish a new event. I'm going to go into more detail on the latter in a future post, because it has some extra complications, but in this post we'll talk about:
- How to customise a WordPress RSS feed
- How to create a webhook-powered Discord bot
Creating a custom WordPress RSS feed
The feed
I could have used default RSS feed, but by default WordPress doesn't include featured images in the RSS feed. Or at least, it doesn't include them in a separate value anywhere. We also (deliberately) don't include featured images in the actual post - they only appear in social cards and in listings of posts. For the Discord bot I wanted to include the featured image, and this meant I needed to be able to grab it in a reliable, programatic way. Adding the image to the RSS enclosures
value seemed like the best bet.
Customising a WordPress RSS (or Atom) feed is reasonably straighforward, but it's a bit hard to find any guidance on how to do it amongst all the noise of WordPress plugins - a problem I've experienced a lot when it comes to WordPress. There are a few different ways to customise feeds but I think the safest and most sensible way to do it is via a theme. In this case, you need to do two things:
- create a new file to control the feed
- update your
functions.php
file
First, you need to look in the /wp-includes
directory of your existing WordPress install. This is where all the default feed templates will be. I wanted a custom RSS 2.0 feed, so I copied /wp-includes/feed-rss2.php
into a new file I put inside my theme files at feeds/feed-featured-image-rss.php
.
Now you can customise the custom file that defines your RSS feed. The changes I wanted to make were pretty simple - I just wanted to add the featured image file as an enclosure. There is already a line in the default feed that processes enclosures for a post:
<?php rss_enclosure(); ?>
However - this only picks up video or audio files included in the post. So we need to add the image manually. Just before this line, I added a few extra lines of code:
<?php $thumbnail = get_the_post_thumbnail_url(get_the_ID(),'full'); ?>
<?php $filepath = get_attached_file(get_post_thumbnail_id(get_the_ID())) ?>
<?php $filesize = wp_filesize($filepath) ?>
<?php if ($filesize > 0) : ?>
<enclosure url="<?php echo $thumbnail; ?>" length="<?php echo $filesize ?>" type="image/png" />
<?php endif; ?>
To break that down:
- get the "thumbnail" file aka featured image
- get the filepath of the thumbnail - we need this to ascertain the file size in bytes
- get the filesize in bytes - we need this to create a valid enclosure value the RSS feed
- if the file actually exists, add the enclosure line - including the URL of the featured image file, the length in bytes, and a MIME type
Because - like category
- we can add multiple enclosures to a feed by just listing them one after the other, this enclosure doesn't interfere with the audio file for our podcast post.
Loading the feed
Now we have a custom feed file, but we need to tell WordPress that it exist and how to find it. This happens in our theme's functions.php
file:
// custom rss
function create_customfeed() {
load_template( TEMPLATEPATH . '/feeds/feed-featured-image-rss.php');
}
add_action('do_feed_featured_image_feed', 'create_customfeed', 10, 1);
do_feed_
is a special WordPress value, so what this function does is create a custom feed called featured_image_feed
, using the template at /feeds/feed-featured-image-rss.php
. You can load the feed at example.com/feed?featured_image_feed
A cool thing about this is that the custom feed works everywhere any feed works. Which means my new custom feed works in all of these locations:
https://newcardigan.org/feed?featured_image_feed # all posts
https://newcardigan.org/category/cardicast/feed?featured_image_feed # only cardiCast posts
https://newcardigan.org/category/cardiparties/feed?featured_image_feed # only cardiParty posts
Creating a Discord Webhook
Whilst there are a lot of legitimate complaints about the use of Discord in open source projects, their own documentation is really excellent.
You can create "bots" in different ways in Discord, but what I was looking for was a simple concept I've used before - a webhook. Webhooks are essentially the inverse of how an API call usually works: instead of making a request to an API endpoint to GET
information when you want it, a webhook POST
s information to an endpoint when new information is available.
In this case, we want to post a message to a particular Discord channel whenever there is a new cardiCast episode. I found an incredibly helpful guide to Discord webhooks from birdie0
. This includes a section on embeds, including how to add a custom colour, images and more.
To do this, I set up a fairly simple Python script. First, we check the RSS feed using feedparser
:
f = feedparser.parse("https://newcardigan.org/category/cardicast/?feed=featured_image_feed")
p = f.entries[0]
Then we check if we've already seen this post. If it's new, we make some content
, and an embed
. The content is any text you like, using Discord Markdown. An embed appears like quoted text or one of those URL embed images you see around the web.
Content
For our content we want to use a custom emoji, a title, link to the episode page, description, and a user category mention ("ping"). We assigned the first RSS feed entry to p
, so title, link, and description are pretty easy, as they come from our RSS feed:
f"**{p.title}**]({p.link})\n\n{p.description}"
So far so good. For emoji and group mentions however, we can't just enter them as text in the same way we would if we were in the Discord app. If I was using Discord directly, I could type :newCardigan:
and the custom newCardigan emoji (our logo) would appear in my message. But if I do that in webhook content
it will render as plain text - the code rather than the emoji. The same thing will happen with @mentions
.
To get it to display the emoji, or to activate the mention, we need to find the unique code. We do this by finding somewhere discrete in the Discord app and appending a backslash (\
) to the mention or emoji:
\:newCardigan:
\@cardiParty ping
Instead of rending the emoji or ping, Discord will instead present you with the ID code you need:
<:newCardigan:1280097925149626419>
I don't really want the world to know what the ID code is for mentioning people who have signed up to be alerted to new cardiCast episodes, so I hid that code behind and environment variable I call cardicast_ping
. So now our content
looks like this:
f"[<:newCardigan:1280097925149626419> **{p.title}**]({p.link})\n\n{p.description}\n\n<@&{cardicast_ping}>\n\n"
Embeds
In addition to content, we can have multiple embeds
in a webhook. I only wanted one, so it looks like this:
[
{
"title": "Listen to this episode right now",
"url": p.enclosures[1].href,
"color": 16741516,
"image": {
"url": p.enclosures[0].href
}
}
]
Like emoji and mentions, colours get a code in Discord. In this case it's not a special Discord code, but rather a Decimal Value. Most other web apps use hex, RGB or HSL codes, but our friend birdie0 comes to the rescue telling us to check spycolor to find the decimal value of whatever colour we want to use.
You'll notice we're referencing two enclosures here. The first value (0
) is the new enclosure we added in the custom RSS feed, representing the featured image file. The second value (1
) is added to our podcast post using the Blubrry extension, and is the link to the actual podcast audio file for this episode.
When we put it all together, it looks like this:
You can check out the full code on my Forgejo repository.