Site icon Treehouse Blog

Creating Vanity URLs in Rails

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.

Exit mobile version