Pretty URLs With Restful Rails

Posted on February 13, 2007

Some Background

On one of our projects at PLANET ARGON, we have a requirement for displaying the username instead of the user model id in the url. For example:

http://ourapplication.com/users/1
http://ourapplication.com/users/1/assets 

should be

http://ourapplication.com/users/myusername
http://ourapplication.com/users/myusername/assets

The Resource Hacks Plugin

Luckily for us, Jeremy Voorhis already created a plugin resource_hacks.

Installing the plugin, allows us to add a member_path to our route definitions. Let's look at some examples.

Without the member path:

map.resources :users do |users|
	users.resources :assets
end

This gives us the default named routes associated with restful rails, such as users_url, assets_url(@user), etc.

Let's add the new member_path:

map.resources :users, :member_path => '/users/:permalink' do |users|
	users.resources :assets
end

This definition gives us access to a params[:permalink] in our users controller. Some examples:

http://ourapplication.com/users/myusername    => params[:permalink] would equal 'myusername'
http://ourapplication.com/users/simple-user   => params[:permalink] would equal 'simple-user'
http://ourapplication.com/users/1             => params[:permalink] would equal '1'

Our Next Steps

So how do we use this :permalink attribute, first we need to create a new model attribute for our user called permalink with a corresponding permalink column on the users table. Second we need to update our users controller to look for the permalink parameter instead of id parameter. Let's get started.

Updating Our User Model

Along with adding a new 'permalink' column on the User model, we also need to update the #to_param like so: def to_param "#{username.gsub(/[^a-z0-9]+/i, '-')}" if self.username end

The #to_param method is used by the named routes in restful rails. The default is to return the model id. What our definition says is to use the username instead of the model id, removing any unfriendly url characters and replacing them with a '-'. For example:

my.user.name => would become 'my-user-name'.

Updating Our User Controller

We need to update the way we find the user in the controller. Hopefully, we are using a before_filter that will find our user, so we only have to update the code in one place. For example:

class UsersController < ApplicationController
	before_filter :find_user

	private

	def find_user
	  @user = User.find(params[:id])
	end

end

The initial step in updating the user find method, is to use the permalink parameter instead of the id parameter, so let's do that:

def find_user
	@user = User.find_by_permalink(params[:permalink])
end

Simple, right? But what if we didn't used restful named routes in our link_to definitions, like so:

link_to "show", :controller => :users, :action => :show, :id => @user.id 

Which would render something like the following, if the user id is 1:

http://ourapplication.com/users/1

When our find_user method received this request, the permalink value would equal '1' and would in most cases not return a user. We can handle this with a little addition to our find_user method, like so:

def find_user
	@user = User.find_by_permalink(params[:permalink]) || User.find(params[:permalink])
end

With this update, if the User.find_by_permalink fails, then a call to User.find is made (hopefully with the model id).

Summarize

Using the resource_hacks plugin, we can get pretty urls with restful rails with a little bit of effort on our part.

NOTE: We would probably want to add some error checking in a real application :)

Comments
  1. Mark CareyFebruary 14, 2007 @ 02:40 PM
    For the find_user method, would it make more sense to test if params[:permalink] > 0 to save yourself the cost of an unnecessary DB quest (2 if it is an integer value)? Strings evaluate to 0 unless their first chars are ints so this should work in every case.
  2. Ingo WeissFebruary 14, 2007 @ 02:47 PM
    Hi, do you even need resource_hacks in this simple case? Wouldn't it be enough to overwrite to_param for the user model like this: class User < ActiveRecord::Base def to_param self.username end end and then in the controller use: @user = User.find_by_username(params[:id]) Ingo
  3. Graeme NelsonFebruary 14, 2007 @ 06:02 PM

    Mark: Nice suggestion. I don't currently do the check, since we changed everything to use the restful named routes, so a model id shouldn't come in as a permalink (but I guess its possible).

    Ingo: That would work if your usernames are url safe. In our case, they weren't. For example, my.username is a valid username in our case and it's not url safe.

  4. Sheila CrockerFebruary 15, 2007 @ 09:29 PM
    If I knew one tiny bite about what you are talking about, I'd post an intelligent comment. However, since I don't know what you are talking about, I'll just post this.