Adaptive features with Drupal modules: Case study with The New Republic

For the relaunch of The New Republic, we were tasked with building a cross-device read position syncing feature. The New Republic is a content-heavy site, where articles can run to 6,000 words or more. The Read Later feature would be a premium feature for subscribers that would automatically save the scroll position of the last article that they were reading, and return them to that same position when they loaded the site again — from any device.

We all know and love the power and diversity of Drupal contrib modules, but sometimes they can be more than we need, glitchy or just not quite suited to project specifications. At Alley Interactive, we try to keep our codebase clean and focused, to keep complexity in check. This is especially important for large, feature-rich builds like The New Republic, but crafting your own modules to tackle features can be good practice at any size.

We’ll look at a prototype of the Drupal module we developed for the cross-device syncing feature, and peek at some of the techniques we use to accomplish things with Drupal. You can check out the demo module on GitHub to dive into the code or watch it in action. This example will cover:

1. A basic REST-style API

2. Using drupal_add_js() to add some reactive properties to your UX

3. Leveraging Drupal.behaviors and Drupal.settings in js

The production module actually relied on a node.js server with web sockets to handle high traffic on a site like The New Republic, but for demonstration purposes, we’ll use built-in Drupal storage for the data model and API. We’ll skip over the specifics of setting up a data model, since depending on the application, you could get more milage out of a Drupal enitity, which makes it easier to display in other drupal contexts like Views or attached fields, as opposed to setting up a table in the database using hook_schema(), as I’ve done for the example (see read_later.install).

The “REST” API

Drupal isn’t an especially good place to do REST (things are looking up in D8!), but there are certain places where it can be useful. To set it up, we implement hook_menu() to define an endpoint, where we can add logic in our callback to handle the page request.

In read_later.module:

function read_later_menu() {
 return array(

   'read_later/%' => array(

     'page callback' => 'read_later_endpoint',

     'page arguments' => array(1),

     'type' => MENU_CALLBACK

Here we define the path which will be open to API requests, with a wildcard in the path to choose the action to perform in the callback. To save a user position, we could have to hit example.dev/read_later/save with the proper query data. The second argument in the path, “save,” is passed as the lone argument to the page callback.

function read_later_endpoint($action) {
 if ($action == ‘save’) {
   $variables = drupal_get_query_parameters();
   if ($variables[‘anchor’] > 0) {
     // Save the data

This is a simplified example of one possible callback path, where ‘save’ is the argument, and the query variable ‘anchor’ is a non-zero scroll position, so we save this record of user scroll position.

The Drupal.settings Object

To get our module to provide a highly customizable experience for the user, we need to expose some variables to page DOM that can be used with our javascript features. Sometimes we see this done by adding metadata to page elements in templates, which also works. However, the approach used in this example keeps things clean and contained by a simple pipe from our php callback to the javascript functions where the good stuff happens.

I’m talking about drupal_add_js(), which can be used in three forms to add javascript to your page: by file, by inline script, and by using the Drupal.settings object. We’re going to use and explain two of those cases in this module.

Let’s implement hook_preprocess_page() to perform some checks and add page variables that will be accessible to the page template file, and javascript settings that will be accessible in the DOM.

In read_later.module:

function read_later_preprocess_page(&$variables) {
 if (isset($variables[‘node’]) {
   if (user_is_logged_in()) {

We’re only going to worry about these actions for logged in users on node pages. We can add more logic here to restrict the javascript we expose to things like certain user roles or content types.

     global $user;
     drupal_add_js(drupal_get_path('module', 'read_later') . '/read-later.js');
     drupal_add_js(array(
       'readLater' => array(
         'uid' => $user->uid,
         'nid' => $node->nid,
         'anchor' => 0
       )
     ), 'setting');
     $variables['read_later_header'] = theme('read-later-header', array('user' => true));

Here’s where we add our custom javascript file, and the variables we want to make available. You can avoid namespace collisions by continuing to use the name you’ve chosen for your module when you extend Drupal.settings. Here we use “readLater” and populate the object with the three pieces of data we’ll need get our javascript working. This is where the user id enables us to retrieve a scroll position record, no matter where the user logs in from.

This feature also requires adding a jump link to the page when we’d like to return to our saved position. A theme template file provides markup for the header. Insert $read_later_header in your page.tpl.php file where you’d like the jump link to appear.

Drupal Settings & Behaviors

The final piece of the puzzle is on the javascript side — features we can run after the user has loaded the page. One part we’ve already taken care of: thanks to the drupal_add_js() calls in our page preprocess, we have extended the Drupal.settings DOM object with those custom values. Remembering the namespace we used, in javascript our values will look like:

var uid = Drupal.settings.readLater.uid;

 

We also have loaded a new javascript file to work in and add our features to the page, so let’s look at how we can use Drupal behaviors to initialize our feature, and provide a namespaced container for the other functions we’ll need to use.

In read-later.js:

Drupal.behaviors.readLater = {
 // Initialize the Read Later function
 attach: function (context, settings) {
   if (typeof Drupal.settings.readLater != undefined) {
     Drupal.behaviors.readLater.getPosition();

This extends Drupal.behaviors with the readLater namespace, giving us a couple of benefits: we can now easily access the methods in Drupal.behaviors.readLater in other javascript files, and by using the attach method, Drupal does the heavy lifting for us and runs the attach method when javascript context is updated — on page load, or if you use Drupal ajax commands.

 getPosition: function () {
   $.ajax({
     url: '/read_later/load',
     type: 'GET',
     data: {
       'uid': Drupal.settings.readLater.uid,
       'nid': Drupal.settings.readLater.nid
     },
     dataType: 'json',
     success: function (data, textStatus, jqXHR) {
       // Make the page move

Here’s the first method to run in attach for Drupal.behaviors.readLater. It tries to grab the scroll position record for this user and node, and then perform some actions, using the Drupal.settings.readLater object we created back in page preprocess. Here, when we get a non-zero scroll position, another method adds a click handler to the jump link, scrolling the user right back to where they left off, which brings us to the end of the demo.

There are also a few other methods in play at this point, so we can start sending some dynamic information to the API endpoint we defined earlier, evaluating page changes and saving data by extending the readLater behaviors object further.  Dig into the example module to find out how you can use the behaviors object to tackle your javascript the Drupal way.

The End

Being equally willing to write our own modules as to leverage a contrib module, depending on the specs for a specific project, helps Alley Interactive fulfill client requirements in the most expedient fashion, while keeping developers on any side of the project sane.

Read Later Example Module — http://github.com/dmachat/read_later_example