Adding an RSS feed to a Rails app

By Exequiel Rozas

- November 06, 2024

RSS is a web content syndication format, the acronym actually stands for Really Simple Syndication.

You can think about RSS like a newsletter but for content: blog posts, news articles, podcast episodes, videos, etc.

Even though it was very popular when self-publishing was more common, some people still prefer it over alternatives like newsletters which can be more promotional or erratic in the updates.

In this article we will learn how to generate an RSS feed with Rails by creating one from scratch. Let's go

Why do I need an RSS feed?

The main benefit is that users can subscribe to content updates they might interest them.

That helps us reach relevant users whenever we publish new content, without the need for them to subscribe to a newsletter that they might not be interested in.

Later, they receive the updates using RSS readers that are standalone apps or browser extensions.

The other benefit from an RSS feed is that we can use it to cross post to sites like Dev or Hashnode by importing our feed from their sites.

Lastly, even though it's not recommended, Google can use our site's RSS feed as a sitemap, we just need to include the link and pubDate fields in the RSS feed, these are analogous to the loc and lastmod fields for a sitemap.

We wrote an article about how to add sitemaps to a Rails app in which we cover the topic in detail. Check it out if you're interested on adding one to your app.

The Rails app

If you already have an application you want to add a feed to, you can skip this section entirely.

We will create an application that has a User model, a Post model, a Category and a Categorization model. With those models, we can have articles associated with an author and many categories.

rails new syndication --css=tailwind

Then we will add the users with Devise and the friendly-id to add friendly URLS to our app

bundle add devise friendly_id
bundle install

Then we install both gems and add a User model with Devise:

bin/rails g devise:install
bin/rails g devise User username:string slug:uniq admin:boolean

Next, we add the Post and Category models with scaffolding:

bin/rails g scaffold Category name description slug:uniq
bin/rails g scaffold Post user:references title content:text slug:uniq
bin/rails g model Categorization category:references categorizable:references{polymorphic}

Note that we are adding slugs to the users, categories and posts, they will correspond

Then we add the associations, so a post can have an author and many categories:

# app/models/categorization.rb
class Categorization < ApplicationRecord
  belongs_to :category
  belongs_to :categorizable, polymorphic: true
end

# app/models/category.rb
class Category < ApplicationRecord
  has_many :categorizations
  has_many :posts, through: :categorizations, source: :categorizable, source_type: 'Post'
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :categorizations, as: :categorizable
  has_many :categories, through: :categorizations
end

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

Now, we will add authors and articles for each one of them with random categories. To do that we will use db seeding using the FFaker gem.

We can add the gem to our :development, :test group in the Gemfile:

group :developmentm :test do
  gem "ffaker"
end

After that, we run bundle install and get to work on the seeds.rb file:

# adding 10 users
10.times do |index|
  username, email = FFaker::Name.name, FFaker::Internet.email
  User.create!(username: username, email: email, password: 123456)
end

# Creating 6 categories
["Rails", "Frontend", "Hotwire", "Stimulus", "Databases", "Performance"].each do |name|
  Category.create!(name: name, description: FFaker::HipsterIpsum.paragraph)
end

# Creating 60 posts
60.times do |index|
  users = User.all
  title, content, user = FFaker::Book.title, FFaker::Book.description, users.sample
  Post.create(user: user, title: title, content: content )
end

# Lastly we add categories to our posts
Post.all.each do |post|
  num_categories = rand(1..2)
  selected_categories = Category.all.sample(num_categories)
  selected_categories.each do |category|
    post.categories << category
  end
end

With this in place, we set the posts index as our root URL, add some styling with Tailwind and we end up with the post feed:

Post feed overview

And we also have a feed of articles scoped to a given category for the category show.html.erb page:

Feed of articles filtered by category

With this in place we can proceed to the first step in this process:

A brief look into the RSS spec

RSS is considered a dialect of XML so that's why it has to conform to the XML 1.0 specification.

Basically, an RSS document looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <!-- Information about the channel -->
    <item><!-- Information about each item in the channel --></item>
  </channel>
</rss>

Even though the RSS specification docs have a very exhaustive list of fields, here's the most important fields for each one of them:

Channel fields

In RSS a channel represents a grouping of items, the required fields for them are:

  • title: in our case it will be the name of our site, but there's no limit to the name you can choose here.
  • link: the complete URL of the entity that the channel represents.
  • description: a phrase or sentence that describes the channel. If it's specific about the topics of the channel even better.

