Vanity URLs with Rails

Rails default URL construction is very straightforward:

www.domain.com/controller/:id

which, in the specific case of a blog with one or more users, translates into e.g.

www.blog.com/users/1

There is obviously nothing wrong with that in terms of functionality, but the impersonal URLs are more difficult to remember, aren’t very useful for SEO, and just don’t have that warm personal touch that a personalized URL offers. So, how to go from

.../users/1

to

.../users/gemfile

?

The answer is not especially complicated but more cumbersome than one might think. (A very quick method is shown at the end of the article.)

Step 1. Creating a slug to hold the URL-acceptable version of a user’s name.

The User model gets a new attribute, slug:

$ rails generate migration add_slug_to_users slug:string
$ rake db:migrate
$ rake test:prepare

The slug is based on the user’s name in this example, which is probably the case 99% of the time:

app/models/user.rb:


class User < ActiveRecord::Base
.
  validates :slug, presence: true, uniqueness: { case_sensitive: false }
  before_validate :create_slug
.
.
  private
    def create_slug
      self.slug = name.parameterize
    end
end

The handy parameterize method converts an input string into something that can function as an acceptable part of a URL:

$ rails console
Loading development environment (Rails 4.0.2)
2.1.0 :001 > "Jéan-Claude & Vân D'amme".parameterize
=> "jean-claude-van-d-amme"

(Silly spelling used for illustration purposes.)

Creation of the slug can also be done manually during user creation/update. In this case, users are free to choose a slug that does not necessarily match their name and also get an easy opportunity to modify the slug if the one they chose already exists in the database. Or, the two methods can be combined into an “automated-with-choice” scenario.

In the case of my blog app, meant to host a small number of known users, no further validation is necessary (though probably still prudent). In other settings, caution must be used to ensure that a user’s slug does not conflict with a controller name, named route, etc. For more on this, see here.

It is important to note that the new code in the User model does not add slugs to existing users unless they go through an update. (Any new users created after this point will receive automatic slugs.) As valid slugs are about to become absolutely essential for the User resource to function, existing users need to get valid values in those fields. This can be accomplished via the browser-based updates to individual user records or programmatically via the Rails console:

$ rails console
Loading development environment (Rails 4.0.2)
2.1.0 :001 > User.all.each do |user|
2.1.0 :002 >     user.update_attribute(:slug, user.name.parameterize)
2.1.0 :003 > end

Successful execution of this code will return an array of all user records, wherein the new slug values can be seen. Alternatively, they can be examined directly:

2.1.0 :004 > User.find(1).slug
=> "gemfile"

Furthermore, for a deployed application, the same change will be necessary on the production server – in the case of Heroku, the console can be fired up via

$ heroku run console

Step 2. to_param

Rails models have a to_param method that returns a record’s database id by default. This is the “1” in “…/users/1”. The following overrides this default behaviour:

app/models/user.rb:

.
.
def to_param
  slug
end
.
.

While this means that the user’s slug will now show up in the URL in the place of “1”, it also necessitates a change in controller behaviour since the controller currently expects to be finding records by their database id. Thus, every instance of


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

has to become


@user = User.find_by(slug: params[:id])

Most if not all of these changes should be confined to the Users controller and thus relatively easy to implement. Finally, while the code works as shown, I find the use of “:id” in the params hash annoyingly inconsistent with the fact that it now holds the user slug. To fix this, the following change can be made:

config/routes.rb:

Blog::Application.routes.draw do
  .
  resources :users, param: :slug
  .
  .
end

With this, the user slug will now be stored in the params hash under “:slug” and not “:id” as previously, and we can write

@user = User.find_by(slug: params[:slug])

Step 3. Bonus points

My default choice of the name “user” for the person/blogger/… resource has been bothering me almost since I first made it without thinking. While renaming the model, controller and view files, changing all named routes, refactoring tests etc. would be just too much of a headache, there is a partial remedy: what shows up in the URL can be changed with just a few keystrokes.

config/routes.rb:

change

resources :users, param: :slug

to

resources :writers, as: :users, controller: 'users', param: :slug

With this, the name of the resource is now “writers” and that is what will show up in the URL:

www.blog.com/writers/gemfile

controller: ‘users’ tells the router which controller to call upon when a “writer” action is requested, while as: :users ensures that, for legacy reasons, all named routes retain their current names:

$ rake routes
      .
      .
      users GET    /writers(.:format)            users#index
            POST   /writers(.:format)            users#create
   new_user GET    /writers/new(.:format)        users#new
  edit_user GET    /writers/:slug/edit(.:format) users#edit
       user GET    /writers/:slug(.:format)      users#show
            PATCH  /writers/:slug(.:format)      users#update
            PUT    /writers/:slug(.:format)      users#update
            DELETE /writers/:slug(.:format)      users#destroy

The quick and not-so-dirty version (which is all you need sometimes):

Vanity URLs can be created with minimal effort by adding the following to a model file:

def to_param
 "#{id}-#{attribute_to_vanitize.parameterize}"
end

That is, to_param now returns the resource’s id, followed by a dash, and then the parameterized version of the attribute which will form the rest of the vanity URL. The reason this works without any further code modification has to do with how Rails’ methods function. to_param returns its result as a string, which is what allows the customization shown above. On the other hand, the find method called by the controller in Model.find(params[:id]) converts its argument to an integer, and when it does that only the numerical id portion of “#{id}-#{string}” survives:

$ rails console
2.1.0 :001 > "123-vanity-url".to_i
  => 123
2.1.0 :002 > Post.find(4) == Post.find("4-vanity-url")
  => true

The controller can thus continue finding records by their id and does not need to be altered. The end result is a browser URL that looks like this:

.../posts/214-vanity-urls-with-rails

#

Advertisements

2 thoughts on “Vanity URLs with Rails

  1. Great article Thank you! Exectly what i was looking for. But, how could i remove the /:controller/ part of the URL? so you had example.com/user-name. Any ideaS?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: