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:
And we also have a feed of articles scoped to a given category for the category show.html.erb
page:
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:
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.