Conditionally Customize Your URLs in Rails
Our primary goal when building FeedStitch was to encourage users to share their feeds by making the process both simple and easily customizable. In the interest of simplicity, we went with OpenID (using RPX through the rpx_now plugin) to combine the registration and login processes so that users could quickly start creating feeds.
As part of encouraging sharing, we wanted to allow users to customize the URL that they could give out to others. The challenge here was keeping this functionality from interrupting the sign-in process while understanding that users may opt out of the customization step altogether.
We implemented this using a quick to_param trick - here's how you can do the same in your Rails application.
The User Model
$ ./script/generate model user username:string $ rake db:migrate
Our user model is simple - we only ask for an optional username. There are only a few modifications needed to make it useful:
class User < ActiveRecord::Base validates_presence_of :username, :on => :update validates_format_of :username, :with => /^[a-z]$/i, :on => :update def self.find_by_identifier(identifier) find(:first, :conditions => ['id = ? OR username = ?', identifier, identifier]) end def to_param username || id end end
This will allow us to find a user by either ID or username - the to_param magic will be required later.
The Users Controller
We'll need a list of users and a way to view an individual user's profile:
$ ./script/generate controller users index show
# config/routes.rb ActionController::Routing::Routes.draw do |map| map.user ':identifier', :controller => 'users', :action => 'show' map.root :controller => 'users', :action => 'index' end
Our controller is pretty standard, the only change is using our custom finder in the show action:
class UsersController < ApplicationController def index @users = User.all end def show @user = User.find_by_identifier(params[:identifier]) end end
The view logic is simple, we can use the user_path helper as usual:
# views/users/index.html.erb <% @users.each do |user| %> <p><%= link_to (user.username || 'N/A'), user_path(user) %></p> <% end %>
# views/users/show.html.erb <p>User Id: <%= @user.id %></p> <p>Username: <%= @user.username || 'N/A' %></p>
Testing it Out
Now that all the pieces are in place, create some users to see how this all comes together:
$ ./script/console >> User.create! >> User.create!(:username => 'reagent')
Hit the homepage and click on the links for the individual user pages. You'll see that the URL switches between the ID and username as appropriate.
How Does It Work?
The secret to this is how Rails generates the URL identifier when ActiveRecord instances are passed to a URL helper. The to_param method will return a username first and then fall back to returning the ID. This data is then exposed in the URL and gets picked up by the custom finder that we defined on the User model.
An interesting side effect of this process is that all the user profiles that have usernames are still available using their unique IDs. This can be prevented with a little more work, but the benefits of such an approach are minimal.