How To Create an Atom Feed in Rails

Your web site is now functionally complete, and it's got a good selection of quality content to interest potential viewers. You'd like to make it easy for those potential viewers to discover your content. One of the best ways to do this is to add a feed to your site.

Feeds can be read by software referred to as a feed reader. Interested users can subscribe to your feed using their feed reader, allowing them to quickly become aware of new content when it's added to your web site. Feed readers are widely available, so any user who's interested in following feeds in this way should be able to find one. Even most web browsers now include a feed-reading capability.

Feeds are generally created to accomplish one of the goals below:

  1. Content Distribution: A feed can contain a complete content item, such as the full text of an article or blog entry. This method allows subscribers to access the full content. Subscribers can easily extract the content and re-use it for their own purposes.

  2. Content Notification: A feed can contain summaries of content items, such as articles, blog entries, videos, etc. Since the feed doesn't contain the full content, it generally includes links that allow users to navigate to a URL where they can view the content. This method can be used to help drive traffic to a web site.

In this article, we're going to create an Atom feed for the KeenerTech.com blog using Ruby on Rails, with the goal of driving traffic to the blog web site. All reasonably well-supported feed readers support both Atom and the various versions of the RSS standard. For our purposes, though, Atom is a better choice since our blog content will contain HTML tags.

The Atom Syndication Standard

The Atom Syndication Format, now at version 1.0, is a standard for syndicating content, very similar to RSS. Like RSS, information is packaged into an XML file.

Atom is, in some respects, an enhanced RSS-like standard that provides better support than RSS for diverse content types, internationalization and modularity. Many people, in fact, use RSS as an umbrella term for feed standards (e.g. — RSS 1.0, RSS 1.1, RSS 2.0, etc.) and just lump Atom in as "another" RSS standard, even though, strictly speaking, that is not correct.

What Does an Atom Feed Look Like?

An Atom feed is just an XML file that adheres to the Atom Syndication Standard. A sample Atom feed is shown in Listing 1.

Listing 1: Sample KeenerTech.com Blog Feed

 <?xml version="1.0" encoding="utf-8"?>
 <feed xml:lang="en" xmlns="http://www.w3.org/2005/Atom">
   <title>KeenerTech.com Articles</title>
   <link href="http://www.keenertech.com/feeds" rel="alternate"/>
   <link href="http://www.keenertech.com/articles.atom" rel="self" 
      type="application/atom+xml"/>
   <id>http://www.keenertech.com/articles.atom</id>
   <updated>2009-08-30T18:56:14Z</updated>
   <generator uri="http://www.keenertech.com">
      KeenerTech.com
   </generator>
   <entry>
     <title><![CDATA[Maven Introductory Presentation 
         Available Online]]></title>
     <link 
        href="http://www.keenertech.com/articles/2009/08/23/maven_intro" 
           rel="alternate"/>
     <id>tag:keenertech.com,2009-08-23:321</id>
     <author>
       <name>Steve Keener</name>
       <uri>http://www.keenertech.com/profiles/steve_keener</uri>
     </author>
     <updated>2009-08-24T01:26:16Z</updated>
     <published>2009-08-23T00:00:00Z</published>
     <summary type="html">My presentation, 
        &lt;a href="http://www.keenertech.com/docs/p/Maven.pdf"&gt;
        Maven: Managing Software Projects for Repeatable Results
        &lt;/a&gt;, is now available online. Find 
        out how to leverage this sophisticated build tool to automate 
        key tasks for your next Java project.
   </entry>
 <feed>

Feed Structure

Like an RSS feed, an Atom feed consists of a feed summary section, with a small number of elements that provide information about the feed, and a set of one or more entries. Each entry contains elements that provide information about a particular content item ( a blog entry for our feed).

Feed Summary Elements

The summary section of the Atom feed consists of the elements defined in Table 1. An asterisk indicates elements that are required to be present for a valid Atom feed. Notes about how the element will be handled for the feed we are building in this article will be in italics.

Table 1: Feed Summary Elements

Element Description
author This element provides information about the author of the feed. Data about the author is contained in one or more child elements.

According to the standard, an author element must be provided within the feed, but there are two valid ways to specify this information. If it appears in the summary section, then it represents the author of the entire feed. Alternatively, each entry must have an author element, which would describe the author of each entry.

In this article, we're building an Atom feed for a blog, so each entry will have its own author element. Accordingly, no author element will appear in the summary section of the XML output.

