How to Implement Permalinks in a Rails Application with the Friendly ID Gem
This tutorial walks through how to implement permalinks into a Ruby on Rails application by integrating the friendly_id gem.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

This tutorial walks through how to implement permalinks into a Ruby on Rails application by integrating the friendly_id gem.

We're making really good progress so far. The next thing on our PivotalTracker list is "Friendly routes".

Friendly Routes

One of the best ways of understanding this concept is through an example. I have a DevCamp site open here, and I utilize friendly routes in the app. Notice how this is a different kind of routing system when compared to our application.

large

If you look at the URL, there are no ids. Instead we have words. URL-friendly words, which have dashes between them. If I had used the basic defaults for my routes, this URL would be something like "/posts/5" or "posts/500" or whatever the id is for that particular post. Instead I am using something called a slug. So, I can give a specific slug to any of these posts, and the slug is used as the id to look up the post in the database.

We are going to implement FriendlyID inside our blog. This is an SEO friendly or search engine optimized way of building an application. It is also better to have 'lookups' and URLs like this as opposed to numbers. Not to mention they are also more visually pleasing.

The Friendly ID Gem

For this optimization, we are going to integrate the friendly_id gem. Go to github.com/norman/friendly_id.
If you scroll all the way down, you will see they've give you a quick start guide on how to get this working in your application.

Rails Quickstart

rails new my_app
cd my_app
# Gemfile
gem 'friendly_id', '~> 5.1.0' # Note: You MUST use 5.0.0 or greater for Rails 4.0+
rails generate friendly_id
rails generate scaffold user name:string slug:string:uniq
rake db:migrate
# edit app/models/user.rb
class User < ApplicationRecord
  extend FriendlyId
  friendly_id :name, use: :slugged
end

User.create! name: "Joe Schmoe"

# Change User.find to User.friendly.find in your controller
User.friendly.find(params[:id])
rails server

GET http://localhost:3000/users/joe-schmoe
# If you're adding FriendlyId to an existing app and need
# to generate slugs for existing users, do this from the
# console, runner, or add a Rake task:
User.find_each(&:save)

So, the first thing we're going to do is bring in the gem. We have a whole section later in the course on how to integrate gems. For now, all you need to know is that a gem is a code file, or a set of code files, that will create specific functionality in your application.

Go to the code and open Gemfile. Scroll to the very bottom of the file and paste gem 'friendly_id', '~>5.1.0'

Now, switch back to the terminal and run bundle install.

This gem is now installed, and we are ready to start using it.

Documentation

When installing gems, one of the best ways to discover what you need to do to enable it, is to look at the documentation. It may sound like common sense, but I've had many students who have come to me to ask how to implement a particular functionality. Meanwhile there were answers right in the documentation, yet the student was unaware!

In this case, we can just follow the instructions in the documentation, and the gem will be easy to integrate.

So, the next thing we'll do is run the generator in the terminal with this code:

rails generate friendly_id

This will create a configuration file that will be called in future generators which we need this in order to have the system working. The generator will also give us a migration file and a new initializer. Let's have a look at both of those.

New Migration File

If you open the migration file, you'll see that friendly_id uses its own database table.

class CreateFriendlyIdSlugs < ActiveRecord::Migration
  def change
    create_table :friendly_id_slugs do |t|
      t.string   :slug,           :null => false
      t.integer  :sluggable_id,   :null => false
      t.string   :sluggable_type, :limit => 50
      t.string   :scope
      t.datetime :created_at
    end
    add_index :friendly_id_slugs, :sluggable_id
    add_index :friendly_id_slugs, [:slug, :sluggable_type]
    add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :unique => true
    add_index :friendly_id_slugs, :sluggable_type
  end
end

This table is full of slugs. As I mentioned, a slug is the item that goes in the URL bar. A slug has an ID, a type and a scope. These are items that we're not going to have to do anything with, but behind the scenes, friendly_id uses them, so it's important to know what is there. We will need to run a migration to get this installed in our schema file, but we won’t do that just yet.

The friendly_id Initializer

Next, open the initializer that was created config/initializers/friendly_id.rb

