Implementing a RSS feed in Saturn
Published onI’m a big fan of RSS feeds. They are my favorite way of keeping up-to-date with various blogs and news sites, so I figured adding a RSS feed to my personal blog should be a priority.
Fortunately, this was very straightforward because of the types living in the System.ServiceModel.Syndication
namespace. Firstly, I added a new route to the main application router, mapping the /rss
path to the RSS handler:
let mainRouter = router {
// ... other routes/pipelines etc.
get "/rss" RssFeed.handler
}
let app = application {
// ...
useRouter mainRouter
}
Let’s see how the RssFeed
module is defined:
module RssFeed
open System
open System.IO
open System.Text
open System.Xml
open System.ServiceModel.Syndication
open FSharp.Control.Tasks
open Saturn
open Giraffe
let handler =
pipeline {
plug (publicResponseCaching 1200 None)
plug (setHttpHeader "Content-Type" "application/rss+xml; charset=utf-8")
plug (fun _ ctx ->
let blogPosts = loadBlogPosts() // fetch the blog posts from the db etc.
task {
let syndicationFeed = generateFeed blogPosts
let serialized = serialize syndicationFeed
return! ctx.WriteBytesAsync serialized
}
)
}
The handler sets the appropriate Content-Type header for RSS and proceeds to generate a SyndicationFeed
instance, that is subsequently serialized and written into the HTTP response. I’ve also specified response caching for added performance, by using Giraffe’s publicResponseCaching
. The generateFeed
function would look something like this:
let generateFeed blogPosts =
let timestamp = DateTimeOffset(DateTime.Now)
let title = "Feed title"
let description = "Feed description"
let blogUrl = Uri("https://www.website.com")
let feed = SyndicationFeed(title, description, blogUrl, "RSSUrl", timestamp)
feed.Copyright <- TextSyndicationContent("Copyright text")
let syndicationItems =
blogPosts
|> Array.map blogPostToSyndicationItem
feed.Items <- syndicationItems
feed
A SyndicationFeed
consists of a few key properties, like the title, description, URL, a timestamp of when it was generated and also a list of items representing the individual blog posts. Mapping a blog post to a SyndicationItem
was done using the blogPostToSyndicationItem
function:
let blogPostToSyndicationItem blogPost =
let publishDate = DateTimeOffset(blogPost.PublishDate)
let title = blogPost.Title
let description = blogPost.ShortDescription
// the slug is used to generate a unique URL for the blog post
let url = Uri("https://www.website.com" + blogPost.Slug)
let id = blogPost.Id
SyndicationItem(title, description, url, id, publishDate)
The last missing piece is the serialize
function, that transforms a SyndicationFeed
into a byte array:
let serialize feed =
let settings = XmlWriterSettings()
settings.Encoding <- Encoding.UTF8
settings.NewLineHandling <- NewLineHandling.Entitize
settings.NewLineOnAttributes <- true
settings.Indent <- true
use stream = new MemoryStream()
use writer = XmlWriter.Create(stream, settings)
let formatter = Rss20FeedFormatter(feed, false)
formatter.WriteTo(writer)
writer.Flush()
stream.ToArray()
Note the
use
binding, instead oflet
, when working with types that implement theIDisposable
interface. This will ensure that values are automatically disposed when they go out of scope.
Accessing the newly created path in a browser will produce a piece of XML that is compatible with any RSS feed reader, like Feedly or Inoreader:
<?xml version="1.0" encoding="utf-8"?>
<rss
version="2.0">
<channel>
<title>Feed title</title>
<link>https://www.website.com/</link>
<description>Feed description</description>
<copyright>Copyright text</copyright>
<lastBuildDate>Fri, 04 Jun 2021 12:58:27 Z</lastBuildDate>
<item>
<guid isPermaLink="false">blog-post-0</guid>
<link>https://www.website.com/article/blog-post-0/</link>
<title>Blog post 0 title</title>
<description>Blog post 0 description</description>
</item>
<item>
<guid isPermaLink="false">blog-post-1</guid>
<link>https://www.website.com/article/blog-post-1/</link>
<title>Blog post 1 title</title>
<description>Blog post 1 description</description>
</item>
</channel>
</rss>