generator This element defines the software or web site that created the feed.

For this feed, the feed generator will be defined as "KeenerTech.com.". The "uri" attribute of the element will provide a link to the KeenerTech.com web site.

id * This element defines a unique and permanent identifier for the feed. In practice, the id is typically defined as the full URL of the feed. Note that the Atom standard considers this value to be an id; under no circumstances should it be treated as a URL. If a URL for the feed is needed, use the "link" element instead.
link The link element provides a URL for a resource related to the Atom feed, with the URL provided in the "href" attribute. The meaining of the link is defined by the "rel" attribute. A feed can include multiple link elements, but only one link for each valid "rel" value.

Our Atom feed will include two link elements in the summary section, one with the "rel" element is set to "self" — indicating that the URL is a self-referential link to the Atom feed itself. The other link element will have its "rel" element is set to "alternate," indicating that the URL is a link to additional information about the feed.

title * The title of the feed.
updated * The date/time that this instance of the feed was updated significantly. Date/time values are in the following format:

     2009-04-24T00:00:00Z

The values are in GMT, and show a four-digit year, a two-digit month and a two-digit day. The "T" character serves as a separator between the date/time portions of the value. The date value is terminated with the "Z" character. Note that it's the "Z" character at the that implies GMT; otherwise a specific offset from GMT can be specified.

The simplest solution, and the one that I recommend, is to specify that dates using GMT. People who use the feed can easily translate that into an appropriate date for their timezone if they need to.

Entry Elements

The Atom feed includes one or more entries, where each entry provides information about a blog entry. The Atom Syndication Format provides a set of standard elements for describing content entries.

Table 2: Standard Atom Entry Elements

Element Description
author This element provides information about the author of the entry. If the entry has multiple authors, then the element will appear multiple times. Within the author element, information about the author is contained in one or more child elements. The "name" element is required. There are also two optional sub-elements, "email" and "uri". The "uri" is typically either the URL of the author's blog site or the URL of a profile page that provides more information about the author.

According to the standard, an author element must be provided within the feed, but there are two valid ways to specify this information. If it appears in the summary section, then it represents the author of the entire feed. Alternatively, each entry must have an author element, which would describe the author of each entry.

In our blog's Atom feed, each entry will have its own author element. Additionally, since our blog entries can only have a single creator, only one author element will be provided for each entry.

content This element contains the full content of an entry.

We're not going to provide a content element for our feed because we're not trying to distribute the blog's content. Instead, we're trying to notify potential viewers of the content that we have available, and entice them to go to the web site. So, we'll provide a summary of the content rather than the full content.

id This element contains a well-formed URI for a entry. A sample entry id is shown below:

     tag:keenertech.com,2009-08-16:322

link The "href" attribute of the link element specifies the URL for a web page that shows detailed information about the entry.
summary Provides a short abstract of the entry.
title A human-readable title describing the entry.
updated The date/time that this entry was last modified. Date/time values are shown in the same format as the "updated" element of the feed summary section.

Onwards to the Code

Implementing an Atom feed in Ruby is a straight-forward task. Ruby provides the Builder module which automates creating XML documents. The general technique shown in most technical books is to create a controller that retrieves a list of content items from the database. The controller then makes that list available to a view, which includes Builder code to generate the necessary XML.

My technique is a little different. In Listing 2, I pull the Ruby code to produce the feed into the model. By doing this, it's easier to write unit tests for the Atom generation code. It's also easier to share the code if you need to produce multiple Atom feeds, e.g. — a separate feed for each category.

