Creating Vanity URLs in Rails

railsBlog

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 %>

Or:

<%= 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

  end
end

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
    "#{id}-#{name}"
  end
end

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(" ", "-")  
  end

  def to_param
    "#{id}-#{slug}"
  end
end

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
    slug
  end
end

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 }
  end
end

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

Conclusion

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.

Free Workshops

Watch one of our expert, full-length teaching videos. Choose from either HTML, CSS or Wordpress.

Start learning

Andrew Chalkley

I'm an alien, I'm a legal alien, I'm an Englishman in Portland. All of my professional life I've worked with computers online. I'm a polyglot programmer and like using the right tools for the job. In my spare time I enjoy spending time with my young family and when I get chance, sticking opponents in Halo 4. You can find me in most places @chalkers.

Comments

16 comments on “Creating Vanity URLs in Rails

    • 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. 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])

  2. 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

  3. 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.

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

    after_create :update_slug
    before_update :assign_slug

    private

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

    def update_slug
    update_attributes slug: assign_slug
    end

    in your models.