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.
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:
- Inspired by: mod_rewriting an entire site – workingwith.me.uk. Inspiration also taken from WordPress’
.htaccessrules. - Nice post: location.hash is dead. Long live HTML5 pushState! – badassJS
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.

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
Thanks for the article. Just wanted to comment that equivalent rewrite rule for HTML5 Pushstate support in Nginx is:
rewrite ^(.+)$ /index.html last;When I try this out, it works fine for top-level urls, like http://www.mydomain.com/resource. But when I call http://www.mydomain.com/deeply/nested/resource/123, the relative src attributes on all my elements resolve to “/deeply/nested/resource/js/scripts/myScript.js”. Thus the scripts don’t run, Backbone never loads and I’m left with a blank page.
Has anybody else run into that problem using the above technique?
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:
Super! thanks, this solved my headache :)
You said you’re not an Apache warrior but to me you are! Thanks a lot
Thanks Raph! Glad you liked it.
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?
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 thewindow.locationand figures out what to do.Hope this helps!
Hey I think this point is very important!
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]
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
For some reason the calls of urls in pushState method only accepts 3 characters change to open on a specific page, has any idea what might be?
following link: http://adrianmiranda.com.br/extranet/labs/bit/bar
Thanks! This is just what I needed!
The more I read about pushState, the more I am convinced it is a good thing, especially with fall backs to hashtags. Good guide, thanks.
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
I had a problem with your .htaccess, because all files and folders was redirected to index.html, but i did one script where solve my problem.
https://gist.github.com/gustavobeavis/4962250
Pingback: Javascript browser history manipulation | Script
Amazing. Thanks for the time saver! Works great!