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
usebinding, instead oflet, when working with types that implement theIDisposableinterface. 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>
George Danila's Blog