Creating RSS Feeds With A Light Hand

Sometimes when you're digging around for the "standard" solution to solve a particular problem in Drupal, you're gifted with the surprise of finding a hidden little treasure that's been lurking in the code all along. This happened to me recently while researching all the options for generating RSS feeds when I "discovered" the <a href="http://api.drupal.org/api/drupal/modules%21node%21node.module/function/node_feed/7" target="_blank">node_feed()</a> function. Sure, it's been around since version 4, and it works pretty much the same in versions 7 and 8 as it did initially, and Drupal uses it right out-of-the-box to give general RSS feeds for nodes, but it's still news to me -- and I'm so excited about it! In this post I'll look at the general use "context" (if you will) for this little gem, discuss a basic implementation scenario, give some caching advice, and suggest possible next steps if stricter display control is needed.

But Isn't That In The GUI Already?!

Anyone who's spent more than a day seriously working in Drupal knows that Views is the go-to solution for aggregating data, and generating feeds is no exception here; you can quickly spit out RSSs or CSVs or even aggregated .doc or .pdf files if you're so inclined. Further, using Views can simplify setting up and maintaining complex selection conditions or esoteric data relationships. However, if there's a problem with Views, it's that it can be a bit resource heavy at times, and if a site needs to be particularly streamlined for speed and efficiency while the project itself makes little use of aggregated data, then the team-lead may declare Views to be an anathema for that code base, and this was the situation I found myself in recently.

In contrast to creating a feed in Views, the node_feed() function can be a relatively inexpensive process, and it becomes even more efficient if caching mechanisms are effectively applied. If your data selection or formatting needs aren't easily supported by Views, then node_feed() is a clearly an option to consider, or if you're working on a project that needs to be kept streamlined and light-weight as possible, then this function may be the right solution for you -- especially if the nature of the project is such that Views are being avoided in general, since you don't want to be the one who wrote code that breaks that goal for the team, eh?

Gimme The Nitty Gritty Obligatory  Example

The general use case for this function is to generate content from a menu, usually filtering for particular data (e.g. based on a taxonomy, content type, author, etc.). This means implementing <a href="https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_menu/7" target="_blank">hook_menu()</a> along with a call-back function which outputs content via node_feed(); e.g.:

  1. /**
  2.  * Implement hook_menu().
  3.  */
  4. function my_module_menu() {
  5. $items = array();
  6.  
  7. $items['feed/%/%'] = array(
  8. 'title' => 'My Feed',
  9. 'page callback' => 'my_module_my_feeds',
  10. 'page arguments' => array(1, 2),
  11. 'access callback' => 'user_access',
  12. 'access arguments' => array('access content'),
  13. 'type' => MENU_CALLBACK,
  14. );
  15.  
  16. return $items;
  17. }

The page call-back now needs to assemble the list of nids to include in the feed and set up the feed itself. There's not much to say here that isn't covered in my in-line coding-comments:

  1. /**
  2.  * Return feed for indicated content.
  3.  */
  4. function my_module_my_feeds($arg1, $arg2) {
  5. // make double darn sure what's coming in is free from evil
  6. $arg1 = check_plain($arg1);
  7. $arg2 = check_plain($arg2);
  8.  
  9. // The content of $channel provides descriptions in the header of your RSS
  10. // feed; see the d.o. API docs for details.
  11. $channel = array(
  12. 'title' => 'MY RSS',
  13. 'link' => url(
  14. 'feed/' . $arg1 . '/' . $arg2,
  15. 'absolute' => TRUE,
  16. )
  17. ),
  18. 'description' => 'This is my super duper RSS feed',
  19. );
  20.  
  21. // The number_of_items variable might be stored in an admin screen, or simply
  22. // adjusted with drush calls at need.
  23. $number_of_items = variable_get('my_module_my_feeds_number_of_items', 10);
  24.  
  25. // Ultimately the $nids variable needs to be an array nid integers.
  26. $nids = db_select('node', 'n');
  27. $nids->join('some_table', 'st', 'n.nid = st.nid'); // the call to join is non-fluent, soo...
  28. $nids->fields('n', array('nid', 'created'));
  29. $nids->condition('n.status', 1);
  30. $nids->condition('st.property1', $arg1);
  31. $nids->condition('st.property2', $arg1);
  32. $nids->orderBy('n.created', 'DESC');
  33. $nids->range(0, $number_of_items);
  34. $nids->addTag('node_access');
  35. $nids = $nids->execute()->fetchCol();
  36.  
  37. // And that's it! Call the function du jour and let the magic happen! =o)
  38. node_feed($nids, $channel);
  39. }

Caching For Fun & Profit

When node_feed() executes, it fully loads all of the nodes from the list of $nids, and to generate that collection there are inevitably DB queries involved. Altogether, this creates a bit more of a processing load than can be tolerated for every single feed request on a high traffic site. There might be an argument to be made for allowing Drupal's standard page caching to deal with this on its own, but sometimes it's better to control this yourself. In such a case, <a href="https://api.drupal.org/api/drupal/includes%21cache.inc/function/cache_set/7" target="_blank">cache_set()</a> and <a href="https://api.drupal.org/api/drupal/includes%21cache.inc/function/cache_get/7" target="_blank">cache_get()</a> make this a breeze.

