Discussion URLs: Opaque, Usable, and Readable «

Code: , , , , , , , , , ,
Comments Off on Discussion URLs: Opaque, Usable, and Readable

I just wrote about Human-Readable ActiveResource URLs, and now I want to examine one example of them more in-depth. Discussion forum URLs have several conflicting goals:

I should get some idea of what the discussion’s about when I hover over a link
bookmarks, incoming links, and search engines all need reliable URLs
If a discussion drifts, it needs a new subject, which means it needs a new URL
I’d rather not roll out the carpet for someone to scrape all the discussions

After a bit of pondering, I decided that URLs should look like this:

http://nearbygamers.com/discussions/{slug}{post count}{subject}

An unchanging random 5-character string.
post count
The number of posts in this discussion, so that when you look at the discussions index you know at a glance if you’ve read a thread because your browser colors visited links. I snagged this off the Joel.
Just the alphanumerics, dash-separated, updated as the subject is edited

So two parts (post count and subject) on a discussion can change, and the controller uses the slug to load the discussion and redirect to the correct URL if either has changed. Bookmarks and search engines can update, and outside links never stop working.

The Code

First, a little snippet of code to generate slugs lives in lib/slug.rb:

def random_slug(length=5)
chars = ("a".."z").to_a + ("A".."Z").to_a + ("1".."9").to_a
Array.new(length, '').collect{chars[rand(chars.size)]}.join

The Discussion model uses this to set a slug on itself if it doesn’t have one yet, and then uses that slug to generate URLs (note that I remove multiple and leading/trailing hyphens from the subjects):

require 'slug'

class Discussion < ActiveRecord::Base def to_param "#{slug}-#{posts.count}-#{subject.gsub(/[^[:alnum:]]+/i, '-').gsub(/-+/, '-').gsub(/^-|-$/, '')}" end protected def before_validation if self.slug.empty? begin self.slug = random_slug end while(Discussion.count(:conditions => ['slug = ?', self.slug]) > 0)

Finally, the DiscussionsController loads discussion objects, throws 404 errors, and redirects appropriately:

class DiscussionsController < ApplicationController before_filter :load_discussion, :except => [ :index ]

def load_discussion
slug = params[:id].split('-').first if params[:id]
@discussion = Discussion.find_by_slug(slug) if slug
raise ::ActionController::RoutingError, "Recognition failed for #{request.path}" if @discussion.nil?
redirect_to discussion_path(@discussion) and return if @discussion.to_param != params[:id]