Adding an RSS feed in Keystone.js

← back to the blog

I recently rebuilt my personal website on KeystoneJS and immediately fell in love with it. I think it's for the following reason: Keystone has successfully married NoSQL with the basic concept of the CMS. This means that any document type which can be envisioned (blog post, recipe, event, product) can just be defined as a schema from the very beginning, without the need to "override" any pre-existing assumptions about what that data might be.

At the same time, data models go way beyond defining field keys and their data-type. You can get a full-functioning CMS in minutes by setting field properties that control their appearance on the "back end" (by which I mean the administration panel).

Beyond that, the framework is just a slightly souped-up version of Express. You define your own routes, rendering engine, and all the front-end code. Nothing appears in the markup sent to the user unless you want it to be there.

Frankly, all things considered, it's a huge relief to go from WordPress to a lightweight, unopinionated system. I have control of the HTML my site emits, and we can Make the Web Great Again. Of course is a double-edged sword, and you might start missing things like RSS feeds, sitemaps, and the SEO features that come with a plugin like Yoast.

These things are not hard add, though. For RSS feeds, I found an npm module that worked with almost no setup. I simply set it up as one of my routes:

module.exports = function(req, res) {
  // define global features of the feed
  var feed = new RSS({
    title: 'Casey A. Ydenberg\'s blog feed',
    description: 'Web development and JavaScript',
    author: '@CAYdenberg'
  });

  // similar to the blog index page, we pull the most
  // recent 10 blog posts
  keystone.list('Post').model
    .where('state', 'published')
    .sort('-publishedDate')
    .limit(10)
    .exec()

    // define features for each blog post
    .then(posts => {
      posts.forEach(post => {
        feed.item({
          title: post.title,
          description: post.content.brief.html,
          url: post.canonicalUrl(),
          date: post.publishedDate
        });
      });

      // alter the HTTP header and send XML
      res.set('Content-Type', 'text/xml');
      res.send(feed.xml());
    });
}

You'll notice the function post.canonicalUrl() to get the URL of a post. Keystone does not have an obvious way to do this. In WordPress, this is easier because data models ("posts") automatically have a canonical URL (permalink) that they are associated with. MVC frameworks don't usually work this way: URLs (or routes) map to a controller or action, but that action could call up all kinds of different data to render the display.

(Of course, this is also a weakness of WordPress - when you try rendering "related posts" in the sidebar and end up writing batshit code like wp_rewind_posts ... ugh).

I decided to give my blog post model an extra function to generate the link.

Post.schema.methods.canonicalUrl = () => keystone.get('url') + '/blog/' + this.slug;

This is sort-of duplicating logic found in the router, but only sort-of. In fact, Keystone's standard set of templating "helpers" comes with just this sort of function for use in the HTML; I've just replaced it with a function associated with the data model (which makes more sense).

As a bonus, I used this function on the blog index page (instead of the helper):

<article class="blog-post">
  <h2 class="blog-post__title"><a href="{{canonicalUrl}}">{{{title}}}</a></h2>
  <div class="blog-post__content">{{{content.brief.html}}}</div>
  <p class="blog-post__read-more"><a href="{{canonicalUrl}}">Read more &gt;&gt;</a></p>
  <hr />
</article>