Dec 09

Date published: 12-9-2013

Rails version: 4.0.0

URLs and readability

One consideration when creating web sites with Rails that a developer should consider is how easy it is to navigate the site. One convention Rails uses for URLs is to have a path that includes the name of the class from the model along with the id of whichever specific item we want to look at. This is great for Rails as it makes it obvious which item we want to search for in our model, but from a human readability standpoint it’s not that great. This tutorial will show you what to add to your application to make your URLs more readable.

Pretty URLs

Pretty URLs

If you run any of the past tutorials I’ve written, you’ll notice they follow a pattern for the paths with the model name and the id number of the item we want to view. For example, in a blog application, our URL would look like Rails knows to map this the post with id 219, but if I’ve looked at several posts today and go back to my history to try to find something, this URL won’t be very helpful. It would be better if I had a URL like which tells me exactly what the post was about.

Copy the template site

The first thing we need to do is copy our template site and create a new project from it. Open a terminal window and switch to the directory where the template Bootstrap site is located. Run the following commands:

$ cp -r template_bootstrap_site/ pretty_urls
$ cd pretty_urls/

It’s a good idea now to change the module name just like we did in this tutorial in the section titled, “Update the module information.” Make sure to commit your changes to Git before moving on.

Create a scaffold

Normally I encourage making each all parts of the model, views, and controller from scratch, but for this tutorial I don’t think it’s necessary. I’m going to assume that we already have a valid blogging application that we want to add the ability to show the title of posts instead of the post_id in the URL. So I’m going to generate a scaffold for the Post model which includes the title of the post. If this were a real application it would have other things like the body of the post, publish date, tags, etc. We’re only interested in the title as it’s what we’ll use to generate the human readable portion of the URL. Go to the terminal window and run these commands:

$ rails generate scaffold Post title:string
      invoke  active_record
      create    db/migrate/20131209202454_create_posts.rb
      create    app/models/post.rb
      invoke    rspec
      create      spec/models/post_spec.rb
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      invoke    rspec
      create      spec/controllers/posts_controller_spec.rb
      create      spec/views/posts/edit.html.erb_spec.rb
      create      spec/views/posts/index.html.erb_spec.rb
      create      spec/views/posts/new.html.erb_spec.rb
      create      spec/views/posts/show.html.erb_spec.rb
      create      spec/routing/posts_routing_spec.rb
      invoke      rspec
      create        spec/requests/posts_spec.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      rspec
      create        spec/helpers/posts_helper_spec.rb
      invoke  assets
      invoke    js
      create      app/assets/javascripts/posts.js
      invoke    scss
      create      app/assets/stylesheets/posts.css.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss
$ rake db:migrate
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
   -> 0.0019s
==  CreatePosts: migrated (0.0020s) ===========================================

This has generated everything we need to create, edit, and delete posts. Let’s change the root path in config/routes.rb:

  root 'posts#index'

Now if you start the Rails server and load http://localhost:3000/ you’ll see a list of posts which is empty. Create a new post titled “Hello world” and it will redirect you to http://localhost:3000/posts/1 which is what we’re going to change to make more readable. Let’s save our changes in Git in case we run into problems later and want to revert back to this point. Type the following commands in the console:

$ git add .
$ git commit -m "Created scaffold for posts"

Add FriendlyId

FriendlyId is a gem that handles creation of slugs and interpreting human readable URLs in Rails. Open the Gemfile and add the following:

gem 'friendly_id', '5.0.1'

We need to run a few commands in the console to install the gem and start using it. Go to the console and run the following:

$ bundle install
$ rails generate friendly_id
      create  db/migrate/20131209205200_create_friendly_id_slugs.rb
      create  config/initializers/friendly_id.rb
$ rails generate migration AddSlugToPosts slug:string
      invoke  active_record
      create    db/migrate/20131209205255_add_slug_to_posts.rb

We need to add an index to our migration for the slugs to make it run faster when users visit the site. Go to the db/migrate/ folder and add the following line:

    add_index :posts, :slug, unique: true

We need to do a database migration, so go to the console and run the following commands:

$ rake db:migrate
==  CreateFriendlyIdSlugs: migrating ==========================================
-- create_table(:friendly_id_slugs)
   -> 0.0067s
-- add_index(:friendly_id_slugs, :sluggable_id)
   -> 0.0005s
-- add_index(:friendly_id_slugs, [:slug, :sluggable_type])
   -> 0.0005s
-- add_index(:friendly_id_slugs, [:slug, :sluggable_type, :scope], {:unique=>true})
   -> 0.0012s
-- add_index(:friendly_id_slugs, :sluggable_type)
   -> 0.0429s
==  CreateFriendlyIdSlugs: migrated (0.0571s) =================================

==  AddSlugToPosts: migrating =================================================
-- add_column(:posts, :slug, :string)
   -> 0.0015s
-- add_index(:posts, :slug, {:unique=>true})
   -> 0.0011s
==  AddSlugToPosts: migrated (0.0033s) ========================================
$ annotate
Annotated (1): Post

Let’s look at the Post model and add what we need to use FriendlyId for our post titles. Change app/models/post.rb to look like:

class Post < ActiveRecord::Base
  extend FriendlyId
  friendly_id :title, use: :slugged

That’s all we need for FriendlyId to know how to handle our Post model. Because we’ve already made a post, we need to do something to make sure it has a slug to use. Go back to the terminal window and type in the following commands:

$ rails console
Loading development environment (Rails 4.0.0)
2.0.0-p195 :001 > Post.find_each(&:save)
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1000
   (0.1ms)  begin transaction
  Post Exists (0.3ms)  SELECT 1 AS one FROM "posts" WHERE "posts"."slug" = 'hello-world' LIMIT 1
  SQL (5.4ms)  UPDATE "posts" SET "slug" = ?, "updated_at" = ? WHERE "posts"."id" = 1  [["slug", "hello-world"], ["updated_at", Mon, 09 Dec 2013 21:05:33 UTC +00:00]]
   (6.0ms)  commit transaction
 => nil
2.0.0-p195 :002 > Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
 => #<ActiveRecord::Relation [#<Post id: 1, title: "Hello world", created_at: "2013-12-09 20:26:57", updated_at: "2013-12-09 21:05:33", slug: "hello-world">]>
2.0.0-p195 :003 > exit

The Rails console allows us to make calls to modify the database. We are telling it to find all the Post objects in the database and save them because FriendlyId will run its own commands to create a slug for Posts that don’t have one on save. Then if we make a call to Post.all, we can see that our first post has a slug now.

We need to make one more small change. The controller needs a change so it knows how to look up the posts using FriendlyId instead of the post_id. In the controller, it’s been setup to call set_post before the show, edit, update, and destroy actions, so this is where we’ll make our change. In app/controllers/posts_controller.rb, change set_post to this:

    def set_post
      @post = Post.friendly.find(params[:id])

That’s it! Start the Rails server and try out viewing, updating, and deleting posts. Notice that on the show and edit pages, it uses the slug instead of the post_id in the URL. Let’s save our changes in Git:

$ git add .
$ git commit -m "Added FriendlyId to posts"

The source code for this tutorial can be found on Github at:

Leave a Reply

preload preload preload