LearnConversational and short URLs on Rails

Treehouse
writes on January 28, 2010

Editors 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”.

Getting started

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?

Friendly and conversational

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

Making it work

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.

Here’s how we did it

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")],
         :order => "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.

Not quite perfect

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.

16 Responses to “Conversational and short URLs on Rails”

  1. What’s up, yeah this post is genuinely good and I have learned lot of things
    from it regarding blogging. thanks.

  2. Hi Jason,

    Great post. Very useful. One question: how did you design your links and navigation such that people could drill-down to get to an event? As in, on a page listing all of the Chicago events, what did the link_to code look like for getting them to the Chicago events on Design? Would it merge the parameters each time?

    thanks,
    Raviv

  3. Adrian M. on May 25, 2010 at 1:59 am said:

    Jason,

    Interesting post. This problem was already noticed by Tim Berners-Lee about 15 years ago with his work on “Matrix URIs”. Unfortunately his ideas didn’t make it to common web browsers.

    That said, I’ve also worked on variants of this idea. I’m wondering how do you generate these conversational URLs.

    Regards

  4. Edmund is absolutely correct. I’ve written a spec that exposes the error in decode: http://gist.github.com/331808

  5. Great article.

    In base58.rb file there is a bit of an error in the decode:

    decoded += multi * alphabet.index(n[i,i+1])

    should be:

    decoded += multi * alphabet.index(n[i,1])

    The former would only work up to 2 characters.

  6. Instead of that nasty looking for..in syntax, you can use the much more Ruby-ish each_slice, which will yield you successive pairs from an Enumerable:

    >> (1..10).each_slice(2) { |a,b| puts [a,b].inspect }
    [1, 2]
    [3, 4]
    [5, 6]
    [7, 8]
    [9, 10]

    • life is good when that for syntax is nasty. 🙂 how much code would that require in php?

      but this is tons cleaner, and i haven’t seen that technique. i’ll throw this in now…

      • It would require a similar amount of code in PHP, the difference here is that Ruby’s block syntax allows someone *else* to write the repetitive code which loops through the array and splits it out. It means that someone reading your code has a much better chance of seeing *what* you want to do, without having to understand the implementation detail of how you are doing it.

        Now I think of it, Rails provides a similar in_groups_of(x) method, which offers better handling of cases where your array doesn’t split exactly into groups of x – probably worth investigating.

  7. This is gold! I love these URL’s thanks for the post. It’s good to see some cool developers out of Salt Lake too. 🙂

  8. Next to these urls not being twitter friendly, google does not like them either.
    When Google has to dig in 4 or more subdirectories it thinks the page is less important or less worthy.

  9. This sounds like a really cool idea – on the SEO implications, it might be worth taking a look at the new canonical link meta tag, which allows multiple pages to pool their SEO ranking against a single URL.

    http://googlewebmastercentral.blogspot.com/2009/02/specify-your-canonical.html

  10. This is absolutely brilliant. Definitely the way forward. One question (forgive me for not being a ruby programmer…)

    Does this mean you can have these two URLs pointing to the same page:

    conferences/on/programming/in/new_york

    conferences/in/new_york/on/programming

    How do we address search engines crawling, what I guess it would see as, duplicate content?

    • Henrik, thanks for that link! I didn’t know about that. I’ll have to grab that plugin.

      And great point, Paul. Yes, they go to the same page. I’m no SEO expert, so I’m not sure about the search implications of that. A friend also mentioned that my underscores weren’t as Google-friendly as dashes. Any SEO gurus care to chime in?

  11. Rather than add a /h/ prefix, you could use a plugin like http://github.com/veilleperso/request_routing and add :conditions => { :domain => MY_SHORT_DOMAIN } to those routes. Saves a few characters.

Leave a Reply

You must be logged in to post a comment.

Learn to code with Treehouse

Start your 7 day free trial today and get access to hundreds of video courses in web development, design and business!

Learn more