George Danila's profile picture George Danila's Blog

Implementing a RSS feed in Saturn

Published on

I’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 of let, when working with types that implement the IDisposable 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>