Web development , php , ajax , symfony, framework, zend
In: web resources
31 Jan 2010Editors note: Interested in web apps? Join us at the Future of Web Apps Miami on February 24th to learn from companies such as Twitter, Facebook, Mint, Reddit and more. Buy your tickets now and get $50 off.
Friendly URLs are plenty popular these days, and are much more user-friendly than the cryptic URLs from five years ago. What’s the nerdiest URL you’ve ever written into a site? I can remember a few. Friendly URLs now save us from the numerical hell we were so fond of and got us used to a more human-friendly version. Instead of post=30 we now have “posts/i-love-ice-cream”.
A recent project of mine got me thinking a step further than these friendly links. I wanted the URL to be conversational. Not just English, something more like an English sentence.
True, friendly and conversational URLs move us away from the hierarchical file structure that our old URLs once promoted. But with sophisticated servers and frameworks, and with the new social ways we’re sharing links, file structures are virtually irrelevant, and a conversational URL is more valuable.
With Hulabalub.com , my new design and tech event listing website, I decided to structure the URLs to tell my readers what exactly they’re looking at. My site centers around events, so that was the natural start to the sentence. Events. Events what?
Since tech events all share a number of key characteristics (such as location, type, category, tag, speaker, etc), and since I knew my users would frequently search these exact terms to find events they’re interested in, I structured the URLs with these variables.
Instead of the formerly friendly “events/cities/chicago” or “events/categories/design”, I chose prepositions to describe the attributes of the query:
"events/in/chicago" or "events/on/design"
So with any number of filters added to my list of events, you can arrive at hundreds of these “sentences” that describe the page you’re looking at (and they can also be interchanged):
events/on/design/in/chicago
conferences/on/programming/in/new_york/about/ruby
meetups/on/entrepreneurship/in/san_francisco/with/fred_wilson
Once I’d designed this new structure, I then had to figure out how to implement it in Ruby. Luckily my trusty collaborator (and Rails Machine CTO) Jesse Newland was on hand to help me find this solution: first figure out how to handle the URLs, and second figure out how to process the query.
Ruby on Rails , my language of choice, has this notion of Routes, a file that helps the app know how to route requests. It’s an awesome way to customize your URL structures. But it’s not feasible to manually list out all combinations of my URLs in the routes file. There are literally hundreds of combinations. Jesse pointed me to a clever solution to dealing with strings of unplanned URLs. We called them Facets, and it works by taking the unknown string in your URL and saving it into an array so your app can act on it.
Please note if you’re a beginning Rails junkie, this tutorial may be a bit advanced.
First I put a line in my routes.rb file to handle this unplanned string:
map.connect ':type/*facets', :controller => 'events', :action => 'facets'
You’ll want to place this near the end of your routes file, with all your other specific routing rules before it so they take precedence.
In this example, I prefaced the array (which I called facets) with the type of event, such as “events” or “conferences”. The * tells Ruby to place everything after into an array called “facets”, and pass it into the controller and action you specify.
Next, I set up my Facets action in the Events Controller to handle this array, and transform it into instructions for the Model to process. In my events_controller.rb:
@events = Event.find_by_facets(params[:facets], params[:type])
I then created a new method in my Model, event.rb, called “find_by_facets”, to handle this call:
def self.find_by_facets(facets, type = nil)
valid_facets = %w(is in on with about under)
proxy = self
proxy = proxy.send("is", type.singularize.capitalize) unless (type.nil? || type == "events")
for i in (0..(facets.length - 1)).step(2)
if valid_facets.include?(facets[i])
proxy = proxy.send(facets[i].intern, facets[i+1]) unless facets[i] == "on" && facets[i+1] == "everything"
end
end
proxy
end
Find_by_facets takes in the array and steps through it, calling the (valid) actions on the Model with the attribute as a variable. In my code above, I first handle the event type, prepending an “is” action, and then stepping through the facets array by 2 to grab the action/variable pairs, stringing them together as a series of calls on the model.
For example, if my array is simply “conferences/”, this method will execute:
Event.is("conference")
And since Ruby is so badass, it will call them successively:
Event.is("conference").in("chicago").about("ruby").with("david_heinemeier_hansson")
But “is” and “in” and “with” aren’t built-in Model actions. So I had to create them, using a technique called “named_scope”. In the model (event.rb), for each new action, I put this function:
named_scope :in, lambda {
|city| {
:conditions => ["location = ? and startdate >=? and is_draft != 1", city.to_word.capitalize_words, DateTime::now().strftime("%Y-%m-%d")],
rder => "startdate asc"
}
}
This tells Rails how to handle Event.in(“chicago”), right down to the conditions and order_by attributes of the query. I repeated this same technique for the other prepositions (“on”, “about”, “with”), creating a handful of custom querying techniques to handle my custom URLs.
Conversational URLs may be easier to read, but are hardly Twitter-friendly. My Hulabalub URLs can quickly get to upwards of 70 characters. So I registered a shorter domain name (hlblb.com ) and wrote a custom URL shortener.
First I had to generate a unique and small string to match up to my unique event record. One popular method is to encode the record ID using a base of your choice, and translate it back when the short url is encountered. Flickr uses Base58, so I decided to as well. There are all sorts of sample code for this online for almost any language.
I created a Base58.rb file in my /lib/ folder and included and included the following line in environment.rb to make this library available:
require 'base58'
Inside Base58 I wrote 2 functions, encode and decode. Encode takes in the ID of my event and returns a unique string, and decode does the reverse. Note, this code was converted from DarkLaunch’s PHP functions .
Download Base58.rb (Please remove the .txt extension before adding to your project)
When I show events on the site, I’ll call the encode method in the Controller:
@shorturl = Base58.encode(@event.id)
and show the new Short URL in the View:
http://hlblb.com/h/<%= @shorturl %>
Next I’ve directed hlblb.com to the same Rails app that runs the main website. I’ve also appended all short URL’s with a /h/, so I can target it in the routes file without having the app think it’s a normal URL:
map.connect "/h/:id", :controller => "events", :action => "shorturl"
Next I’ve created this Shorturl method in the Controller to handle the call. Retrieving your record is as simple as decoding the shorturl and finding the event that matches that ID:
@event = Event.find Base58.decode(params[:id])
If successful, I then redirect the action to the full URL, including the original domain name, hulabalub.com:
if @event.nil?
render :action=> "notfound"
else
redirect_to "http://hulabalub.com" + @event.get_url
end
And there you have it. Conversational URLs with a bit of Rails magic, and Short URLs using some old fashioned math nerdery. These same tactics can be certainly achieved with other languages, and combine for a unique and eye-catching way to escort your users around your site.
This blog delivers stylish and dynamic news for designers and web-developers on all subjects of design, ranging from: CSS, Ajax, Javascript, web design, graphics, typography, advertising & much more. Our goal is to help you communicate effectively on the web with an engaging website or functional interface.
3 Responses to Conversational and short URLs on Rails
Riki
March 15th, 2010 at 6:08 pm
Many people on the site use tinyurl to post things, so if too many try it too often, it triggers the 'antispam' blocker and you get error999 when you post it. Sometimes it will work (it times out once in a while) but usually not. The same thing can happen with ANY overlyposted URL on here, including photobucket, youtube, etc.
Tiny
March 23rd, 2010 at 7:42 pm
I don't know, but let's have sex, you're hot.
Dienne
March 28th, 2010 at 8:50 pm
i tried it using my spare account just to be safe, and unfortunately, I got a failure notice. It said
""Recipient address rejected: User unknown in local recipient table""
Anyway, this could be considered as "cheating" and your account may be frozen just by the email address you are using.. which is definitely not good.