History API: Tips & Tricks

I’ve just made an update to my website so that page navigation takes advantage of the brand new history api which allows us to push history states to the browser without using hashes (like new twitter). There are great advantages of using the history api instead of hashes - your visitors see the same version of your site as search engines do so there is only one URL for any given page. Adding this functionality on to an existing site meant that I automatically used progressive enhancement - those that don’t have a modern browser, or even don’t have javascript can still use my website.

Along the way I encountered some hurdles and came up with some pretty nifty solutions. I thought I’d share some of them.

Detecting support for the history api

This one’s easy, just wrap any history api specific javascript in the following IF statement:

if (!!(window.history && history.pushState)) { ... }

If you want to know the basics of using the API, take a look at Dive Into HTML5 and Mozilla’s Documentation.

Stripping out the header and footer for Ajax requests

The most common use of the history api is probably for loading new content via Ajax. If you are loading new pages from the same website, you probably only need the page content, not the header and footer.
jQuery (and other frameworks) set a special header for their calls; HTTP_X_REQUESTED_WITH. So all we need to do is check for this header. If it isn’t set, then return the header and footer. In PHP, you can use the following code:

if(empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
    // Add our header and footer here.
}

Sending the page title via Ajax

So we’ve hijacked our link to load via Ajax and only returned the content, but what’s the page title? We could return the title within the content and extract it using javascript, but that seems like a bad approach. I’ve decided to send a custom header with any Ajax call. I’ve called it ‘History-Title’ (I made that up) and the value is our new page title. So my PHP looks like this:

if(empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
    // Add our header and footer here.
} else {
    header("History-Title: " . $title);
}

And I can retrieve this in my javascript:

$.ajax({
    url: this.href,
    success: function(data, textStatus, jqXHR) {
        document.title = jqXHR.getResponseHeader('History-Title');
        $content.html(data).fadeIn();
    }
});

Too easy right? That’s all there is to it.

Don’t be too aggressive

If you’re going to modify the behaviour of all same site links using this technique, make sure you let people open links in a new page/tab. We need to ignore clicks with a modifier key:

$('a').live('click', function(e) {
    if (e.which == 1 && !e.metaKey && !e.shiftKey) {
        ...
        return false;
    }
}

By binding the event handler using ‘live’ instead of ‘bind’ I can ensure that content loaded via Ajax also takes advantage of the history api.

Sorry subdomain, you weren’t invited to the party.

I was saddened to find that my tumblr blog (at blog.joshemerson.co.uk) couldn’t take advantage of the history api. One requirement of the api is that the url must be on the same domain. There isn’t a workaround for this, and for good reason - imagine the uses of providing a fake URL whilst loading a different page instead. I’ve moved my blog to Wordpress and now that it’s on the same domain it works very nicely.

One great thing about the relative newness of the history api is that the same old browsers that struggle with complex animations and javascript computation also get a simpler navigation experience. Most of the time progressive enhancement works out as a great solution for everyone.


Notice: Array to string conversion in /var/www/joshemerson.co.uk/public_html/site/plugins/tags.php on line 22

Fatal error: Uncaught Error: Function name must be a string in /var/www/joshemerson.co.uk/public_html/site/plugins/tags.php:22 Stack trace: #0 /var/www/joshemerson.co.uk/public_html/site/templates/post.php(12): tags(Object(page)) #1 /var/www/joshemerson.co.uk/public_html/kirby/lib/template.php(36): require('/var/www/joshem...') #2 /var/www/joshemerson.co.uk/public_html/kirby/lib/template.php(25): tpl::loadFile('/var/www/joshem...', Array, true) #3 /var/www/joshemerson.co.uk/public_html/kirby/lib/site.php(203): tpl::load('post', Array, true) #4 /var/www/joshemerson.co.uk/public_html/kirby/system.php(65): site->load() #5 /var/www/joshemerson.co.uk/public_html/index.php(71): require_once('/var/www/joshem...') #6 {main} thrown in /var/www/joshemerson.co.uk/public_html/site/plugins/tags.php on line 22