HTML5 History / pushState URLs, .htaccess and You

This guide explains the issues with using JavaScript’s HTML5 History (pushState) URLs in Apache and covers how to set up your .htaccess file to handle URL routing.

If you’re not sure about pushState and the HTML5 History API, here’s a solid starter. If you just want the .htaccess code, you can skip the explanation.

So we’ve reached this point now where every half-decent JavaScripter knows their way around the pushState / replaceState History API and the related wrappers/plugins, and has a decent idea of browser support and cross-browser polyfills for HTML5 History. I think.

But at the same time, the very people who want to be using it are faced with a blight that, for love nor money, they can’t fix: HTML5 History URLs screw up Apache. And there are just no damn guides anywhere about how to make it work (until now!)

The Apache web server was designed long before the first murmurings of rewriting URLs without a page refresh were heard, and a certain amount of tomfoolery is required to make dynamically-updating URLs work seamlessly.

So without further ado, I present the problem and the starter-pack solution.

Apache doesn’t like you with your damn pushState

So, as you hopefully know by now, to navigate between different sections, screens or pages of a client-side JavaScript application, you’d traditionally use hash (#) or hash-bang (#!) fragments (e.g. .com/#shopping-cart or .com/#!/profile/yourmum/). The JS code picks up the hashchange event (either natively or via a custom listener/event in older browsers) and fires off a function in your app to switch out the content, or perform some killer action.

When you use HTML5 History, you don’t use hash fragments; instead, your JS app or website uses .pushState(), or you use your library/wrapper of choice, to navigate (update the URL and create an entry in the window history, enabling Back/Forward support) to e.g. .com/shopping-cart or .com/profile/yourmum/ – all without a page refresh.

Perfect. But… when you save/bookmark/send these URLs, then try to visit them a little later on, you may be disappointed to discover that they don’t exist. The horror!

Why don’t they exist?

Think about it – you’re trying to find a resource located at a specific URL. It has to be served through Apache. Apache is gonna look for that file, because it doesn’t have any idea that you’re looking for a dynamic resource, and even if it did, it wouldn’t know what to send back to help you find it!

Apache can handle HTML History/pushState with .htaccess

Apache was just being mean, earlier. It doesn’t have anything against pushState. In fact, it loves pushState: now every user, instead of requesting 20 pages in quick succession, requests only one page, with those 20 states being requested bit-by-bit (as smaller fragments, or however your site or JS app handles it.)

But it does choke a little bit.

As I mentioned before, it has to follow the rules: when a visitor requests a resource (or a location that looks like a resource) Apache has to look for it, first in the .htaccess file, if any, then in the filesystem to see if it can find a matching directory/file.

It won’t find it, so it throws a 404 error. “I wanted to help,” it mumbles, “I just couldn’t find the damn thing you wanted. Not my problem!”

So we gotta tell it where to look!

Where would it look? Well, where is the JavaScript app that’s going to check the URL and route the page/screen/section accordingly?

For most apps, it’s index.html, which would include in all the JavaScript required to handle this. For example, that’s the page containing your single-page app, in Backbone.js, or jQuery, or just (whoa) plain ol’ JavaScript.

And that’s what this solution assumes.

.htaccess rules for HTML5 pushState support:

So the solution: you add a few lines to your .htaccess file.

These lines check firstly whether the requested filename is NOT a static resource (such as an image, stylesheet, or other asset, so that they don’t all get redirected to index.html), then whether the requested resource is not already index.html (uh oh infinite loops!) and then finally redirects everything to – you guessed it – index.html.

# html5 pushstate (history) support:
<ifModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} !index
    RewriteRule (.*) index.html [L]
</ifModule>

There might even be a nicer way to write it. I’m no Apache warrior, so to speak, but this works for us.

What does this mean for the visitor?

Obviously, when on the site/single-page app, they don’t notice a thing. Everything works fine.

The kicker is that when they bookmark a page, or email a funny link to their friend, the next time that URL is opened (the one that was navigated to by pushState), Apache will simply direct the visitor to the homepage. At that point, the JavaScript app takes over. Boom.

This works for multiple-levels of nesting, too, which was a big pain-point when finding a solution. Thus the following URL redirects to index.html without a fuss:

.com/profiles/yourmum/images/?starttime=[stalkerishly long time ago]
  -> .com/index.html

At which point the JavaScript code would step in and do it’s nasty business.

Et voila. The .htaccess rules above may not work for everyone and YMMV, but it’s doing it for us.

Further reading:

Any comments? Suggestions, improvements, errors? You know what to do!

PS. I know, I know. “Don’t use Apache blah blah nginx blah.” I know. But people still use Apache.

# edit: Updated .htaccess rules to be a bit better, based on WordPress permalinks’ .htaccess rules.

19 thoughts on “HTML5 History / pushState URLs, .htaccess and You

  1. Ahmad Alfy

    I can’t thank you enough. Yesterday I was thinking of how to achieve this and you described it in an extremely easy way to understand.

    I am using backbone.js When declaring pushState to true, it will use the HTML5 new History API on modern browsers and degrade gracefully to use hash on older browsers.

    Backbone.history.start({  pushState: true, });

    I am in love with Backbone.js and the new HTML5 APIs <3

    Reply
    1. Joss Post author

      Yeah, this exact problem – it’s pretty common I think.

      Try adding a <base> tag in your <head> with the URL which your resources are relative to:

      <base href="http://www.example.com/app/" />
      Reply
  2. ericsoco

    i’m confused…doesn’t a mod_rewrite change the URL, so when it’s time for the js to “step in and do its nasty business”, window.location returns only ‘.com/index.html’?

    so then how would the js know about the state info that’s supposed to be encoded into the bookmarked url (e.g. /profile/yourmum/) if that gets ripped off the url by apache?

    Reply
    1. Joss Post author

      Hey Eric, great question.

      mod_rewrite doesn’t change the URL in this case (we’re using RewriteRule, not Redirect).

      So the URL in the browser bar stays the same as what you typed in – but on Apache, it’s serving up the content in index.html. That’s then the JS reads the window.location and figures out what to do.

      Hope this helps!

      Reply
  3. Michael Sharman

    For some reason this didn’t work for me, these rules did though:

    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
    RewriteRule (.*) /index.html [L,QSA]

    Reply
    1. Etienne

      Same thing for me, the only way to make it work:

      RewriteEngine On
      RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
      RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
      RewriteRule (.*) /index.html [L,QSA]

      Don’t forget the ‘/’ before index.html

      Reply
  4. Okyo

    Hello,

    Nice Tutorial!

    But i could not get into this really! If you redirect an url from mysite.com/page1/page12 -> to -> mysite.com, the page has 2 different states? redirecting to the homepage is not the aim, isnt it? I dont anderstand it … Would be nice if anyone can explain it to me, when i am wrong.
    thanx a lot
    okyo

    Reply
  5. Pingback: Javascript browser history manipulation | Script

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>