Skip to content

Intelligent State Handling

SlexAxton edited this page Feb 11, 2011 · 35 revisions

Everyone seems to use the hashbang (#!) in URLs now... despite google recommending it - hashbangs are not a recommended best-practice - even when it comes to a high ranking in google.

Why Hashbangs Are Less Than Ideal

  1. Google is the only search engine which supports it.
  2. Pages that rely solely on hashbangs are not linkable for people with JavaScript disabled
  3. There are two urls for the same content - e.g. http://twitter.com/balupton and http://twitter.com/#!/balupton
  4. The urls get polluted in some cases when not started on the home page - e.g. http://www.facebook.com/balupton#!/balupton?sk=info

Common Pitfalls that Often Accompany Hashbangs

  1. Many popular sites don't put in the effort to make the site accessible to non-javascript users and other search engines - e.g. forcing people to use http://twitter.com/#!/balupton instead of http://twitter.com/balupton
  2. It is likely that custom controller actions will be coded to handle the ajax requests specifically - rather than AJAXing the original urls and returning just the appropriate content (the template and any data, not the full page).

Solving the Problems

Problems #1 and #5 are solved by coding your website normally (without any server-side ajax support), hooking into the hashchange, and querying the non-hashed version of the new hashed url:

var $content = $('#content');
// hook into http://mysite.com/#/page
$(window).bind('hashchange',function(){
	// ajax request http://mysite.com/page
	var url = document.location.protocol+'//'+(document.location.hostname||document.location.host)+'/'+document.location.hash;
	$.get(url,function(data){
		// find content in the page's html, and apply it to our current page's content
		$content.html($(data).find('#content'));
	}
}

The solution above is a great improvement, though it still suffers from the other problems. The problem of #6 would be the next logical step, as the current solution causes a lot of overhead (we just need the updated content, not the entire page), so let's solve that now with the following server-side code:

<?php
	// Our Page Action
	public function pageAction ( ) {
		// Prepare our variables for our view
		// ...
		
		// Handle our view
		return $this->render('page.html');
	}
	
	// Render Helper
	public function render ( $template ) {
		// Get the full template path
		$template_path = ...
		
		// Render the template
		$template_html = file_get_contents($template_path);
		
		// Check for the XHR header
		if ( IS_XHR ) {
			// We are a AJAX Request, return just the template
			$this->sendJson({'content':$template_html});
		}
		else {
			// Wrap the Template HTML with the Layout and proceed as normal
			// ...
		}
		
		// Done
	}

In fact, jQuery Ajaxy has supported these solutions for problems #1, #5 and #6 out of the box since July 2008. There is also a Zend Framework Action Helper to make solving problem #6 easier.

Though, this still suffers from the other problems. This is where the HTML5 History API comes to the rescue, as without that it is not possible to solving those problems. The HTML5 History API allows users to modify the URL directly, attach data and titles to it, without changing the page! Yay.

That's great, though each HTML5 Browser (Safari, Chrome and Firefox 4) all support it differently, so if you just use the native API available then you're going to come across a whole series of bugs. History.js has been made to provide a cross-compatible solution for the HTML5 browsers, as well as supporting HTML4 browsers by a hash (#) fallback (optional as of v1.5). We do not use a hashbang (#!) fallback as it would be useless as our website is already indexable by search engines.

So what would the ultimate solution look like that solves all these problems?

var $content = $('#content');
// hook into http://mysite.com/#/page
$(window).bind('statechange',function(){
	// ajax request http://mysite.com/page
	$.get(History.getState().url,function(data){
		// find content in the page's html, and apply it to our current page's content
		$content.html($(data).find('#content'));
	}
}

And bang you have just solved 5 out of 6 of all our problems. Problem 6 requires a server side effort - plugins are currently being made for the every darn CMS and Framework.

Notes

Okay, so that sounds too good to be true. What do we need to be aware of?

  • If you choose to support HTML4 browsers by using a hash fallback using History.js the use case of "A HTML4 JS-enabled user shares a url with a JS-disabled user, the url does not work." - the only way to solve this is by just supporting states in HTML5 browsers and not HTML4 browsers - so not using a hash fallback at all. There are some cases when using the fallback is appropriate and others where it's not, make the correct judgement call for your use-case.
  • If you have multiple areas on your website that change (more than just one div called #content), or if you want to utilise multiple levels of ajaxable pages (e.g. pages as well as sub-pages) for further optimisation and a better experience then this requires some more server side work. Currently there is only the HTML4 jQuery Ajaxy + Zend Framework solution mentioned earlier. Plugins for HTML5 History.js + every darn CMS and Framework are coming soon.

TL;DR

Hashbangs are not ideal. The intent behind the google crawlable ajax specification was so ajax'd content could be indexable by search engines. However, with html5 pushState and a little bit of work, you can get better results AND better compatibility, accessibility and maintainability.

The End.

Feel free to get in touch with me.

Benjamin Lupton

Like it. Share it.

Intelligent State Handling: Solutions for horrific hash/hashbang fallbacks and HTML5 History API (pushState) problems http://j.mp/etU7q6

Licensing

Copyright 2011 Benjamin Arthur Lupton Licensed under the Attribution-ShareAlike 3.0 Australia (CC BY-SA 3.0)

Clone this wiki locally