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.
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 myblog.com/posts/219. 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 myblog.com/posts/easy-mac-and-cheese 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 end
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]) end
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: