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:

human-readable
I should get some idea of what the discussion’s about when I hover over a link
permanent
bookmarks, incoming links, and search engines all need reliable URLs
editable
If a discussion drifts, it needs a new subject, which means it needs a new URL
opaque
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}

slug
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.
subject
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 end `{lang=”ruby”}

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) end end end

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

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

private 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] end