Optional fields can include:

  • Language: it allows aggregators to group our channel with other channels in the same language.
  • pubDate: this represents the latest published date for the content in the feed. It should conform to the RFC 822 specification.
  • copyright: the copyright notice for the content in our channel.
  • Image: a single image for the channel. It's shown in RSS reader software. For a site or a podcast show it could be the logo, for a user the avatar. It should be a GIF, JPEG, or PNG image. It should include a <url> to the image, and it can also include a <title> to describe it.
  • category: it can include one or more categories specific to the site.
  • managingEditor: an email address for the person responsible for the content it can be suffixed with the authors name in parentheses.
  • Webmaster: an email address for the person responsible for technical issues. The format is the same as for the managingEditor.

Item fields

An item represents an element in our feed. In the case of our application it's going to be a post, but it can be anything you want to group within your feed.

The required attributes for the <item> field are the same as for the channel:

  • title: this would be the title for our post.
  • link: the URL of the item.
  • description: a short description or synopsis of the item.

Optional fields can include:

  • author: an email address for the author of the item, it can be suffixed by the author name in parentheses.
  • category: it can include one or more categories specific to the article.
  • Guid: a string that uniquely identifies the item. It can be the same as the link attribute.
  • pubDate: the date that the item was published.
  • comments: a URL to the comment section of our item.

Adding an RSS feed to our application

After we studied the fields that are associated with an RSS feed, we will be adding one to our app.

First, we will add one main feed to the/rss path of our application. For this, we will need to add a route to handle that:

# config/routes.rb
get "rss", to: "posts#rss", defaults: { format: "rss" }

Note that we are adding RSS as the default response format in order to avoid having to append the .rss to the URL.

Then, in our PostsController we define the rss action and fetch the posts from there.

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def rss
    @posts = Post.limit(24)
  end
end

Please note that you can pick the logic you want to fetch the posts. Maybe you want to only access published posts or show less posts for the feed, it's up to you.

This will render the following XML when we visit the /rss path:

# app/views/posts/rss.rss.builder
xml.instruct! :xml, version: "1.0"
xml.rss version: "2.0" do
  xml.channel do
    xml.title "Syndication Blog"
    xml.description "Your favorite authors and their latest posts."
    xml.link posts_url
    xml.language "en"

    @posts.each do |post|
      xml.item do
        xml.title post.title
        xml.description ApplicationController.helpers.strip_tags(post.content)
        xml.author "#{post.user.email} (post.user.username)"
        xml.pubDate post.updated_at.rfc822
        xml.link post_url(post)
        xml.guid post_url(post)
      end
    end
  end
end

And the feed would look like this:

The XML RSS feed

Making our feeds automatically discoverable

Even though there's a certain convention about having the main RSS feeds located at /rss there's no actual hard rule about where to locate them.

That's why there's a way to publish our feed's URL in our HTML source using a link tag that we can locate within our <head> tag:

<link rel="alternate" type="application/rss+xml" href="/rss" title="RSS Feed" />

But we don't need to explicitly set this tag, we can use the auto_discovery_link_tag within our view in order to have the tags render in the layout's head.

This tag can receive four params:

  • type: it's the first parameter and should be passed as a string or symbol. It's of type :rss or :atom or :json.
  • path: the path of the link to the feed.
  • title: the title for the RSS feed.

So, if we use this tag from our application.html.erb layout we can configure it like this in our document's head:

<%= auto_discovery_link_tag(:rss, rss_path(format: :rss), title: "Syndication blog RSS Feed") %>

Adding an RSS feed to authors

Some people might want to receive updates from specific authors instead of the whole site feed

We start by defining a route that maps to the rss action of the AuthorsController and a route to show the author from

# config/routes.rb
get "authors/:author_id/rss", to: "authors#rss", as: :author_rss

If you want, you can namespace the route to an Authors::FeedController to keep your controllers RESTful.

Then, we add the controller:

# app/controllers/feed_controller.rb

class AuthorsController < ApplicationController
  def show
    @author = User.friendly.find(params[:author_id])
    @posts = @author.posts
  end

  def rss
    @author = User.friendly.find(params[:author_id])
    @posts = @author.posts
  end
end

And finally the view which is very similar to the site feed:

# app/views/authors/feed/rss.rss.builder
xml.instruct! :xml, version: "1.0"
xml.rss version: "2.0" do
  xml.channel do
    xml.title "#{@author.username} RSS Feed"
    xml.description "Latest posts from #{@author.username}"
    xml.link author_url(@author)
    xml.guid author_url(@author)
    xml.language "en"

    @posts.each do |post|
      xml.item do
        xml.title post.title
        xml.description ApplicationController.helpers.strip_tags(post.content)
        xml.pubDate post.created_at
        xml.author "#{post.user.email} (#{post.user.username})"
        xml.link post_url(post)
        xml.guid post_url(post)
      end
    end
  end