Listing 2: The Ruby Code Behind the Atom Feed

 class Entry < ActiveRecord::Base

    def self.generate_atom_feed(title, entries, options = {})
      feed_ref_url = 'http://www.keenertech.com/feeds'
      feed_url = 'http://www.keenertech.com/articles.atom'
      base_entry_url ='http://www.keenertech.com/articles/'
      buffer = ''
  
      xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => 
        options[:indent], :target => buffer)
      xml.instruct! :xml, :version=>'1.0', :encoding=>'utf-8'
  
      xml.feed 'xmlns' => 
          'http://www.w3.org/2005/Atom', 'xml:lang' => 'en' do
        xml.title title
        xml.link 'rel' => 'alternate', 'href' => feed_ref_url 
        xml.link 'rel' => 'self', 
                 'href' => feed_url,
                 'type' => 'application/atom+xml'
        xml.id feed_url
        xml.updated Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
        xml.generator 'KeenerTech.com', 'uri' => 'http://www.keenertech.com'
  
        entries.each do |entry|
          if !entry.name.nil?
            xml.entry do
              # Create Atom entry elements
              xml << "    <![CDATA[" + entry.name + "]]>\n"
              xml.link  "rel" => "alternate", 
                "href" => base_entry_url + 
                entry.display_date.strftime('%Y/%m/%d/') + entry.url_name
  
              xml.id "tag:keenertech.com,#{entry.display_date.strftime
                ('%Y-%m-%d')}:#{entry.id}"
              xml.author do
                xml.name "#{entry.user.first_name} #{entry.user.last_name}"
                xml.uri entry.user.first_name + "_" + entry.user.last_name
              end
              xml.updated entry.updated_at.strftime("%Y-%m-%dT%H:%M:%SZ")
              xml.published entry.display_date.strftime
                ("%Y-%m-%dT%H:%M:%SZ")
              xml << "    " + 
                 Entry.escape_html(entry.summary) + "\n"
            end
          end
        end
  
      end
      
      buffer
    end
  
  protected

    def self.escape_html(str)
      return str.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>') if str
    end

 end

In the code above, I've hard-coded the feed paths. If I were to generalize this code to produce multiple Atom feeds, such as for different categories, I'd need to re-factor this code slightly since each feed should have its own distinct URL.

To produce the XML, the caller must pass in the title of the feed, the list of entries to be included in the feed and any options. The options are passed directly to Builder::XmlMarkup; the only option that is really supported is the ":indent" option, which specifies how many spaces each level of XML should be indented. The Builder::XmlMarkup object will direct its output into the "buffer" string.

The Builder::XmlMarkup object makes it easy to produce XML. A statement like:

xml.title title

will produce the XML below:

<title>KeenerTech.com Articles</title>

Likewise, a statement like this:

xml.generator 'KeenerTech.com', 'uri' => 'http://www.keenertech.com'

will produce an XML element that includes both content and an attribute:

<generator uri="http://www.keenertech.com">KeenerTech.com</generator>

Since I've pulled the Atom generation code into the model, the controller will need to call the Entry.generate_atom_feed method in order to produce the output. The controller will then receive the XML as a string, which it will then render without benefit of a view. The controller code is shown in Listing 3.

Listing 3: Controller Code

  def index
    @articles = Entry.find(:all, 
                           :include => :user, 
                           :order => "entries.display_date desc")
                           
    respond_to do |wants|
            wants.html do               
              render :layout => 'keenertech' 
            end
            wants.atom do
              xml = Entry.generate_atom_feed('KeenerTech.com Articles',
                @articles, :indent => 2)
              render :text => xml, :content_type => 'application/atom+xml'
            end
          end     
  end

The controller generates the list of articles using the find method. Note that it uses the ":include" option to incorporate user information into the list. Since the user information has been pre-fetched by find, traversing from an entry to its corresponding user will not generate additional database calls (which is something that the Atom generation code will be doing).

The controller uses the respond_to statement to determine what type of content the caller wants to produce. If HTML, Rails will use a view to generate the appropriate HTML output. If the caller has specified Atom, then the code generates the Atom output by calling the model. It then renders the content directly.

Does this break the MVC paradigm? It probably bends the rules a little bit, but the advantages outweigh the disadvantages.

The primary advantage is that the code for producing the Atom output is centralized in the model, where it can easily be shared and unit-tested. Meanwhile, the model and controller still have clearly defined roles in producing the output.

The model simply produces the output; it doesn't render it. The controller determines what data elements will appear in the feed, gets the Atom output from the model and then causes the output to be rendered. While the controller causes the content to be rendered, it doesn't otherwise manipulate it. We could have a view that renders the Atom output string, but it seems kind of pointless to have a one-line view.

Atom Validation

After producing the feed, you should validate it using the Feed Validator. This free service will perform a detailed analysis of Atom and RSS feeds. It produces excellent diagnostic output, as well as good recommendations for items that optional. Don't even think about considering your feed to be "finished" until you've successfully validated it.

References

There are a number of relevant references available that can be useful for those producing Atom feeds:



Comments

No comments yet. Be the first.



Leave a Comment

Comments are moderated and will not appear on the site until reviewed.

(not displayed)