Day 4 of the 30 Day Challenge
In the immortal words
or of Colonel Sanders, I’m too drunk to write this blog post. But eff it (note: I don’t know if Colonel Sanders said that part). Don’t take this as a knock that I don’t care about this blog post because I do. The HTML5 history API is one of my favorite things ever (and Tony Camp is one of my personal heroes). If the HTML5 history API (or Tony Camp) came up to me and said, “Hey baby, I work to spec in all the latest browsers and degrade gracefully in the others. What are you doing later?” I would say, “Writing some code. See you later.” I love it so much, I gave a talk about it at a previous Tag Soup meetup. But here I am going to tell you how to convert your current Octopress blog into an Octopress blog that uses HTML5 pushState (and how to report bugs to me where I messed up, because I definitely did).
But seriously I think the 4 turkey burgers (trying to watch my figure) I ate tonight match up well with the 5 vodka sodas (again, with the figure) I drank tonight, so we should be good to go to learn about some sweet, sweet HTML5 history.
Can I use this?
Short answer: yes. Long answer: maybe, but it’s complicated. The history API is great. Remember hashbangs (#!)? Of course you do, they are still in use at some big-time websites (including Twitter). That was a mistake. But there is a much better way. The history API enables you to change the entire URL (as long as it is the same domain) when you AJAX in some sweet new content. The part that is complicated goes by the ugly name of “partial browser support”.
I’m sure you all love feature detection. And I’m also sure that you love not being lied to. Now imagine if your feature detects lied to you. You would probably be upset. We all would. No one likes being lied to. So when Safari 5.1 is like “Oh what’s up Modernizr. The history API? I support that. No problem. Throw it at me.” You are gonna feel all warm and fuzzy inside until another supported browswer says “Oh yeah, I support that as well. But I do it differently than Safari. That browser is dumb. Seriously, that browser is so dumb it got hit by a parked car.” And you’re going to say, “Come on browsers. Can’t we all just get along. Don’t let native apps win!”
But there is a solution. Benjamin Lupton wrote History.js. It has bugs (including a bug where its pushState method won’t accept a URL with a hash which I have hit multiple times), but it also solves a lot of browser inconsistencies and it has tests. So I use it. This is a case where I don’t want to reinvent the wheel, so I’m going to use a well tested library that someone else has written. Yes, some of the bugs are a pain, but I’d rather start a project with it than without it.
Show me some code!
Oh yeah, sorry about that. I ramble when I’m drunk. So Octopress, you use it, you love it. It is static and fast. But you want to bypass the full page reloads. I have a solution for you (and I could use your help on improving it, see the last paragraph). Here is the basic flow that we want to follow in order to get some sweet JS that does what we need it to do:
- Capture internal link clicks
- Prevent the default action
- Use HTML pushState to register the interaction
- Capture that interaction by listening to an event
- Change our content
Sound easy enough? It is!
Note: This code requires jQuery, History.js and the History.js jQuery adapter.
I just said show me some code and you rambled again. Seriously, show me some code.
Yes. I promise I will show you some code. Let’s start with step 1. This is some code I yanked from Benjamin Lupton and tweaked a little to work a bit better with Octopress. This code is a jQuery helper to grab any internal links. The idea is that we are only going to want to attach our click handlers to internal links. The meat of this code is where we assign a boolean value to
isInternalLink. We are checking whether the href of the link is equal to the root url or if it doesn’t contain a colon. We also want to make sure that the href doesn’t start with a hash (therefore it being a named anchor) and also isn’t a link to the RSS feed.
1 2 3 4 5 6 7 8 9 10
Next, we want to to use another method I yanked from Benjamin. This is enables us to run the
ajaxify() function against jQuery objects. The only trick here, is the bug I talked about earlier. We have to separate the hash from the clicked url and pass it to the pushState event listener through the first parameter, which is a data object that can contain any arbitrary data.
Another note worth mentioning is the second parameter of the pushState method. That accepts a title for the new page, but as far as I know, no browsers support it. Also when using the
ajaxify function in this way, we don’t know the full title of the next page at this junction. This could be rearranged, but since the browsers don’t support it, I don’t see much reason for doing so
know now (especially since we will update the title manually later).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
The history.js plugin exposes a
statechange event to the
window. If we listen for that event, we can capture any state changes and bend them to our will. Here is the code I am using on my blog.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
History.getState() provides us with an object that contains the URL, title and data object from the link that was just clicked. Here is where it can get a bit hairy. HTML is HTML right? We should just be able to AJAX in the URL we want, parse out the specific content areas that interest us, and replace the old content areas with the new ones. Well, almost.
We are responsible for making the page exactly as it would appear if it came from the server. This includes adding/removing any body or content area classes, changing the title and running any JS that would happen on document ready.
For gists, the rub lies in where for each gist we include a script tag which write our code from GitHub to our page. This doesn’t that well dynamically since it uses
document.write is used after the page is already loaded, is empties the content of the page, and inserts its new content. Pretty selfish, right? Well
document.write may be an asshole, but that’s not the end of the world. I came up with a server-side solution for my blog, which will take some URL parameters and fetch the gist HTML for you. Ben Nadel also came with a solution earlier this year that uses an iFrame to override the
document.write method. I love his way of doing it, because it all stays client-side, but I think the server-side method is a bit safer (since you aren’t overriding a
In my method, I am locating any gists in the incoming HTML. If there are any, I ask my server-side script to supply me with the HTML for that gist. I am also pushing that result of that request (a JQuery Deferred object) to an array. Later I am asking that array to let me know when all of its gists have been loaded.
Once all gists have been loaded (or immediately if there were no gists), I take the new content, assign it an appropriate class and ajaxify its internal links (
Any other interactions? Bueller? Bueller?
There actually is! In our mobile media query, we use a select to change pages. This won’t work for us, since our code only captures the interaction of internal links
a elements. We need to change the
getNav function so the select will call pushState for us. My code is below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
THe new part is where we are binding the change of the select. If we have the history API at our disposal then we will call pushState, leaving
window.location.href as a fallback.
Woooo! Almost done.
disqus_function(dsScript)including the prior setting of
_gaq.push(['_trackPageview', ((relativeUrl.charAt(0) === '/')?'':'/')+relativeUrl])
The first function (
addCodeLineNumbers) already exists as a global function in
octopress.js. This is good (although polluting the global namespace is normally frowned upon) because we can just call the function to add line numbers to any gist we have available in our new content.
The second function pertains to the Disqus comments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Here, the main point I changed, was to assign the IIFE that ran the Disqus code to a function expression so I could call it with the appropriate script when it needed to be. I call it with
count.js if we are on the homepage and
embed.js if we are on a post page (and therefore embedding comment thread).
I also do the same thing with the tweet button.
1 2 3 4 5 6 7 8 9 10
I assign the code to a function expression, which I can then run at my convenience. Normally this type of code is meant to only be called on the initial loading of a page, but since we are accepting the role of Dr. Frankenstein and taking code and making it into our own monster, we have to make it callable from other places.
The last two things I am doing don’t require code changes from anywhere else, but are important nonetheless. The first is to change the page title. This isn’t the most important thing, but you know that feeling when you have tons of tabs open and you can’t tell what is what based on the titles. Don’t be hostile towards your users! Show them an accurate page title and they will be happy and come back to your blog to read about how awesome your cats are!
Lastly, I am telling Google Analytics to track the pageview. You know how important this is. The only trick I am pulling here, is to make sure that the URL I am sending to Google starts with a
/ since I believe that is required (not 100% sure about that).
Wake up! I’m about to do a recap.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
The only new stuff here is at the very top, where I am exiting the function and not running any of the goodness described above in the browser doesn’t support the HTML5 history API. The test is
!!(window.history && history.pushState). Once support is confirmed I am then calling everything inside a DOM ready callback.
If you include the codeblock directly above on your pages and replace the other codeblocks (disqus.html, twitter_sharing.html and octopress.js) you should be good to go. Also don’t forget, this code requires jQuery, History.js and the History.js jQuery Adapter.
But wait there’s more!
Your mileage may vary (YMMV). I know that sucks to hear but there is good news. I definitely did not cover the edge cases on this one. My blog doesn’t have any videos or some of the other Octorpress or Jekyll plugins. I would love to hear if you implement this code and find some JS that doesn’t work. Seriously, tell me where I am wrong or where my code falls short!
Peace out, and if you don’t chew Big Red, well it’s spicy and you’ll love it.