LearnCreating Vanity URLs in Rails

Andrew Chalkley
writes on May 21, 2013

Share with your friends


A vanity URL is a search engine optimized (SEO) URL. Instead of having a meaningless URL like http://storeofthefuture.com/products/2015 wouldn’t http://storeofthefuture.com/products/hoverboard be better and more meaningful?

It’s fairly straightforward to make URLs nicer in Rails. I’ll walk you through the process.

Baby Steps

In Rails the models have a method called to_param. This method gets called when you pass a model into a URL helper like:

<%= link_to product.name, product %>


<%= link_to product.name, product_path(product) %>

By default the to_param returns the id as a String rather than a Fixnum. Under the hood Rails is calling to_s on the id attribute, which allows us to override this with our own string. To do this we define the to_param method.

class Product < ActiveRecord::Base
  attr_accessible :description, :name

  def to_param


Within the to_param method, let’s set a string with the id followed by the name separated with a dash.

class Product < ActiveRecord::Base
  attr_accessible :description, :name

  def to_param

This would generate a URL of /products/2015-Hoverboard for a product with the name of “Hoverboard”.

Now what happens in the show action in the ProductsController? The params[:id] is the String of “2015-Hoverboard” and when you do a Product.find(params[:id]) the find method calls to_i. In Ruby when you call to_i on a String it grabs the first part of the String that looks like an integer. So Product.find("2015-Hoverboard") is exactly the same as calling Product.find(2015).

However, there’s an issue with names that contain a space. For example, a product with the name of “Flux Capacitor” would generate /1955-Flux%20Capacitor/, which isn’t very friendly. We should format the names so that we get them all lowercase and replace the spaces with dashes.

We can do that by creating a method called slug. The word slug describes the part of a URL that is made up of human-readable keywords.

class Product < ActiveRecord::Base
  attr_accessible :description, :name

  def slug
    name.downcase.gsub(" ", "-")  

  def to_param

So as you can see we just downcase the name and then gsub out the spaces with a dash and then swap the name with slug in the to_param.

So now the URLs are /2015-hoverboard and /1955-flux-capacitor.

Much nicer.

Taking it Further

Now we’ve done only a little bit of work and haven’t altered the database or the controller. Let’s say we wanted to ditch the ids completely from the URLs.

We’ll do this by generating a new attribute called slug, and then manually or programmatically update all the existing products to have a slug.

We’ll first need to create and run the migration.

rails g migration AddSlugToProducts slug:string
rake db:migrate

Then update your model to have :slug as an attr_accessible and validate its presence while ditching the slug method from the previous implementation. We can also just return the slug in the to_param method like this:

class Product < ActiveRecord::Base
  attr_accessible :description, :name, :slug

  validates_presence_of :slug

  def to_param

Next fire up your rails console and cycle through each product and update the slug in each existing product.

>> Product.all.each do |product|
?> product.slug = product.name.downcase.gsub(" ", "-")
>> product.save
>> end

Now that we’ve done that, we need to modify the controller. The find method looks up products by the id, so for all instances in your controller where you find(params[:id]) on the @product you’d want to find_by_slug(params[:id]). This is because the :id is defined in the routes.rb, not because it is the :id of the object.

$ rake routes
    product GET    /products/:id(.:format)      products#show

So in the show action in the ProductsController is should look something like this:

def show
  @product = Product.find_by_slug(params[:id])
  respond_to do |format|
    format.html # show.html.erb
    format.json { render json: @product }

Depending on how you’re using the model in other actions in your controller, you may need update those too.

You’ll also want to update any forms to include a slug field as this is now required to generate a new model. It’s required because without it you won’t be able to generate a URL or find any existing models.

This implementation of creating clear, human-readable and friendly URLs will yield the urls /products/hoverboard and /products/flux-capacitor, which is exactly what we wanted!

A Small Note on Performance

The id column of databases are normally indexed, if not the first things to be checked and indexed when performance issues occur. Finding by id is a fast method for looking them up. However if you have lots of models it may be worth adding an index on the slug to improve performance.

Here’s what you’d add to a migration:

add_index :products, :slug


Depending on how clean you want your URLs, or how much effort you want to go in to cleaning up existing URLs, either one of these strategies will work for you.

16 Responses to “Creating Vanity URLs in Rails”

    • Thanks for sharing that! It is nice though to see how to do this natively. Helps me understand Ruby & Rails a bit better.

    • Jules Copeland on May 28, 2013 at 7:02 am said:

      This is a great gem, but it’s always felt a bit heavy in terms of performance.

      The one thing it solves that hasn’t been mentioned yet though, is that when a slug changes, old links will still work.

      Most of the solutions posted here will return 404s or worse in the event that someone clicks an old link.

  1. ejstembler on May 22, 2013 at 1:27 pm said:

    Nice article.

    In case you were not aware, Rails has a nice helper method parameterize:


  2. 1. Rails has a parameterize method

    2. You have to validate the uniqueness of the slug

    3. Add a *unique* index to the slug column

    4. Use the param option in your route: resources :products, param: :title so that you can call Product.find_by(slug: params[:slug]) (Rails 4 syntax) or Product.find_by_slug(params[:slug]) (Rails 3 syntax) rather than Product.find_by_slug(params[:id])

  3. I meant `resources :products, param: :slug`

  4. Matt Potter on May 23, 2013 at 9:02 am said:

    I always use the Stringex gem for this. Very easy to use, and handles special characters, eg. converts $25 into the word ’25-dollars’, 25% in to ’25-percent’ etc. https://github.com/rsl/stringex

  5. Kalman Hazins on May 23, 2013 at 10:29 am said:

    Nice article!

    >> You’ll also want to update any forms to include a slug field as this is now required to generate a new >> model. It’s required because without it you won’t be able to generate a URL or find any existing
    >> models.

    You can also just keep the slug method around and automatically create a slug for your new record based on the :name (or whatever you used for the existing models) using before_create http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

  6. setiram on May 23, 2013 at 11:45 am said:

    Thank you for this step by step process :) bugs puzzles, insects puzzles

  7. chris stringer on May 23, 2013 at 12:46 pm said:

    Rather then populating the slug field via script/console, my suggestion would be to also do this in your migration so that the data changes are under version control.

  8. nickmerwin on May 23, 2013 at 2:28 pm said:

    Thanks for the write up! Here’s another vanity slug helper that works across models and allows arbitrary scoping to subdomain, etc:


  9. Nice end-to-end solution to this common problem. Thanks for sharing!

  10. You should assign slug after create, and update slug after update automatically by using this one:

    after_create :update_slug
    before_update :assign_slug


    def assign_slug
    self.slug = "#{ id }-#{ name.parameterize }"

    def update_slug
    update_attributes slug: assign_slug

    in your models.

  11. Nice Article, I’m just in need of this information.

Leave a Reply

Learn to code with Treehouse

Start your 14 day free trial today and get access to hundreds of video courses in web development, design and business!

Learn more