#314 Pretty URLs with FriendlyId
- Download:
- source codeProject Files in Zip (79.8 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (9.46 MB)
- webmFull Size VP8 Video (10.5 MB)
- ogvFull Size Theora Video (20.9 MB)
We have a simple Rails blogging application. Its home page shows a list of the most recent articles with a link to each one. If we click one of the links we’re taken to that article’s page but the URL isn’t very descriptive of its content.
The article is described in the URL only by its internal id
which is the default behaviour in Rails. We’d have better URLs if the article’s name was included somehow.
The easiest way to do this is to override the to_param
method in the model whose URLs we want to change, in this case Article
. This is an internal method that Rails uses to convert an object to a URL parameter.
class Article < ActiveRecord::Base def to_param "#{id} #{name}".parameterize end end
We’ve overridden this so that it returns the article’s name as well. We need to call parameterize
on the string to covert it to a URL-friendly value. This will change our article’s URL to this:
http://localhost:3000/articles/1-superman
It’s important when we do this that the object’s id
is at the beginning so that ActiveRecord’s find
method still works. If we don’t want the id
included in the URL it’s a bit more challenging to get this working, but it can be done.
Introducing FriendlyId
This is where the FriendlyId plugin comes in. This makes it easier to use the name of a model in a URL without having to prefix it with an id
. The gem has a number of features but before we look into them we’ll see what’s involved in adding it to our application. As ever, the first thing we’ll need to do is add it to our application’s gemfile and then run bundle
to install it.
source 'http://rubygems.org' gem 'rails', '3.1.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'friendly_id'
In the model instead of overriding to_param
we extend the model with the FriendlyId
module and define the attribute we want to use in the URL.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name end
Now our article will have a URL that includes the article’s name
but not its id
.
http://localhost:3000/articles/Superman
This approach isn’t particularly pretty for articles with more complex names, though; an article called “Batman & Robin” will get this URL:
http://localhost:3000/articles/Batman%20&%20Robin
Using Slugs
The URL is generated from the full name of the article including spaces and punctuation. This won’t be a problem if the parameter passed to FriendlyId is already URL-friendly, but this isn’t the case for our articles’ names so we’ll use a new slug attribute. We can do this by using friendly_id’s use option.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged end
This will look for a slug
column in the database’s articles
table so we’ll need to create it and we’ll create a migration to do so.
$ rails g migration add_slug_to_articles slug:string
It’s a good idea to add an index for this attribute as it will be used for finding records.
class AddSlugToArticles < ActiveRecord::Migration def change add_column :articles, :slug, :string add_index :articles, :slug end end
We can now run rake db:migrate
to add the column and index to the database table. There’s one more thing we need to do here: we already have some existing article records and their slug column won’t be filled in. To fix this we can open the Rails console and re-save each record.
1.9.2-p290 :001 > Article.find_each(&:save)
Now our “Batman & Robin” article will have a much nicer URL based on its new slug.
http://localhost:3000/articles/batman-robin
Handling Changed Article Names
If we edit an article and alter its name its slug will be updated as well. Changing our “Batman & Robin” article to be called “Batman & Robin 2” will change its slug to batman-robin-2
and therefore its URL. If we visit the article’s old URL now we’ll see an error message.
There are a couple of solutions for this problem. One is to instruct FriendlyId to not update the slug when an article’s name changes which we can do by overriding the should_generate_new_friendly_id?
method.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged def should_generate_new_friendly_id? new_record? end end
With this code in place the slug will only be generated when a new Article
is created. If we update our “Batman & Robin 2” article now so that its name is “Batman & Robin 3” its slug will remain batman-robin-2
.
What if we want the best of both worlds so that the slug updates when the name changes but older slugs are still recognised so that old URLs don’t break? We can do this by using FriendlyId’s history
option. We’ll need to remove the overriden should_generate_new_friendly_id?
method and add the history
option which will keep a record of the previous slugs.
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: [:slugged, :history] end
This history needs to be stored somewhere so we’ll create a new database table. FriendlyId provides a generator that will do this for us.
$ rails g friendly_id create db/migrate/20120101000001_create_friendly_id_slugs.rb
This will create a friendly_id_slugs
table once we’ve run rake db:migrate
.
There’s a gotcha involved with this, however: the history feature only seems to work on newly-created records. If we have any existing records we’ll need to regenerate them when we add this feature. We’ll create a new article called “Hello World” so that we can test this out. This article will have the following URL:
http://localhost:3000/articles/hello-world
If we edit the article and set the name to “Hello World 2” its slug will change to hello-world-2
but the original URL will still work.
It would be better if we were redirected to the current URL when we visit an out-of-date URL. To do this we’re going to need to make some changes to the controller. The ArticlesController
is a standard RESTful-style controller with the usual seven actions. We’ll change to the show action as this is the action that shows a single article. This is how it currently looks:
def show @article = Article.find(params[:id]) end
We want this action to redirect us to the article’s current URL if the URL used to visit the page isn’t the current one. We can do this by seeing if the path used to visit the page is not equal to the current article’s path. If this is the case then the user used an older slug or the article’s id
. In either of these cases we should redirect to the current URL.
def show @article = Article.find(params[:id]) if request.path != article_path(@article) redirect_to @article, status: :moved_permanently end end
If we visit http://localhost:3000/articles/hello-world now we’ll be redirected to that article’s current URL.
We’ve only covered a few of FriendlyId’s features here and you should read the documentation to see what else it can do. For example its Reserved module allows us to reserve certain keywords, such as new and edit, so that they’re not taken by a slug. The Scoped module lets us scope slugs within an association while the SimpleI18n module add internationalization support for multiple languages.