Backwards Compatible HTML5 PushState using JavaScript

31st July 2014  -  by Alex Bimpson  -  0 Comments

I was recently building a new website that had to animate nicely between pages, rather than simply loading each page individually like a normal website. Obviously, the best solution to this is AJAX, whereby each page is dynamically loaded into the current window using javascript when it's required. This saves on loading unnecessary content, but allows you to animate the page however you want.

However, you immediately hit a few major issues, very similar to those that always plagued Flash-based websites. For starters, if you're loading content via AJAX then search engines can't crawl this content, as they won't execute javascript. Secondly, in the (increasingly rare) scenario that a user has javascript disabled, they won't be able to use your site either. And third, you don't get nice user-friendly URL's that allow people to share and link directly to specific pages on your site.

HTML 5 Pushstate

Well, that third problem is where HTML 5's pushstate functionality comes in useful. Essentially, pushstate allows you to change the URL in a browser using javascript, without actually reloading the page. This means you get to use individual URLs for every page, even though you're loading content dynamically with AJAX. People can use the browser's forwards and back buttons to navigate your site, just like normal, and they can save and share normal URL links to specific pages within your site.

Unfortunately, pushstate alone doesn't go very far to solving all our problems. It's all very well dynamically changing the URL, but there are still several things holding you back from the full desired experience:

  1. Your site needs programming to check the URL on load to ensure it immediately takes you to the right page.
  2. Even with pushstate, your site is not search engine crawlable unless you set it up properly - you're still loading content with javascript, which search engines won't do.
  3. And we haven't even got to the biggest hurdle yet: pushstate isn't supported in browsers that are more than a few years old, IE 9 included.

But, with a bit of careful consideration, a small dose of logic and a few late nights, I think I've solved all these problems, and thought I'd share to get your thoughts and ideas. I've tried to take care of everything, whilst making it all as efficient as possible.

Step 1 - Making Your Content Crawlable

Luckily, our first and second problems are really one and the same: search engines and users without javascript can't see content dynamically loaded with AJAX. Sure, you may have read some recommendations from other bloggers, or even Google, on using escaped fragments, or "hashbangs", but it's quite a nasty hack.

The best solution is to actually make your site work perfectly well without any javascript whatsoever. That's right, forget the AJAX for now - let's get things working just like the good old static sites of yesteryear. Then, you add the dynamic AJAX stuff on top. This approach is often referred to as "PJAX" (progressive AJAX) or "Hijax", because the site will work normally without javascript. If javascript is available, however, then we hijack any links to use AJAX instead.

Of course, we're going to bear in mind that this will go on to become an AJAX site, so we're going to be careful about the way we set up our static site.

File structure

I'm going to over-simplify things to try and make it clear - you can adapt these principles as required.

To start with, I'm using a php file, index.php, as my main site file. This contains my header, footer, nav and everything other than the page content that I'll be loading in with AJAX. Then, I've got a folder called html-pages which contains a separate PHP file for every page. You can put whatever content you want in these pages, ready to be loaded in with AJAX.

The names of these files must match the URL paths you'll be using. For example, for the page at http://mysite.com/about I would name the file about.php. In place of directory slashes, I'm using underscores in my file names, e.g. http://mysite.com/about/contact would be saved as about_contact.php.

URL rewriting with .htaccess

If you're not familiar with the basics of URL rewriting, you may want to check out this great overview on Smashing Mag. Essentially, it allows you to make it look like you're visiting a page at one URL, when in fact the content is being loaded from a completely different directory. What we want to do is let users think they're visiting nice user-friendly links, but actually always just load our index.php file.

To achieve this, we're going to upload a .htaccess file with the following content:

<IfModule mod_rewrite.c>
	##### REWRITE SETTINGS #####
	RewriteEngine on 
	RewriteBase /
	##### REWRITE RULES #####
	RewriteRule ^index.php$ - [L]
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule . /index.php [L]
</IfModule>

This will direct all dynamic page requests through your index.php file.

The PHP

On your index page, we're first going to detect the current page from the url. The following PHP snippet detects the current URL path and converts it to a page name for us. If the page name is blank, I'm telling it to use the 'home' page. As mentioned, I'll be replacing any forward slashes with an underscore for the benefit of our php page file names.

$page = str_replace('/','_',substr($_SERVER['REQUEST_URI'],1));
if($page == ''){
	$page = 'home';
}

Then, I'm including the content of the current specified path in the body of the index.php file, within a specific container div.

<div id="page_wrapper">
	include('html-pages/'.$page.'.php');
</div>

And that should do it. Your index.php file will now load in the correct content, meaning you should have a site that fully functions without javascript, and is therefore fully crawlable by Google and other search engines.

Step 2 - Backwards Compatibility

Let's look now at backwards compatibility. Of course, AJAX websites existed before the days of pushstate, so how were they generating their unique and shareable URL's? Hashbangs, of course! They're not pretty, and arguably not recommended, but that didn't stop them becoming commonplace across the web; even Twitter used hashbangs for a while. You'll likely recognise them - a hash symbol followed by an exclamation mark (#!) in the URL. Google even has a guide on how to make these pages crawlable, although we don't need to do that because we've made sure that our site already works without any javascript.

Anyway, I've written a basic javascript function which simply detects the availability of pushstate in your browser. Essentially, if a user who's browser doesn't support pushstate tries to visit one of our pretty URLs, it'll redirect them to a hashbang formatted URL instead, and vice versa.

var checkPushstate = {
	// CHECK PUSHSTATE AVAILABILITY
	available: false,
	check: function(){
		if(!!(window.history && history.pushState)){
			checkPushstate.available = true;
			checkPushstate.yes();
		}else{
			checkPushstate.available = false;
			checkPushstate.no();
		}
	},
	// IF PUSHSTATE IS SUPPORTED, CHECK FOR HASHBANGS IN URL
	yes: function(){
		if(window.location.hash){
			var u = window.location.hash;
			var location = '/' + u.replace('#!/','');
			window.location.replace(location);
		};
	},
	// IF PUSHSTATE IS NOT SUPPORTED, CHECK FOR URL PATH
	no: function(){
		var u = window.location.pathname;
		if(u!='' && !window.location.hash){
			var location = '/#!' + u;
			var location2 = 'http://test.kerve.co.uk'+location;
			window.location.replace(location);
		};
	}
};
checkPushstate.check();

Just make sure this script executes right at the top of your page before anything else loads, for speedy redirecting when required. This is pure javascript, so doesn't require jQuery to run.

Step 3 - Hijack Your Links

Now, the hijack itself. You simply need to add an onclick handler to your page links, either using the onclick="" attribute of the <a> tag, or using something like a jQuery .click handler.

For example, I'll give a class of "hijack" to all the links I want to hijack, and use jQuery to intercept them.

$('.hijax').click(function(){
	//GET FILE PATH
	var path = $(this).attr('href');
	//CONVERT TO FILE NAME
	var filename = path.substr(1, path.length); //remove first slash
	filename = filename.replace(/\//g,'_'); //replace slashes with underscores
	if(filename == ''){
		filename = 'home'; //if filename is blank, default to 'home'
	};
	//CHANGE URL
	if(checkPushstate.available==true){
		history.pushState(
			null, //data
			'My Website', //title
			'/'+filename //url
		);
	}else{
		window.location.hash = '!/'+filename;
	}
	//AJAX
	ajaxContent(filename);
	//PREVENT DEFAULT CLICK BEHAVIOUR
	return false;
});

This simply reads where the link is hard coded to take us, and instead passes this dynamically to the URL. Using the checkPushstate.available var from the previous snippet, we know whether to use pushstate or URL hashing. We then load in our content using AJAX - more on this later. Lastly, use a return false statement to prevent the link clicking through like normal and skipping our javascript.

Step 4 - Watch for URL Changes

Now, we watch for the history state or hash to change (whichever's applicable) and ensure that when it does, we tell our site it needs to load in a new page.

window.onpopstate = function(e){
	loadPage();
};
window.onhashchange = function(e){
	loadPage();
};

The loadPage function then reads in the URL in the browser, and the path into a page name that we can use to load in a corresponding file.

function loadPage(){
	//GET CURRENT URL PATH
	if(checkPushstate.available==true){
		var path = window.location.pathname.substring(1);
	}else{
		var path = window.location.hash.replace('#!/','');
	}
	//CONVERT TO FILE NAME
	var filename = path.replace(/\//g,'_'); //replace slashes with underscores
	if(filename == ''){
		filename = 'home'; //if filename is blank, default to 'home'
	};
	//AJAX
	ajaxContent(filename);
};

The ajaxContent function is down to you to write - this is where you use AJAX to load in the contents of the specified page when a hijacked link is clicked.

It'll probably be something like the following:

function ajaxContent(filename){
	$('#page_wrapper').load('/html-pages/'+filename+'.php', {}, function() {
		//your callback code here
	});
};

Step 5 - Loading the Right Page

So, we're all set with backwards compatibility, and search engines are loving our code. We've also got some really nice sharable URLs for each page. The final step is to ensure that when someone pastes one of those pretty URLs into their browser, we actually take them to the right place. We've already got our PHP loading the right content on page load, but in browsers that don't support pushstate our PHP can't read in our hash bang variables, so we'll need to take care of that with some javascript.

This is just a case of running the loadPage function on document ready. Easy!

$(document).ready(function(){
	if(checkPushstate.available!=true){
		loadPage();
	};
});

Wrap Up

Well, that's about it. I've uploaded the code to GitHub for you to use and edit if you wish, and you can check out a simple demo in action at hijax.prrple.com. Go ahead, try it out. Try disabling javascript, or using it in an old browser that doesn't support pushstate.

If anything doesn't quite make sense, or you can suggest improvements or a better way to do things, then please post in the comments!

by Alex Bimpson

Web designer and developer, and founder of Prrple. Works at Kerve, and studied Architecture at the University of Bath.

Comments

Keep Up To Date

Want to ensure you always catch our latest posts? You can follow us on Twitter, like us on Facebook, and even subscribe to a good old fashioned RSS feed. Just click the relevant links below to make sure you never miss a thing.