In the following example, code in the my_module_my_feeds() example is expanded to include exactly that type of caching.

  1. /**
  2.  * Return feed for indicated content.
  3.  */
  4. function my_module_my_feeds($arg1, $arg2) {
  5. $cid = 'my_module_my_feeds_' . $arg1 . '_' . $arg2;
  6.  
  7. if (
  8. ($cache = cache_get($cid))
  9. && is_object($cache)
  10. && !empty($cache->data)
  11. && time() < $cache->expire
  12. ) {
  13. drupal_add_http_header('Content-Type', 'application/rss+xml; charset=utf-8');
  14. print $cache->data;
  15. }
  16.  
  17. /// Generate $nids and $channel as above, but replace the call to node_feed()
  18. /// with the following:
  19.  
  20. // The call to node_feed() outputs content directly, so its output needs to
  21. // be captured so it can be cached.
  22.  
  23. // node_feed() sets the header, so it doesn't need to be manually set here as
  24. // it is when it's pulled from cache.
  25. node_feed($nids, $channel);
  26. $feed = ob_get_clean();
  27.  
  28. // The cache_expire_time variable might be stored in an admin screen or just
  29. // adjusted with drush calls at need; obviously, this code assumes the value
  30. // to be something parsable by <a href="http://php.net/manual/en/function.strtotime.php" target="_blank">strtotime()</a>.
  31. $cache_expire = strtotime(variable_get('my_module_my_feeds_cache_expire_time', '+2 hours'));
  32.  
  33. // Store the feed in cache for later use without all this work of generating
  34. // content.
  35. cache_set($cid, $feed, 'cache', $cache_expire);
  36.  
  37. print $feed;
  38.  
  39. return false;
  40. }

But It's Not Pretty Enough! =o(

Suppose that the markup being generated in RSS content isn't quite to your liking. There are a couple of ways to approach the problem of restructuring what's included.

One option would be to copy the existing node_feed() code to your own my_module_node_feed() function and make adjustments from there, obviously calling this new function from the code above. There are probably contexts in which this will be the appropriate response, but the more canonical and upgrade-friendly approach would be to re-theme the content using native mechanisms. You might note that node_feed() calls <a href="https://api.drupal.org/api/drupal/modules%21node%21node.module/function/node_view/7" target="_blank">node_view()</a> and passes 'rss' as its $view_mode argument, and it's useful to know that in turn this calls <a href="https://api.drupal.org/api/drupal/modules%21node%21node.module/function/node_build_content/7" target="_blank">node_build_content()</a> by passing that same argument. So, you could implement <a href="https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_entity_view_mode_alter/7" target="_blank">hook_entity_view_mode_alter()</a>, detect that the current URL structure matches the path to your feed, and change the $view_mode to something else; e.g. 'my_module_rss'. Strictly speaking, this step isn't necessary, but it makes the following suggestions a bit more streamlined.

From here, it's a simple enough matter from <a href="https://api.drupal.org/api/drupal/modules%21node%21node.api.php/function/hook_node_view/7" target="_blank">hook_node_view()</a> and/or <a href="https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_entity_view/7" target="_blank">hook_entity_view()</a> to modify content before rendering, but that can get a bit clumsy in some circumstances. A better approach might be to implement a node-preprocess function in a module or theme and add a template suggestion. If you've modified the view mode as suggested above then that could be used to detect changes at the theme level, or all of the code might be consolidated in that preprocess function; e.g.:

  1. /**
  2.  * Implement hook_preprocess_HOOK() for node.
  3.  */
  4. function my_module_preprocess_node(&$variables) {
  5. // if view_mode has been altered as described above:
  6. if('my_module_rss' == $variables['view_mode']) {
  7. $variables['theme_hook_suggestions'][] = 'node__my_module_rss';
  8. $variables['theme_hook_suggestions'][] = 'node__' . $variables['node']->type . '__my_module_rss';
  9. }
  10.  
  11. // if view_mode was not previously modified, then this might make sense:
  12. if(
  13. 'rss' == $variables['view_mode']
  14. && 'feed' == arg(0)
  15. ) {
  16. $variables['theme_hook_suggestions'][] = 'node__my_module_rss';
  17. $variables['theme_hook_suggestions'][] = 'node__' . $variables['node']->type . '__my_module_rss';
  18. // ... and perhaps you'll need special formatting with some args...
  19. if (!!($arg1 = arg(1))) {
  20. $variables['theme_hook_suggestions'][] = 'node__' . $variables['node']->type . '__my_module_rss__' . check_plain($arg1);
  21. if (!!($arg2 = arg(2))) {
  22. $variables['theme_hook_suggestions'][] = 'node__' . $variables['node']->type . '__my_module_rss__' . check_plain($arg1) . '__' . check_plain($arg2);
  23. }
  24. }
  25. }
  26. }

Obviously, you could then implement node--[type|nodeid]--my-module-rss--[arg1]--[arg2].tpl.php (or any of the other degrading variations just implemented) and output precisely the content you'd like to see in your RSS feed. =o)

And That's It In A Nutshell!

As you can see, node_feed() is fairly simple withal. Leveraged correctly, it provides sites with a great deal of flexibility even while remaining fairly light with processing. It's now my go-to solution for an RSS feed system, though I imagine I'll continue to use Views as and when appropriate.

Makes ya wonder though: What other wee gems are lying around in Drupal core just waiting to be discovered?! =oP Feel free to share your favorite Drupal gem in the comments! Learn more about RSS feeds with Rob Bates here.

Sean MacCath-Moran