Wednesday, Nov 30, 2011

Generating RSS feeds from a Backbone.js Collection in Node.js

The awesome Anja Skrba has generously translated this post into Serbo-Croatian and made it available here for viewing.

When designing and developing this site, I wasn't entirely sold on using Backbone structures on the server-side. The benefits weren't immediately clear. Although after creating the blog using Models, Collections and Events, I'm very glad I chose Backbone. Events are very helpful for re-rendering the RSS feeds as you will see.

No magic is being written here, nor the solving of any new issues, just elegant code organization and implementation.

These are required dependencies:

How to structure the server:

Since this post is specifically about generating RSS, I'll show the shell of what my server looks like to make it easier to envision implementing in your own application. I will break down the Model and Collection code after showing the server.

// Require server dependencies var Backbone = require("backbone"); var express = require("express"); var RSS = require("rss"); /* Post Model and Posts Collection exist here outside the route, which is * possible since Node.js is the server and script coupled, and exists * entirely in memory. */ // Route to specific post site.get("/post/:id", function(req, res) { /* ... */ }); // Currently does nothing, will show soon how to serve this site.get("/rss.xml", function(req, res) { /* ... */ }); // Render out home page with list of posts site.get("/", function(req, res) { /* ... */ });

Checking out the Model:

The Post model is responsible for dealing with the slugification, which is used when constructing unique URLs for each Post. The idAttribute is very handy here later on when you need to find the Post that corresponds to the id parameter that is available in the Post entry route that you can see above in my server.

var Post = Backbone.Model.extend({ // This is a great way to reference the post later, since its unique idAttribute: "slug", // When initialized from collection fetch generate the slug initialize: function() { this.set({ slug: this.slugify() }); }, // Convert the title attribute into a URL friendly slug slugify: function(title) { return this.get("title").toLowerCase().replace(/ /g, "-") .replace(/[^\w-]+/g, ""); } });

Setting up the Collection:

When the Posts collection is initialized, a new RSS feed object is created and attached as a reusable property. The sync method was intentially ommitted since that is a lot of code not pertinent to this post. Just be aware that I override sync in order to actually fill the collection from files on the filesystem.

var Posts = Backbone.Collection.extend({ model: Post, // Sort by most recent posted date comparator: function(post) { return -1 * new Date(post.get("posted")); }, // Fetches posts from Git folder sync: function(method, model, options) { /* ... */ }, // Create a new RSS feed and assign it as a property on the Collection initialize: function() { this.feed = new RSS({ title: "Tim Branyen @tbranyen", description: "JavaScript and web technology updates.", feed_url: "", site_url: "", image_url: "", author: "Tim Branyen @tbranyen" }); } }); // Initialize a global reference to all Posts var posts = new Posts();

Adding items to the Feed:

RSS feed regeneration only needs to happen when the Posts collection is updated. Since the only mutation I do is reset, that event is sufficient to add items to the feed.

posts.bind("reset", function() { // Iterate over all the Posts and set the context to the Collection posts.each(function(post) { // Add each post into the feed property on the Collection this.feed.item({ title: post.get("title"), description: post.get("title"), date: post.get("posted"), url: "" + }); }, posts); });

Triggering the reset event:

The reset event is triggered under-the-hood by Backbone, whenever the Posts collection contents are replaced completely. Since I'm never going to incrementally add new Post models, this event will be called correctly whenever I trigger the fetch method. Internally Backbone will reset the new fetched data into the collection.

This updating is done with an interval that runs once an hour, outside of this code I have another place that I can manually trigger in the server to do the same thing (useful for when I push a new Post, like this!)

setInterval(function() { posts.reset([]); posts.fetch(); // Refresh Posts Collection once an hour }, 3600000); // Always fetch immediately posts.fetch();

Delivering the RSS:

Coming back to that RSS route, this is how the actual delivery function looks now. I simply have to set the correct Content-Type and call the xml method on the feed object.

site.get("/rss.xml", function(req, res) { res.contentType("rss"); res.send(posts.feed.xml()); });

As seen above the structure is really quite nice, and doesn't turn into callback hell since I'm using events. What do you think?