Discussion URLs: Opaque, Usable, and Readable «
»


Code: , , , , , , , , , ,
4 comments

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

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

Comments

  1. What is the impact on search engine spidering if the URLs are so dynamic? Could be a risk of Google flagging your site as having a lot of duplicate content.

  2. I directly addressed search engines in the article — it’s coded so that at any given time only one URL presents a page and all previously valid URLs redirect to it.

Leave a Reply

Your email address will not be published.