Adding RSS Feeds to a Lucky app
Tags: crystal, webdev, rss, lucky
Published: 2019-06-13 16:30:05

Just a quick post for anyone looking to implement RSS Feeds in Crystal Lucky Framework. This post works with Lucky 0.14.1 but it should work with 0.15 as well.

Thanks to [@paulcsmith](https://twitter.com/paulcsmith) and [@jeremywoertink](https://twitter.com/jeremywoertink) for helping me work this out in Gitter.

If you haven't heard of Lucky, check out the website [here](https://luckyframework.org/). It's a web framework written in [Crystal](https://crystal-lang.org/)

First, create an `Action` that inherits from `Lucky::Action`.  We’ll add a method called `xml` which can be called in each of your actions, passing in the data for the feed. The `xml` method will then create the xml string with Crystal’s built in XML Builder and iterate over the data that you pass in.

~~~crystal
# src/actions/xml_action.cr
require "xml"
abstract class XMLAction < Lucky::Action
    def title
        "Website RSS Feed"
    end

    def description
        "Updates for Website"
    end

    def link
        "https://websiteurl.dev"
    end
    
    private def xml(articles : ArticleQuery)
        string = XML.build(indent: "  ", encoding: "UTF-8") do |xml|
            xml.element(
                "rss", 
                version: "2.0", 
                "xmlns:dc": "http://purl.org/dc/elements/1.1/",
                "xmlns:content": "http://purl.org/rss/1.0/modules/content/",
                "xmlns:atom": "http://www.w3.org/2005/Atom",
                "xmlns:media": "http://search.yahoo.com/mrss/"
                ) do
                xml.element("channel") do
                    xml.element("title") { xml.cdata title }
                    xml.element("description") { xml.cdata description }
                    xml.element("link") { xml.text link }
                    xml.element("generator") { xml.text "Lucky Framework" }
                    xml.element("lastBuildDate") { xml.text Time.utc_now.to_s }
                    xml.element("atom:link") { 
                        xml.attribute "href", "#{link}#{request.path}"
                        xml.attribute "rel", "self"
                        xml.attribute "type", "application/rss+xml"
                    }
                    xml.element("ttl") { xml.text "60" }
                    articles.each do |article|
                        xml.element("item") do
                            # title, description, link, category, dc:creator, pubDate, content:encoded
                            xml.element("title") { xml.cdata article.title }
                            if article.meta_description
                                xml.element("description") { xml.cdata article.meta_description.not_nil! }
                            end
                            xml.element("link") { xml.text "#{link}articles/#{article.slug}" }             xml.element("dc:creator") { xml.cdata "Author Name" }
                            xml.element("pubDate") { xml.text article.created_at.to_s }
                            if article.og_image
                                xml.element("media:content") do
                                    xml.attribute "url", "article.og_image"
                                    xml.attribute "medium", "image"
                                end
                            end
                            if article.content
                                content = Markdown.to_html(article.content.not_nil!)
                                xml.element("content:encoded") { xml.cdata content }
                            end
                        end
                    end

                end
            end
        end
        Lucky::TextResponse.new(context, content_type: "text/xml; charset=utf-8", body: string status: 200)
    end
end
~~~

Although this abstract class looks quite large it’s not really doing much except generating the XML to output to the browser. By doing this it will greatly simplify our action classes.

In the example we’re passing in an `ArticleQuery` which you will need to implement in your own app. For reference, mine has a scope for `published` so I can keep unpublished articles from being viewed.

~~~crystal
class ArticleQuery < Article::BaseQuery
  def published
    published_at.lte(Time.now)
  end
end
~~~

The `Article` model has the following attributes: `title`, `meta_description` (optional), `slug`, `content` (optional), `og_image` (optional). You will need to update this for your own purposes as well.

At the end of the `XMLAction#xml` method we’re using `Lucky::TextResponse` to send the XML string to the browser. Note that `application/rss+xml` is the proper content type to respond with, however, if you want it to be viewable in a web browser you need to use `text/xml; charset=utf-8` (This is how Ghost does it at least).

## Example Action

Since all the XML logic is in the `XMLAction` we can make our actions really clean. In the example below `Rss::Index` overrides the feed title and then passes in a list of published articles through the `xml` method.

~~~crystal
# src/actions/rss/index.cr
class Rss::Index < ::XMLAction

    def title
        "Latest Dailies"
    end

    get "/feeds/all.rss" do
        articles = ArticleQuery.new.published.published_at.desc_order
        xml articles
    end

end
~~~