Rolling Your Own RSS Feed with Express and Jade
RSS feeds are a great way to facilitate a loyal readership. In fact, as I write this, the RSS feed in David's sidebar is touting over 11,400 subscribers. Hitting the front page of Hacker News is always nice, but for most sites that's not going to translate into a reliable source of traffic. Getting each and every post in front of thousands of intentional subscribers though (who have their own followers on Twitter, Google+, etc.)? That's a traffic generator.
There's really only one gotcha to RSS feeds - you actually have to have one. A little over a month ago I launched a new blog, DevSmash. My shiny new site had been public for less than 48 hours when I received a tweet asking where my RSS feed was. Not if I had an RSS feed, but where was my RSS feed. You see, I had the good fortune of receiving some decent promotion on one of my first posts. Users started showing up, and evidently some of them looked for some means of subscribing. I have an RSS feed now, of course, but all those users who showed up in that first week or so - they're long gone. Moral of the story: RSS feeds are great, but you need it before your readers show up.
Alright - let's call that sufficient context. DevSmash is built on top of all the newfangled goodness you could ask for: Node.js, Express, Jade, Stylus, MongoDB, Mongoose, etc. It's a stack that I absolutely love hacking on, but it exposes an intentionally lean feature set, so "rolling your own xyz" often comes with the territory. Such was the case with my RSS feed. This post provides an overview of how I built the DevSmash RSS feed, and I hope that it will be useful to others building on this increasingly popular stack.
Cast of Characters
Before we get started, let's do a quick overview of the main technologies we'll be using:
Express
From the Express home page: "Express is a minimal and flexible node.js web application framework, providing a robust set of features for building single and multi-page, and hybrid web applications." What TJ Holowaychuk is too modest to say here, is that Express has become the de facto standard for building web apps on Node. There are of course other options, but you definitely owe it to yourself to check it out if you haven't already.
Website: http://expressjs.com/
Jade
From the Jade readme: "Jade is a high performance template engine heavily influenced by Haml and implemented with JavaScript for node." This is my go-to template engine of choice - terse, feature rich, and a syntax that reads as well as it writes.
Website: http://jade-lang.com/
Mongoose
From the Mongoose GitHub repo: "Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment." In other words, Mongoose provides a model layer for interacting with your MongoDB collections from Node.
Website: http://mongoosejs.com/
Note: It's relatively inconsequential that we're using Mongoose in this post. The concepts should translate well enough to however you're managing your persistence.
RSS Requirements
One last thing before we dig into the code - let's identify our basic requirements for our RSS feed:
- The feed should include the most recent 20 published posts.
- Output should be a valid RSS 2.0 feed (I'm personally using the W3C feed validator to verify).
Simple enough.
The Code
For the sake of this article, we only need to be concerned with three files:
blog-post.js
: Our BlogPost Mongoose Model (implementation details aren't too important for this article, but it's included for the sake of completeness).feed.js
: Our route handler (responsible for fetching posts from the database and feeding them to our view).rss.jade
: Our RSS template (responsible for turning our posts into a valid RSS 2.0 feed).
blog-post.js
We won't spend too much time talking about this file - it's purely here for reference since we'll be interacting with it later on.
var mongoose = require('mongoose'), Schema = mongoose.Schema; var BlogPostSchema = new Schema({ title: { type: String, required: true, trim: true }, slug: { type: String, required: true, lowercase: true, trim: true, index: { unique: true } }, body: { type: String, required: true }, teaser: { type: String, required: true }, author: { type: String, required: true, trim: true }, published: { type: Boolean, required: true, default: false }, createdAt: { type: Number }, updatedAt: { type: Number } }); // update timestamps on save BlogPostSchema.pre('save', function(next) { this.updatedAt = Date.now(); if (this.isNew) this.createdAt = this.updatedAt; next(); }); // create and export our model module.exports = mongoose.model('BlogPost', BlogPostSchema);
feed.js
A common Express convention for route handlers is to put them into a dedicated routes/
folder. In my own apps I generally have my route files export a single function that accepts the Express application instance, like so:
// some-route-handler.js module.exports = function(app) { app.get('/some/path', function(req, res, next) { // handler logic }); };
With a code structure like this in place, your main app.js
file simply needs a line like the following:
require('./routes/some-route-handler')(app);
Alright then, here's what a functional RSS handler actually looks like:
var BlogPost = require('../lib/model/blog-post'); module.exports = function(app) { app.get('/feed/rss', function(req, res) { BlogPost.find({}) .sort('-publishedAt') .where('published', true) .limit(20) .select('title slug publishedAt teaser') .exec(function(err, posts) { if (err) return next(err); return res.render('rss' { posts: posts }); }); }); };
As can be seen, there's not much need for cruft around populating our RSS feed. We simply query for the most recent 20 published posts, and render them with our RSS template. Which brings us to...
rss.jade
While Jade's primary use case is generating HTML output, it is just as handy at generating XML. Here's what our Jade template looks like:
doctype xml rss(version='2.0', xmlns:atom='<a href="http://www.w3.org/2005/Atom" rel="nofollow">http://www.w3.org/2005/Atom</a>') channel title DevSmash link <a href="http://devsmash.com" rel="nofollow">http://devsmash.com</a> atom:link(href='<a href="http://devsmash.com/feed/rss" rel="nofollow">http://devsmash.com/feed/rss</a>', rel='self', type='application/rss+xml') description Developers talking about stuff that developers like to talk about. language en-US if posts.length lastBuildDate= new Date(posts[0].publishedAt).toUTCString() each post in posts item title= post.title link <a href="http://devsmash.com/blog/#{post.slug}" rel="nofollow">http://devsmash.com/blog/#{post.slug}</a> description | <![CDATA[ | !{post.teaser} p: a(href='<a href="http://devsmash.com/blog/#{post.slug}')!=" rel="nofollow">http://devsmash.com/blog/#{post.slug}')!=</a> 'Read more »' | ]]> pubDate= new Date(post.publishedAt).toUTCString() guid(isPermaLink='false') <a href="http://devsmash.com/blog/#{post.slug}" rel="nofollow">http://devsmash.com/blog/#{post.slug}</a>
The Jade syntax may look a little foreign if this is your first time seeing it, but for the most part things are fairly self-explanatory. There are a few things worth pointing out though:
- The
atom
stuff isn't strictly required, but was suggested by the W3C feed validator. - Outputting the post body (or teaser in this case) requires some special care. You can't encode the markup, or you'll simply see encoded HTML in your RSS reader, but at the same time we need to protect the XML. The standard solution, then, is to wrap the post markup inside of
CDATA
tags.
And there you have it! Not even 40 lines of code (not counting the model) for a custom RSS feed. I hope this was helpful, and I'd love to hear any thoughts, questions, or concerns in the comments!
About Jeremy Martin
Jeremy Martin is a software developer and Open Source Evangelist at his day job, a Node.js contributor and MongoDB fan boy in his spare time, and husband to the greatest gal on the planet.
Thanks for the article – I am working on a site and currently using the node package ‘rss’ for the feeds side of things, but given how simple you make it look, I might try this instead.
Especially as the package does not support alternate link types – whereas I can do it just by amending the template :)
Thanks for the article, I am working on a site and used the ‘rss’ package, but would be good to extend it to support additional media links – I think I might just do what you have done instead :)