The only constant is change, continuing change, inevitable change, that is the dominant factor in society [and web apps!] today. No sensible decision can be made any longer without taking into account not only the world as it is, but the world as it will be.
– Isaac Asimov
R.I.P #!
It did not take us long to discover we shared the concerns of Twitter’s Dan Webb on hashbang routes, but it was almost a year before we were able to remove them from Artsy. Here’s how it went down.
Artsy relies on the Backbone.js framework for our client application which offers a solid pushState routing scheme. This includes a seamless hashtag fallback for browsers that don’t support the HTML5 History API (looking at you IE 9).
The pushState routing is optional, but “the world as it should be” suggests we say “Yes!” (or true) to pushState.
Backbone.history.start({ pushState: true })
The Client
At Artsy, we had left Backbone out of the loop for most of our internal linking. Our markup href attributes all began with ‘/#!’ and expected the browser’s default hash behavior to keep the page from refreshing. For a proper pushState scheme, the app’s internal linking should begin with an absolute route. Backbone.js defaults to ‘/’, but this is configurable.
# Optional root attribute defaults to '/'
Backbone.history.start
pushState: true
root: "/specialized/client/"
Internal Links
All internal links need to begin with your configured root (‘/’ for Artsy).
Be sure to leave out your domain (http://artsy.net).
<a href="/">Home</a>
<a href="/artwork/matthew-abbott-lobby-and-supercomputer">My Favorite Work</a>
We now needed a global link handler that will leverage Backbone’s navigate
method which takes
care of updating the URL and avoiding a page refresh or alternatively wiring up the hashtag fallback.
Since we follow the convention of starting all href
attributes with our application’s root, we
can match on that in our selector to get all anchors whose link begins with our root, a[href^='/']
.
This link handler is a great place to ensure backward compatibility while #!s are removed from
internal links.
# Globally capture clicks. If they are internal and not in the pass
# through list, route them through Backbone's navigate method.
$(document).on "click", "a[href^='/']", (event) ->
href = $(event.currentTarget).attr('href')
# chain 'or's for other deny list routes
passThrough = href.indexOf('sign_out') >= 0
# Allow shift+click for new tabs, etc.
if !passThrough && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey
event.preventDefault()
# Remove leading slashes and hash bangs (backward compatablility)
url = href.replace(/^\//,'').replace('\#\!\/','')
# Instruct Backbone to trigger routing events
App.router.navigate url, { trigger: true }
return false
Thank you TenFarms for the excellent write up on proper link handling for pushState enabled browsers.
External Links
The application will need a small check early in the initialization process to redirect external links still expecting the #! routing scheme.
# Our Backbone App namespace
window.App =
# Namespace Backbone components
Models: {}
Collections: {}
Views: {}
redirectHashBang: ->
window.location = window.location.hash.substring(2)
# DOM is ready, are we routing a #!?
$ ->
if window.location.hash.indexOf('!') > -1
return App.redirectHashBang()
# else... continue on with initialization
The Server
Now that our app will receive requests to full URLs ‘https://artsy.net/artwork/mattew-abbott-lobby-and-supercomputer’ instead of ‘https://artsy.net/#!/artwork/mattew-abbott-lobby-and-supercomputer’, we need to update our Rails setup.
Below is an excerpt from our Rails application’s router.
Note references to our home and artworks controllers. Both use a before
filter
to determine a user’s authentication state and serve a different layout, with
unique assets or Backbone applications.
Controllers related to specific models now have the opportunity to bootstrap associated JSON or mark up and we now get expected 404 (file not found) error behavior without extra work required by a hash routing scheme.
# Server - Rails
Application.routes.draw do
root :to => "home#index"
# Controller logic determines the layout and could bootstrap data
resources :artworks, path: "artwork", only: :show
# Plural to singular redirect - mistakes happen!
get "/artworks/:id" => redirect('/artwork/%{id}')
# No match? Rails handles routing the 404 error.
end
An added bonus here is a near one to one mapping with the Rails and client routes.
# Backbone.js - Client
class App.Routers.Client extends Backbone.Router
routes:
'' : 'home'
'artwork/:id' : 'artwork'
'artworks/:id': 'redirectToArtwork'
URLs R 4 Ever
Dan Webb’s assertion that URLs are forever is correct, but so is Isaac Asimov’s statement on change. You can’t predict the future. You make decisions based on the best data you have at the time. We started our app with hashtag routing in early 2011 and added the ! around five months later (about the same time Dan Webb wrote his post). Had we started Artsy today, even six months ago, I’m confident we would have enabled Backbone’s pushState routing. There’s no need to look back. The future is here and its URLs are #! free!
Footnotes
- Backbone.js
- Google offers #! to aid the crawlability of AJAX hash routed applications
- Browser support for the HTML5 History API (aka pushState)
- Twitter advocates #!
- Dan Webb’s critique It’s About the Hashbangs
- Twitter ditches #!
Editor’s Note: This post has been updated as part of an effort to adopt more inclusive language across Artsy’s GitHub repositories and editorial content (RFC).
Comments