Categories

Single-Page Apps and HTML5 pushState

Posted on: August 3, 2015 by Dimitar Ivanov

Single-page apps and websites become more and more popular each day. Before building such an application, let's define how these apps should looks like and behave:

  • cross-browser support
  • bookmarkable links
  • working browser' Back/Forward buttons
  • seo-friendly links
  • crawlable application
  • works with disabled javascript

In my previous post (Crawlable AJAX Applications) I discussed how to get your ajax-driven app indexed by search engine crawlers and spiders using the hash-bang method. Now I intend to extend that topic by covering the HTML5 History pushState method.

History API

Open your browser' console and type window.history or simply history. In modern browsers you should see the following:

HTML5 History API

pushState

Pushes the given state onto the session history, with the given title, and, optionally, the given URL.

Syntax:
history.pushState(state, title, url);

Parameters:

state
A state object is an object representing a user interface state. When user navigates to a new state the popstate event is fired, and the state property of the event is equal to the history state object.
title
(currently ignored) A title for the state.
url
(optional) URL for the state. Both absolute and relative url's are accepted.
<script>
history.pushState(null, null, 'new-page.html');
// or
history.pushState({url: 'new-page.html'}, 'New Page');
// or
history.pushState({url: 'new-page.html', page: 2}, 'New Page', 'new-page.html');
</script>

popstate event

The popstate event is fired every time the current history entry changes. That happens when the user click on browser's Back/Forward buttons or programmatically by calling the history.back(), hitory.forward(), history.go() methods.

<script>
// jQuery
$(window).on('popstate', function (e) {
    var state = e.originalEvent.state;
    if (state !== null) {
        //load content with ajax
    }
});

// Vanilla javascript
window.addEventListener('popstate', function (e) {
    var state = e.state;
    if (state !== null) {
        //load content with ajax
    }
});
</script>

Working example

The next example demonstrates how the real single page app should looks like. See our working pushState demo. Source code is available on this GitHub repo.

<script>
$(function () {
    var load = function (url) {
        $.get(url).done(function (data) {
            $("#content").html(data);
        })
    };

    $(document).on('click', 'a', function (e) {
        e.preventDefault();

        var $this = $(this),
            url = $this.attr("href"),
            title = $this.text();

        history.pushState({
            url: url,
            title: title
        }, title, url);

        document.title = title;

        load(url);
    });

    $(window).on('popstate', function (e) {
        var state = e.originalEvent.state;
        if (state !== null) {
            document.title = state.title;
            load(state.url);
        } else {
            document.title = 'World Regions';
            $("#content").empty();
        }
    });
});
</script>

<a href="africa">Africa</a>
<a href="asia">Asia</a>
<a href="europe">Europe</a>
<a href="north-america">North America</a>
<a href="oceania">Oceania</a>
<a href="south-america">South America</a>
		
<div id="content"></div>

Single-page app SEO

You should differentiate the requests to the server and send proper content to the browser. For example: when request is made through XMLHttpRequest (AJAX requests) the response should contain only the requested content (specific portion of webpage). For regular requests you must sends whole webpage, e.g. html, head, body + page content.

Why pushState instead of hash-bangs

  • Clean URL's that doesn't broke the RFC 3986
  • Works even without javascript
  • Less care about crawling and indexing

In conclusion, since modern browsers are widely used, you should consider using the HTML5 pushState for building your killing single-page applications, and use #! hash-bang method if you intend to support old browsers. In fact, Facebook uses a dual approach - hash bangs for IE9 and pushState for modern browsers.

Browser compatibility

Chrome 5+, Firefox 4+, IE 10+, Safari 6+, Opera 11.5+

Make your website more secure by using the HTTP Headers for Wordpress, and never face a cross-origin issue again. Oh yes, it's FREE.
See also
References
Share this post

Share this post, then leave me a comment below with your thoughts about single page applications. Thanks so much for reading!


10 Comments

ralph
Nice and simple demo you've made from this. I have a small question tho'. In your anchors you have this <a href="africa">africa</a> How do you load the content from the africa file without an extension?

I can make it work with an extension such as africa.php in my own environment, but if I do a page refresh then it loads only the content of africa.php in the browser. How did you tackle this in your tutorial demo, because there a hard page refresh loads the content of africa in the #content div?

Thanks!
Dimitar Ivanov
Hi Ralph,

I've used a .htaccess file to rewrite the URLs.

RewriteEngine On
RewriteRule ^(africa|asia|europe|north-america|oceania|south-america)$ $1.php [L,NC]

But the most important is when you do AJAX requests to exclude the additional data from the response, e.g. header and footer.

$isXHR = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtoupper($_SERVER['HTTP_X_REQUESTED_WITH']) === 'XMLHTTPREQUEST';

if (!$isXHR) {
// header
}
// content
if (!$isXHR) {
// footer
}

Cheers,
Ralph
Thanks Dimitar!

I did figure out the .htaccess myself too and to tackle the hard refresh issue, I just have full static pages thus with html, head and body tags on my ajax pages and I just take out the content (from div.content) that I need to load in from those pages with the following:

var dynamic = $('.ajax-content'),
load = function (url) {
$.get(url).done(function (data) {
dynamic.html($(data).find('.content').html());
})
};

It's a bit cumbersome like this, because I have to maintain full static pages, but it did the job :)

I might look into your PHP snippet and try that too.

Thanks again!
Evgeniy
Hi Dimitar! Great helpful article. Thanks for that. Is there any way to use JQ only to load necessary page after refresh? I'm writing little site with 3 pages and don't want to use any back-end (all of the content is loading via load() function of jq). Thanks in advance!
Salam
Thank you so much i was looking around in google and i found too many complicated solution but you fixed it in two lines of code .. perfect
Bram de Hoop
I can't get it to work. i'am stuck at the .htaccess file.
Can i download the whole example somewhere?

tnx in advanced
ThePassenger
By any chance, could it be possible that you upload on github the full working demo, along with the htaccess? Il would be more than helpfull for many people.
Dimitar Ivanov
The source code of our demo is already available on GitHub: https://github.com/riverside/pushstate
Marco Muciño
Dimitar, but this way you must have two differents programs, right? The program with all the content (header and footer) and the program with just the content... this way if you want to change the content just be aware to change both programs, right? And if is a system full of functionality I think this way will be a nightmare. What do you think?
Dimitar Ivanov
Marco,

Indeed, this isn't necessary. If you're using a server-side language (PHP, Python etc.) you can easily split your codebase into separate files. That way the need to update your content on multiple places will disappear. See the example on the GitHub repository.

Leave a comment

Captcha