end

Lastly, we can add an auto discovery tag for the authors page.

In order to do that we should remove the tag itself from the application.html.erb and render it from our home page which is the posts index page.

First, we replace the tag from the head with a call to the yield helper in the head:

# app/views/layouts/application.html.erb
<head>
  <%= yield :head %>
</head>

Then, we add the call to the auto discovery tag in our views:

# app/views/posts/index.html.erb
<%= content_for :head do %>
  <%= auto_discovery_link_tag(:rss, rss_url(format: :rss), title: "Syndication Blog RSS Feed") %>
<% end %> 
# app/views/authors/show.html.erb
<%= content_for :head do %>
  <%= auto_discovery_link_tag(:rss, author_rss_url(format: :rss), title: "#{@author.username} RSS feed") %>
<% end %>

As you can see, we could add as many RSS feeds to our Rails application as we see fit. If users might find them useful, you should consider adding them.

Using the Atom format instead of RSS

Like RSS, Atom is a syndication format that uses XML to declare web feeds.

It was initially released on 2005 to improve on perceived shortcomings of the RSS format. It includes some changes like:

  • Feed is the root container: instead of using the <channel> tag we use a <feed> tag that contains our feed.
  • Date formats: Atom relies on the RFC 822 formatted timestamps.
  • Internationalization: with Atom we can add item specific language using the xml:lang attribute.
  • A unique ID is required for every entry: you can opt to use a permalink or provide a URN (Uniform resource name).
  • Author information: it provides a more structured way to add author information using the <author> tag which can include name, email, and URI tags.
  • Published and updated: the <published> and <updated> tags replace the <pubDate> and <lastBuildDate> tags.
  • Items are entries: instead of using the <item> tag we use the <entry> tag. The same happens with other tags like description which becomes a summary.
  • We can add HTML and other type of content: by specifying the content type with the type attribute.

There are more changes in the Atom specification, but we can build our feed using what we already know:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" version="2.0">
  <title>Syndication RSS Feed</title>
  <summary>The latest posts from the Syndication blog</summary>
  <link>https://example.com</link>
  <language>en</language>
  <atom:link rel="self" type="application/rss+xml" href="https://example.com/rss"/>
  <!-- Our list of entries -->
  <entry>
    <title>Example post</title>
    <summary type="html"><p>We can use HTML inside our summary</p></summary>
    <author>
      <name>John Doe</name>
      <email>jdoe@example.com</email>
    </author>
    <published>Thu, 19 Oct 2024 10:32:26 +0000</published>
    <updated>Thu, 24 Oct 2024 13:05:26 +0000</updated>
    <link>https://example.com/posts/example-post</link>
    <guid isPermalink="true">https://example.com/posts/example-post</guid>
    <category>Hotwire</category>
    <category>Front End</category>
  </entry>
</feed>

This is an Atom compatible feed, and we can generate it exactly like we generated the site and author-specific feeds.

Consider that some pages mix the Atom and RSS formats to include the best of worlds while keeping compatibility with RSS readers that might not fully implement new formats.

To maximize compatibility we can add the xmlns="http://www.w3.org/2005/Atom" attribute to the <rss> tag and add the <atom:link> tag to a regular RSS feed and then add just the Atom formatted fields we need.

Feel free to check what other sites are doing and always use a tool like the W3 Validator to check your output.

Summary

RSS, which stands for Really Simple Syndication, is a convenient way to notify interested users/programs whenever we publish new content.

It's not as popular as it once was, but it's still used by many users, especially those that are technically savvy.

To add one to a Rails app we have to define a feed in an XML file which has a <channel> field that describes the entity that holds the feed: a news website, a blog, a podcast, etc.

Inside the channel we define a series of entries within the <item> tag with information about the entities that will be finally syndicated: a news article, a blog post, a podcast episode, etc.

Then, create the feed itself by defining a route like /rss and then rendering an XML view with the RSS format using the Rails included XML builder.

We can add an auto discovery tag which allows readers to find where our feeds are so they can show users in order for them to decide to subscribe to our feed or not.

Adding many feeds for our application can be a good idea if our users might use them to subscribe to particular feeds.

Lastly, using the Atom format or mixing it with the RSS format can gives us the most versatility for our feeds.

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.