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 id
s 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.
thanks for the article. but how would one force the user to type in the correct name? if i do your solution, even though i type the incorrect name it still goes to the correct page. any ideas?
Nice Article, I’m just in need of this information.
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.
Nice end-to-end solution to this common problem. Thanks for sharing!
Thanks for the write up! Here’s another vanity slug helper that works across models and allows arbitrary scoping to subdomain, etc:
https://github.com/lemurheavy/vanity_slug
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.
Thank you for this step by step process 🙂 bugs puzzles, insects puzzles
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
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
I meant `resources :products, param: :slug`
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])
Nice article.
In case you were not aware, Rails has a nice helper method parameterize:
http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize
It’s really a shame that nobody knows about this.
good to know the method, thanks
OR you could just use Friendlyid https://github.com/FriendlyId/friendly_id
Thanks for sharing that! It is nice though to see how to do this natively. Helps me understand Ruby & Rails a bit better.
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.