Adding an RSS Feed With Python

Yes, It Still Lives On In Our Hearts

The decline of RSS usage in the late aughts and its eventual near-demise has left many a tech-geek deflated, but not entirely hopeless. The reality is that RSS still exists out there in the wild, and some folks still depend on it for accessible, personalized, and syndicated content. So I decided to add it to my site!

Let's Get to It

I'm going to jump right into it this time. So I wasn't quite sure where to start, so I did the prudent thing and asked my search engine. I eventually landed on this interesting library called fastapi_rss, by Dogeek.

Perfect!

But, if you've read any of my past posts, you probably know that I sometimes tend to do things the hard way.

So yeah, I decided just to build it from scratch for my site.

Let's Really Get to It

I started first by looking up the RSS specification. In spite of the format's strange and storied past, there is still enough information online to get us there.

The gist of it (from rssboard.org):

At the top level, a RSS document is a <rss> element... Subordinate to the <rss> element is a single <channel> element, which contains information about the channel (metadata) and its contents... A channel may contain any number of <item>s. An item may represent a "story"...

Basically, I needed to create an xml document. At it's minimum, it would look something like:

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">

<channel>
<title>Python By Night</title>
<link>https://www.pythonbynight.com</link>
<description>Coolest Site Ever</description>

<item>
<description>Such A Simple RSS Feed</description>
</item>
</channel>
</rss>

Now, theoretically, that would get the job done, but to get the most out of syndication, I wanted to create new <item> tags with each of my new articles so that they update the four people that would be subscribed to my feed.

In addition, there are a few optional parameters that I wanted to add. You'll see those peppered through some of the following examples.

Starting With a Jinja Template

I already use Jinja as my templating engine for this blog, so I decided to use another template to house the skeleton of my RSS feed.

My RSS file is called rss_feed.html and it looks something like:

{% import "main/macros/rss_item.jinja2" as item -%}

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Python By Night</title>
<link>https://www.pythonbynight.com</link>
<description>{{ meta_description }}</description>
<language>en-us</language>
<pubDate>{{ rss_channel_pub_date }}</pubDate>
<docs>https://www.pythonbynight.com/rss</docs>
<generator>https://www.pythonbynight.com</generator> 
{% for article in articles %}
{{item.build_item(article.meta_title, article.slug, article.meta_description, article.rss_pub_date) -}}
{% endfor %}
</channel>
</rss>

What you'll note here is that I have created a Jinja macro to populate the <item> fields in my xml snippet above. I'll talk about that a little later. A quick thing to note here, however.

Database Tangent

The RSS spec allows for the optional <pubDate> field to be populated. Here is how the spec describes that field:

The publication date for the content in the channel. For example, the New York Times publishes on a daily basis, the publication date flips once every 24 hours. That's when the pubDate of the channel changes. All date-times in RSS conform to the Date and Time Specification of RFC 822, with the exception that the year may be expressed with two characters or four characters (four preferred).

What you'll note is that the spec requires the date to be in RFC 822 format.

As you may already know, I'm using MongoDB as my backend database, and since my data is relatively young, I decided to add a field to my current documents called rss_pub_date which contains a date string in the preferred RSS format.

And notably, going forward, any time I publish a blog post and save, I use the excellent pendulum library to add an RSS-ready entry so I can call that directly from my application.

All I have to do is add this piece of code to the method that handles publishing:

import pendulum

dt = pendulm.now()

@router.post("/admin/create-article)
async def publish_article(
    request: Request,
    pub_article: pub_article
)
    ...
    pbn_article.rss_pub_date = pnd.now().to_rss_string()
    await pub_article.save()

There is way more code for this task, but this article isn't about that.

My main point is, if you're just starting out and know that you will be adding RSS in the future, it might be worth adding a field to your database with the string that you'll need for your RSS feed.

It just means you won't need to make a call to the db and then do a conversion before sending the date to your feed.

Back to Jinja

The other aspect of the template above is the macro. To populate your feed with your entries, you'll want specific pieces of information. These are the pieces I decided to add to each item (rss_item.html):

{% macro build_item(item_title, item_link, item_description, item_pub_date) -%}
<item>
<title>{{ item_title }}</title>
<link>https://www.pythonbynight.com/blog/{{ item_link }}</link>
<description>{{ item_description }}</description>
<pubDate>{{ item_pub_date }}</pubDate>
<guid>https://www.pythonbynight.com/blog/{{ item_link }}</guid>
</item>
{% endmacro %}

Out of each build_item, I am extracting four pieces of information that are already in my database. This includes the item_link variable, which is a slug of my item_title that is also generated on database save.

The Jinja macro acts as a function (build_item) with the four variables listed above.

From my application, I only need to send the rss_feed.html template the proper context that includes a list of articles I want to include on the feed.

What this means is, I make a database call with Beanie to find my articles. I send a list of those objects directly to my template.

The template then loops through each of the article objects and populates the four items listed in the macro.

Creating the Feed

Now that I have the template in place, I just need to make the appropriate call to the database to retrieve my articles.

One thing to note, the general <channel> tag has the <pubDate> field I talked about. I want to populate this with the date of my latest entry.

In addition, each of the <item> tags also have their own <pubDate> fields. Since I've already made sure that each of my database collections has a string rss_pub_date, it should make for straightforward logic.

Here is what my route looks like:

from fastapi.templating import Jinja2Templates
from app.models import ArticleDB
from app.config import settings


templates = Jinja2Templates("path/to/templates")

@router.get("/rss")
async def rss(request: Request):
    """Generate rss feed."""

    rss_articles = await ArticleDB.find(
        ArticleDB.published==True).sort(
            -ArticleDB.pub_date).to_list()

    rss_channel_pub_date = rss_articles[0].rss_pub_date

    return templates.TemplateResponse(
        "main/rss_feed.html",
        context={
            "request": request,
            "meta_description": settings.site_description
            "articles": rss_articles,
            "rss_channel_pub_date": rss_channel_pub_date
        },
        media_type="application/rss+xml"
    )

Okay, let me walk through that a little bit.

The templates variable instantiates the template engine that allows us to send a TemplateResponse. You can read more about how to do this in the FastAPI documentation.

The database call does most of the heavy lifting. I find all my published articles and sort them on pub_date (descending on published date).

The channel <pubDate> will be populated by the rss_pub_date of my most recently published article. I send that in the template context as rss_channel_pub_date, which is then used in rss_feed.html.

The TemplateResponse contains a string to the template path ("main/rss_feed.html"), as well as the context and media type.

The context is one of the most important parts of the template. This is how objects are sent from your app to the actual Jinja template.

Here's a quick rundown of context dictionary. The request object is mandatory (per FastAPI). The articles variable contains a list of my database objects.

I also happen to keep my site description in my settings module, that way I can update it easily when needed. I send that through to the RSS channel <description> with the meta_description variable. And I've already talked about the rss_channel_pub_date.

Lastly, I include the media type of "application/rss+xml" as per the RSS specification.

Feed Me!

The last piece of the puzzle is to include an RSS icon up in key areas of my web page. Well, something like that. You may need to do a little troubleshooting here and there (I know I did at least).

The rssboard.org website contains an RSS Validator that you can use to test your link. I failed a couple of times.

Apparently, it may give you a warning about adding an Atom link, but I chose not to go down that path.

If everything goes well, you should be able to validate!

Validated RSS

HUZZAH!

Anyway, hopefully this proved helpful. Feel free to let me know if you have any thoughts, and try out the feed!