# FriendlyId Global Configuration
#
# Use this to set up shared configuration options for your entire application.
# Any of the configuration options shown here can also be applied to single
# models by passing arguments to the `friendly_id` class method or defining
# methods in your model.
#
# To learn more, check out the guide:
#
# http://norman.github.io/friendly_id/file.Guide.html
FriendlyId.defaults do |config|
  # ## Reserved Words
  #
  # Some words could conflict with Rails's routes when used as slugs, or are
  # undesirable to allow as slugs. Edit this list as needed for your app.
  config.use :reserved

  config.reserved_words = %w(new edit index session login logout users admin
    stylesheets assets javascripts images)

  #  ## Friendly Finders
  #
  # Uncomment this to use friendly finders in all models. By default, if
  # you wish to find a record by its friendly id, you must do:
  #
  #    MyModel.friendly.find('foo')
  #
  # If you uncomment this, you can do:
  #
  #    MyModel.find('foo')
  #
  # This is significantly more convenient but may not be appropriate for
  # all applications, so you must explicity opt-in to this behavior. You can
  # always also configure it on a per-model basis if you prefer.
  #
  # Something else to consider is that using the :finders addon boosts
  # performance because it will avoid Rails-internal code that makes runtime
  # calls to `Module.extend`.
  #
  # config.use :finders
  #
  # ## Slugs
  #
  # Most applications will use the :slugged module everywhere. If you wish
  # to do so, uncomment the following line.
  #
  # config.use :slugged
  #
  # By default, FriendlyId's :slugged addon expects the slug column to be named
  # 'slug', but you can change it if you wish.
  #
  # config.slug_column = 'slug'
  #
  # When FriendlyId can not generate a unique ID from your base method, it appends
  # a UUID, separated by a single dash. You can configure the character used as the
  # separator. If you're upgrading from FriendlyId 4, you may wish to replace this
  # with two dashes.
  #
  # config.sequence_separator = '-'
  #
  #  ## Tips and Tricks
  #
  #  ### Controlling when slugs are generated
  #
  # As of FriendlyId 5.0, new slugs are generated only when the slug field is
  # nil, but if you're using a column as your base method can change this
  # behavior by overriding the `should_generate_new_friendly_id` method that
  # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
  # more like 4.0.
  #
  # config.use Module.new {
  #   def should_generate_new_friendly_id?
  #     slug.blank? || <your_column_name_here>_changed?
  #   end
  # }
  #
  # FriendlyId uses Rails's `parameterize` method to generate slugs, but for
  # languages that don't use the Roman alphabet, that's not usually sufficient.
  # Here we use the Babosa library to transliterate Russian Cyrillic slugs to
  # ASCII. If you use this, don't forget to add "babosa" to your Gemfile.
  #
  # config.use Module.new {
  #   def normalize_friendly_id(text)
  #     text.to_slug.normalize! :transliterations => [:russian, :latin]
  #   end
  # }
end

This file is where you can set all kinds of configurations. For example, there are some reserved words

  config.reserved_words = %w(new edit index session login logout users admin stylesheets assets javascripts images)

These reserved words help to protect against any conflict. You can even add additional words here. For example, if you wanted to ensure that there was no profanity inside a URL as the slug, you could add those words here, and the initializer will prevent those words from being used as a part of a slug. This is good for security, and also gives better control over your site.

I'd definitely recommend exploring this file and its comments because they have some very effective configuration items that you can use. In this application, we're only going to use the default actions, but it's good to know that you can make these additional customizations.

Next, switch back to the terminal and migrate the database with the command
rails db:migrate

This will update our schema file so we can proceed.

Switching back to the documentation, you’ll notice that the quickstart guide gives you examples as if you were creating a new application. However we already have an application so we can skip that section. The next step is to run a scaffold.

rails generate friendly_id
- rails generate scaffold user name:string slug:string:uniq
rake db:migrate

We also do not need to scaffold anything because we already have our blog posts and we are going to use those as a case study. However, we do need to add some items to our blog scaffold, so we will modify this. While their scaffold calls for a user name and slug, which needs to be a string and unique, all we needs is the last bit, a slug that needs to be a string and unique.

Changing a Database Table

Whenever you want to add, change or take attributes away from a database table, you need to do a migration. We will use a migration generator. We will start with rails g migration and then add a description add_slug_to_blogs

