23 AugHuman readable URLs with Friendly_id in rails 3
Tuesday, 23 August 2011 — 21:44If you follow rails standards, most of your routes for showing, editing, updating or deleting records from your tables, will depend on the record_id. For example, if you have users in your application, you can get the list of them by yourdomain.com/users. And you get a specific user with yourdomain.com/users/3, where 3 is the user.id value.
There are some cases, where it’s interesting to have human readable urls. Let’s think we want to have places than can be shared between friends. What would you prefer for sharing? a url like: yourdomain.com/places/times_square_new_york, or a url like yourdomain.com/places/1 ?
Just this is what i pretended with my project, and here i will explain how easy can it be to accomplish. Slugify is basically a term that defines the process of getting a human readable word from a string. In the previous case, we are getting the slug from place definition (Times Square, New York).
There are different alternatives available to accomplish this. You can get a list of the most popular ones here. I have to admit that i was looking at different alternatives, and i finally chose friendly_id because of its potential and popularity. I tested it and it really works fine to me.
Let’s see the steps:
1. Add the gem to your Gemfile and run bundle
+# Slug. https://github.com/norman/friendly_id +gem "friendly_id", "~> 4.0.0.beta8"
> bundle
2. Get our model ready. Let’s think the model is Places.
class Place < ActiveRecord::Base extend FriendlyId friendly_id :location, :use => :slugged end
3. Create a migration file for adding a new slug column to our model. You can use the special syntax for doing it automatically
rails generate migration add_slug_to_places slug:stringYou will get a migration file that adds that column automatically. But i finally edited it a little bit. Make it look like this:
class AddSlugToPlaces < ActiveRecord::Migration def self.up add_column :places, :slug, :string, :unique => true, :null => false Place.all.map(&:save) add_index :places, :slug, :unique => true end def self.down remove_index :places, :slug remove_column :places, :slug end end
I’m adding the new slug column to places table, as well as creating some constraints at database level. Then i’m re-saving all the existing Places i have in the database. With this, i’m making sure all the places are slugified. Before we save a new record, the slug is computed, so after adding the slug column to the table, i want to have it filled with the slug. If you don’t have any record yet on the table, you can comment that line out. I just had a couple of them, so for easiness i preferred to go that way. If you have more, i recommend to use a rake task for that.
This step is essential to accomplish the last action. We are creating a new index over the unique value of the slug. We can’t have two identical slugs in our application. So we firstly compute them, looking at location column. The gem will make sure they are not repeated.
4. Now you can ran migrations
rake db:migrate
5. Lunch your server and get ready for the magic. You are done!
rails s
Note aside. I like to have some before_filter actions on edit and destroy actions of model_controller. Those are really sensitive actions that you need to be really cautious. I like to make sure the user that pretends to remove or edit that particular record is the owner of that record. I’m doing that with something like in the controller
before_filter :validate_owner, :only => [:destroy, :edit] def validate_owner unless current_user.places.exists?(params[:id]) redirect_to(places_path) end end
Ok, it’s likely that you get redirected to places_path because of friendly_id gem. I reported that to Norman, and you can find all the info here
EDIT: As a temporal fix, you can replace the exists? with:
def validate_owner unless current_user.places.where("slug = :id OR id = :id", {:id => params[:id]}).limit(1).select(1) redirect_to(places_path) end end
24 Aug 13:25
Hello,
In most cases, you can use the ‘to_param’ method from your active record models, not needing a gem and the middleware it adds.
Just overwrite the ‘to_param’.
def to_param self.username #if unique endExample:
24 Aug 14:26
Thanks for the reminder. I also considered that approach, but changing the existing finders (at least in your controllers), from Model.find(params[:id]) to Model.find_by_username(params[:id]) looked to me a dirty hack. With friendly_id you don’t have to make changes on that, and find function works with both id and slug. There is even an option for keeping historical changes of your slugs so that all the slugs will point to the same record. That came by default on version 3 and now it’s optional in version 4.