Conversational and short URLs on Rails

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.

Add your comment 1 Comments

Post your message and we'll contact you immediately.
Thank you for your desire to work with us.

Please, fill out the required fields!

Close
OK