Here, the naming is actually very important because there are certain keywords that rails looks for in this command. For example, the word add placed at the front of this string, indicates to Rails that we want to add an item to the database table.

This is a remarkably helpful generator. It’s possible to work without using this, as you can manually create the migration file, but this saves you a lot of time.

The last word in the string is the name of the table that you want to add it to. Everything that goes in-between is just a description and doesn’t matter as much, but the first word being add and the last one being the table name will help create a lot of code for you. After that, we'll paste the slug code from the documentation slug:string:uniq So, the complete code is:

rails g migration add_slug_to_blogs slug:string:uniq

Here, slug is the attribute to be added, string is the data type of the slug and the last word uniq is an optional parameter that indicates to the system that we want each slug to be unique. We obviously don't want two people to have the same slug because then, the database won't know which blog to show for which slug.

When you run the code, it'll create a migration file for us. In the migration file you'll see that it knows to add a column called slug of type string to the blogs table. This is because we used the word add at the beginning of the command and then ended it with the word blogs (notice the class name in the migration)

class AddSlugToBlogs < ActiveRecord::Migration[5.0]
  def change
    add_column :blogs, :slug, :string
    add_index :blogs, :slug, unique: true
  end
end

Now because we indicated unique, it is also creating an index. We'll talk more about index and what they do later. For now, understand that it allows the slug value to be unique. Index also allows for the look up to be very fast.

Next, let's run the migration rails db:migrate

Now we have a new attribute available for blogs. Open your schema.rb file and take a look at the blogs table.

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string   "slug"
    t.index ["slug"], name: "index_blogs_on_slug", unique: true, using: :btree
  end

Now we have a new attribute named slug, which has an index to insure that each will be given unique value.

There are two more things we need to do to get this working. Go to the documentation, and you'll see some code that needs to be added to the model. The documentation indicates to place the code in the user model. Of course, we're not applying this to the user model, rather we will add this to our blog model.

Copy the items inside from the documentation, then open your blog.rb file . You can use ‘command t’ to open a fuzzy search to find the file. Alternatively, you can find it in app/models/blog.rb. Now paste this code inside the method.

class Blog < ApplicationRecord
  extend FriendlyId
  friendly_id :name, use: :slugged
end

This code simply extends the FriendlyId. However, we need to make a change. We need to indicate title instead of name because title is the attribute we have for our blog.

- friendly_id :name, use: :slugged
+ friendly_id :title, use: :slugged

If you remember back to our schema file, this title is in reference to the blog title. Essentially, when we’ve chosen a blog title and hit save, the system will generate a title for us. Additionally, it will take the title and convert it into a slug. That's a pretty impressive. And the system does all of it automatically.

