Already a customer? Please sign in.

Backbone.js and Collections: Routers

by Zack Tollman on November 26, 2013 / 6 comments

The biggest challenge with building a Backbone.js powered theme for WordPress was handling routing. This post will explore those struggles and outline the solution used in Collections.

The Backbone.js router

Backbone.js routers connect permalinks with actions and events in a JavaScript (JS) application. When defining a router, individual “routes” are defined with the purpose of matching a permalink pattern and associating it with a JS function. As an example, for an app that employs a standard RESTful API, a typical permalink for a post might look like http://mysite.com/post/45 for the post with ID 45. In Backbone.js, this permalink can be matched by a route and sent to a function with the following code:

routes : {
    'post/:id' : 'getPost'
},

getPost: function( id ) {
    alert( id );
}

The getPost function will be called with 45 as the first argument when a visitor requests http://mysite.com/post/45.

Backbone.js is geared toward applications that utilize a RESTful API. With such applications, routes are straightforward and easy to implement. In the world of WordPress, this is where things get really tricky.

Routes in WordPress

WordPress supports custom rewrite rules, which allow 3rd party code to hijack the request routing and create unique URL patterns to return specific assets. This means that there are an infinite number of possibilities for a WordPress site’s permalink structure (or it could even have none!). A theme should work with a user’s permalink structure, no matter what that structure may be. This was a goal for Collections from the outset and it became a mammoth challenge.

Bolting a SPA onto a WordPress theme requires that the app can handle these permalink permutations. When dealing with routers in WordPress, it is extremely difficult to write routes for all of these possibilities as it would require porting WordPress’ regex based custom rewrite rules to Backbone.js’ token based routes. As such, Collections cannot predict what permalink structure will be used.

Even if one could write routes based on WordPress’ custom rewrite rules, the results would not be useful. Only two of the five default permalink structures involve a post ID. In other words, only two of these permalinks would allow the route to extract an ID, which can be used to query the server. As such, it is not that feasible to build routes for a WordPress theme.

As a last option, the theme could define its own permalink structure to something more RESTful; however, this is rife with compatibility issues, SEO problems, and is a poor decision overall. What to do?

The Collections solution

With Collections, we ended up avoiding the problem altogether. We used a “catch all” router that will match any URL and pass it on to the defined function.


routes: {
    '*notFound' : 'default',
    ''          : 'default'
},

default: function( pathname ) {
    alert( pathname );
}

'*notFound' will match any permalink and '' (a blank string) matches any requests to the root domain or any URLs with no paths after the root domain (e.g., http://mysite.com/?id=45).

Collectively, these routes will match all URLs, then call the default function in the Backbone.Router object. Instead of passing along nicely parsed arguments from the URL, the function just receives the pathname in the URL (i.e., everything but the root domain). We are effectively sidestepping the RESTful assumptions in Backbone.js.

Turning a pathname into JSON

With WordPress, it is quite easy to display a JSON representation of a normal page. You can just think of the JSON view as a different template for the data. To achieve this, we used the following code in Collections:


function collections_add_json_query_var( $vars ) {
    $vars[] = 'cspa-json';
    return $vars;
}

add_filter( 'query_vars', 'collections_add_json_query_var' );

function collections_json_template_redirect() {
    global $wp_query;

    // If this is not a request for json then bail
    if ( ! isset( $wp_query->query_vars['cspa-json'] ) || '1' !== $wp_query->query_vars['cspa-json'] ) {
        return;
    }

    // Set the appropriate header
    header( 'Content-Type: application/json; charset=utf-8' );

    // Help prevent MIME-type confusion attacks in IE8+
    send_nosniff_header();
    get_template_part( 'json', 'posts' );
    exit;
}

add_action( 'template_redirect', 'collections_json_template_redirect' );

First, a new query variable, cspa-json, is defined. Next, on template redirect, presence of that query variable is detected. If present, a JSON template is used to display the data. This allows the theme to tightly control the exact data that it returns when a new page is requested. This data is then used to render new views.

Another reason that we chose the template based method for the JSON data is that it will work seamlessly with popular page caching solutions used in the WordPress community. If we were to use the method of pinging the ajaxurl for every request, this would significantly slow the application response.

Putting it together

When the Single Page Application (SPA) in Collections is initiated and a new user browses to a new URL, the catch all route sends the pathname from the URL to the default function. The default function appends cspa-json=1 to the pathname passed to the function. An AJAX request, using Backbone.js internals, is made to the server using this compiled URL. Due to the presence of cspa-json=1, the JSON representation of the URL is returned. The JSON data is then used with an Underscore.js template to render to new view.

This approach allows us to avoid worrying about the permalink structure of the current site because it just uses the URLs that exist. Additionally, it removes the need for a fully featured RESTful API, which is currently a tough problem in WordPress. Finally, tight control over the JSON template allows us to better control what data is returned.

Mo’ SPA, mo’ problems

Of course, this wasn’t the only challenge with building a SPA in WordPress. In the next post in this series, I will discuss the issue of plugin compatibility.

Enjoy this post? Read more like it in From the workshop.

6 Comments

  1. Tim

    Interesting stuff. This is all very clever and have been keen to follow what you have been doing with backbone and wordpress. I’ve been running similar experiments myself, but I can’t help thinking that using WordPress at all at this point is a rather clumsy solution.

    SPAs are definitely the way to go since they offer much improved user experience, but trying to squeeze this model into the WordPress theme model looks like hard work. SEO also needs some attention.

    Keep up the good work!

  2. Zack Tollman

    Thanks Tim!

    Mixing a client side with a server side application is really tough. Given that WordPress is a server side application, you have to work within those constraints when bolting the SPA on top of it. We are really happy with how this turned out, but there were definitely some moments where the solution felt “clumsy”.

  3. Carl Danley

    Great article Zack! It’s really cool to see the solution you resolved to. I like the approach and it’s a great example of what it means to “think outside the box.” Keep up the great work!

  4. Zack Tollman

    Thanks Carl! It was a tough issue and I think the solution is very reasonable.

  5. Marko Jakic

    I’m not sure if understood this correctly, but when define routes with ‘/’ at end of each, it works.

    For example:
    ‘about-us/’: ‘aboutUs’

  6. Zack Tollman

    Hi Marko!

    You are absolutely correct! I definitely wasn’t clear about this, primarily because the issue is complex.

    If you hope to support the default permalink structure, the URLs look like:

    http://mydomain.com/?p=56

    Unfortunately, Backbone.js sees that route as simply “/”. Thus, “http://mydomain.com/?p=56″ and “http://mydomain.com/?p=79987″ are both seen as “/” and the router cannot differentiate between the two.

    This is not a problem if you control the permalink structure (e.g., you are building a client site), but with a theme, it should support all default permalink structures.

    I found it easiest to use a catch all router to solve this problem. I could then reconstruct the URL and initiate the request.

Post a comment

Basic HTML is allowed. Your email address will not be published.

Subscribe to this comment feed via RSS