Introduction
I wanted to add an RSS feed to this blog but decided that instead of installing a dependency I’ll build one myself! Here’s how you can set one up in a Phoenix application:
Creating the RSS view
First let’s define a new route in lib/your_app_web/router.ex
:
scope "/", YourAppWeb do
...
get "/rss", RSSController, :index
end
A view module in lib/your_app_web/views/rss_view.ex
:
defmodule YourAppWeb.RSSView do
use YourAppWeb, :view
end
A controller module in lib/your_app_web/controllers/rss_controller.ex
with all published posts which we’ll iterate over in the RSS view, and the most recent updated_at
timestamp which we’ll use as the RSS feed’s <lastBuildDate>
later on:
defmodule YourAppWeb.RSSController do
use YourAppWeb, :controller
plug :put_layout, false
alias YourAppWeb.Content
def index(conn, _params) do
posts = Content.all_published_posts
last_build_date = Content.most_recently_updated_post.updated_at
conn
|> put_resp_content_type("text/xml")
|> render("index.xml", posts: posts, last_build_date: last_build_date)
end
end
Note that we’re disabling layout with plug :put_layout, false
for the RSS feed.
We’re also changing the response content type to "text/xml"
, technically "application/rss+xml"
is the correct content type, however it seems "text/xml"
is still the most widely supported option, you can read more about it here.
Here’s how I’ve written the most_recently_updated_post
function:
def most_recently_updated_post do
Post
|> where([p], p.is_published == true)
|> order_by([p], desc: p.updated_at)
|> limit(1)
|> Repo.one
end
Lastly create a new rss
directory and index.xml.eex
in lib/your_app_web/templates/rss/index.xml.eex
which we’ll fill in next:
Writing the XML
For the RSS feed there are required and optional fields, you can see the full RSS 2.0 specification here.
This is the basic layout for an RSS XML document:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
# data that is defined only once goes here
# e.g. RSS feed name, URL, build date, language etc.
# next we have multiple items, one for each post:
<item>
# within the item we'll have post attributes
# such as title, URL, description, category
</item>
# next item:
<item>
# same fields as for previous item
</item>
...
</channel>
</rss>
Here’s the full RSS feed I built based on the variables we declared earlier in the controller:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Blog Title</title>
<link><%= Routes.blog_url(@conn, :index) %></link>
<atom:link href="<%= Routes.rss_url(@conn, :index) %>" rel="self" type="application/rss+xml" />
<description>Your blog's description...</description>
<language>en</language>
<copyright>Copyright <%= DateTime.utc_now.year %> Your Name</copyright>
<lastBuildDate><%= @last_build_date |> to_rfc822 %></lastBuildDate>
<category>IT/Internet/Web development</category>
<ttl>60</ttl>
<%= for post <- @posts do %>
<item>
<title><%= post.title %></title>
<link><%= Routes.post_url(@conn, :show, post) %></link>
<guid><%= Routes.post_url(@conn, :show, post) %></guid>
<description><![CDATA[ <%= post.excerpt %> ]]></description>
<category><%= post.category.name %></category>
<pubDate><%= post.inserted_at |> to_rfc822 %></pubDate>
<source url="<%= Routes.rss_url(@conn, :index) %>">Blog Title</source>
</item>
<% end %>
</channel>
</rss>
A few things to note:
In my case post.excerpt
returns HTML
so I need to wrap it in an XML CDATA section: <![CDATA[ content_goes_here ]]>
. The XML parser won’t parse any characters within this tag. If your content is plain text you don’t need this tag and could simply write <description><%= post.excerpt %></description>
.
The number in the <ttl>
section defines how long in minutes an RSS reader should wait before reading the feed again, in my case I set it to 60 minutes between reads.
You’ll notice I used a custom function called to_rfc822/1
to format DateTime fields correctly, here’s how that works:
Formatting date time fields to RFC822
I used the Timex
library to format DateTime
fields to RFC822
. First declare Timex
as a dependency in mix.exs
:
defp deps do
[
...
{:timex, "~> 3.5"}
]
end
Then install with mix deps.get
.
Now let’s create a new function in the rss_view.ex
module so we can use it in our views:
defmodule YourAppWeb.RSSView do
use YourAppWeb, :view
use Timex
def to_rfc822(date) do
date
|> Timezone.convert("GMT")
|> Timex.format!("{WDshort}, {D} {Mshort} {YYYY} {h24}:{m}:{s} {Zname}")
end
end
Note that Timex
has an inbuilt option to format to RFC822
however it wasn’t validating correctly so I ended up writing my own formatter. I think the issue was the output of year as 4 digits versus 2 digits, here’s the difference in outputs:
date
|> Timezone.convert("GMT")
|> Timex.format!("{RFC822}")
# Wed, 27 Aug 20 11:37:46 +0000
date
|> Timezone.convert("GMT")
|> Timex.format!("{WDshort}, {D} {Mshort} {YYYY} {h24}:{m}:{s} {Zname}")
# Wed, 27 Aug 2020 11:37:46 GMT
Enabling RSS auto discovery
Next let’s add an RSS auto discovery link so that browsers and RSS readers can automatically find the new feed. To do that add the following code in your <head>
tags in app.html.eex
:
<link rel="alternate" type="application/rss+xml" title="Blog Title" href="<%= Routes.rss_path(@conn, :index) %>" />
Validating your new feed
Last thing you’ll want to do is validate your feed here. You can copy and paste the page source code before deploying to check for errors, and after deployment use URI option instead.
With that your feed is good to go!