Let's test this out in our console. You can open the console with rails c. First, let’s create a blog with a title and body.
Blog.create!(title: “My great title”, body: "asdfasd”)

Our creation was successful. If you look at the last line of the returned data, you'll see that it created a slug for us: slug: “my-great-title". Notice how it put the title all in lower case and placed dashes in between the words. The slug is now compatible to be used in a URL bar.

Let's start the rails server. We are almost done implementing this friendly_id gem. Hopefully you can appreciate how easy this implementation is compared to building it out yourself. If you had to build this functionality in other programming languages like python or Java, it wouldn't be quite this easy!

Change the Lookup

Referring back to the gem documentation, the next step we need to take is to change the lookup. Before, we were using blogs.find(params[:id]) as our look up. Now we need to pass in the friendly item as indicated.

So, open the blogs_controller.rb. Go all the way down to our set_blog method, and change the code by adding the word friendly:

- @blog = Blog.find(params[:id])
+ @blog = Blog.friendly.find(params[:id])

This new code is overriding the default behavior. Before, the set blog method was looking in the params for an id, so it was looking for a number. If you don't add the word friendly to the method, the system will continue to look for an id. However, we don't have one, as it is now a slug, so if we don’t change this, the action will be muddled. By adding friendly to this method it will still look for the parameters, but now it will pull in the slug, map the slug to the id, and then go and find the item requested. This ‘friendly’ addition does the work for us and saves us from having to write quite a but if code. By placing it in the set method in this way, we won't have to change anything else in the controller!

With the server running, let's test it out. Before that though, we need to run all of our files again. I'll explain why.

First, open the rails console with the command rails c. Now, pull the first blog record with the imperative Blog.first.

large

Did you notice that the value of slug is nil? We need to have a slug for each item or this process we've created will not work. There are a few ways to correct this issue. If you go to the documentation, you'll see a piece of code.

# If you're adding FriendlyId to an existing app and need
# to generate slugs for existing users, do this from the
# console, runner, or add a Rake task:
User.find_each(&:save)

You can do something similar, except change the User to Blog. Another way is to run rails db:setup, as this action will create a new database for us and accomplish the same purpose. Notwithstanding, let’s go ahead and follow the directions from the documentation and run the code

Blog.find_each(&:save)

Essentially, this code will find each blog in the database and re-save it. Indeed, the way friendly works is each time a blog is saved it creates or updates the value of the slug. Even if we don't set the slug, and these are pre-existing posts, it will create and save a slug for each of them.

Now if you run Blog.first again, you'll see a slug value.

large

Some Ruby Code

Let's Start the rails server with rails s.

One thing to note: If the syntax here in the User.find_each(&:save) code is not clear, don't worry as we'll talk about it in detail later in the course. If you've taken my Ruby courses, you'll know that it's called passing a method to a block. Essentially this is iterating over each item in the collection, (in their case Users, in our case blogs) and calling save on each of them. Technically, we could have gone through the items one by one and re-saved each of them by hitting the save button, however that would've taken a lot of time. Using the pure Ruby code is a much faster way of doing it. It also gives us an efficient way to communicate with the data, iterate through the items, and run scripts pretty quickly.

Go to your browser and type "localhost:3000/blogs".

The blogs page will display all the blogs for us. If you click on a "show" link, you'll see that our URL is now a slug. With just a few lines of code and a few scripts that we ran, we we're able to get a blog that functions not only like a real-world type of blog, but we were able to make the change for pre-existing blogs as well. If you try to edit it, you can see the same slug is used again.

I wanted to included this friendly_id gem in this part of the course not only because it's effective and the URLs are more appropriate for a blog in the slug format, but also because it correlates directly to data flow management.

Data Flow Review

If you open routes.rb, you'll see that we have a resources of blogs

resources :blogs

We made no changes to this file whatsoever. Yet, when a user navigates to a route such as

/blogs/my-blog-post-1/edit

the portion my-blog-post-1 is a parameter that is passed in, through the routing system straight to the controller. If you like sports, you can think of it as a quarterback passing the ball to a wide receiver. In our case in the blogs_controller.rb file, we are passing the my-blog-post-1 parameter to the set_blog method as the (params[:id])

    def set_blog
      @blog = Blog.friendly.find(params[:id])
    end

Friendly is taking it and is running with it and handing it off to blog. Then we are able to store it as the instance of @blog. That entire blog database record inside the @blog variable can then be used in other actions like edit or show. This value is then passed to the view. As a matter of fact, it can be passed to any view file, some examples being edit or index or show, depending on the method that was triggered. If you open show.html.erb, you'll have the same instance variable, @blog, that is traveling all over the files.

These views have access to the variable because it all it started with the router, then it went to the controller, the controller communicated with the model, and finally delivered it to the view.

If you're already familiar with Rails, you may wonder why I keep reviewing this. First, it’s because this is the data flow section, and when it comes to data flow, this is how it works, and I want to focus on that. The other reason is that I've been teaching for many years now, and in this course, I wanted to focus on some common questions that I was getting from the students through the years. The concept of data flow, especially the MVC architecture, has been at the top of the list of questions from students because they don't understand it right away. So, by taking all these examples and dissecting them from all these different angles, such as configuring custom routes, using resources defaults, or passing hard-coded routes, hopefully you are starting to understand how the data flows into the system and how that data can be used.

Fantastic job going through that!

Let's look at the changes we made by running a git status. We’ve made a number of changes and we want to add all of them with git add . Next we will commit them with git commit -m “Integrated friendly routes for blog posts”. Last push it up git push origin portfolio-feature.

Our final step for this guide will be to update our project management software by checking off our friendly routes task. In our next guide we will cover how to create a custom action for updating a status!

Resources