# Friendly URLs with the FriendlyId gem

Friendly or “pretty” URLs are addresses that are unique, human-readable, and meant to conceptualize the content that lives within them.

They are easier to share, remember, and they help search engines better understand our site.

In this article, **we'll explore how to implement the pretty URL feature in a Rails application** with and without using the FriendlyId gem.

The goal is to learn what FriendlyId does for us behind the scenes. At the same time we learn how to implement a basic version of those features so we can get rid of the dependency if we think it's reasonable.

Let's start:
## Why do we need friendly URLs?

There are several reasons to use friendly URLs in our applications.

The first is that **they're easier to share and remember** since they consist of recognizable words.

The second reason is that **search engines favor them** because they help their algorithms better understand the content.

For example, a `/how-to-make-bread` path is more relevant to a bread-making tutorial than `/tutorials/151` or the more generic `/bread`.

Another reason to use friendly URLs is that **they hide sequential resources.**

Because `ids`are auto-incremented at the database level, it's easy to infer that `/resources/10` was created before `/resources/11` and you may not want to share that information with the world.

This is particularly important in situations where this information can be used to extract data about your business or your users.

If an e-commerce app exposes the sequential `ids` for their orders in the 'thank you' page or in the after-checkout email, we could place a couple of orders in a month and extrapolate the amount of orders they receive per-month.

{{info}}
**TIL:** The term 'slug' commonly used to refer to the unique and readable part of a pretty URL, traces its origin to the traditional typesetting and printing process. It actually **referred to a piece of metal** used in linotype machines to hold characters or words for printing. It evolved in journalism and publishing to refer to a short and unique label used given to articles or stories.
{{/info}}

## The default URL behavior in Rails

Without any customization, Rails uses the resource `id` to uniquely identify it. That's why IDs auto-incremental and unique by default.

When we define routes in our application, we also match the resources to a unique `id`:

```ruby
# config/routes.rb
resources :articles, only: [:show]
```

When making a request to `/articles/10` the params hash will have the `id` key set to '10', and we use that to retrieve the desired `Article` from the database.

```ruby
# app/controllers/articles_controller.rb
def show
  @article = Article.find(params[:id]) # Returns the Article with an 'id' of 10
end
```

We may think that all the `.find` method does is construct a query like:

```
SELECT * FROM "articles".* FROM "articles" WHERE "articles"."id" = :id LIMIT 1
```

And it does, but before generating the query, the Ruby method `.to_i` is called on the `id`. As long as the string starts with something that can be parsed to an integer, it will return an integer.

This means that a string like `10-and-any-string-we_want` gets converted to just the integer `10`.

```ruby
"10".to_i # returns 10
"10-and-any-string-we_want".to_i # returns 10
"and-any-string-we-want-10".to_i # returns 0 which is not what we want.
```

With this information, let's see how we can have pretty URLs in Rails without using a gem:

## Friendly URLs in Rails without a gem

In this tutorial, we will explain **how to integrate the FriendlyId gem**, but we also want to show you how you can make your own version of the feature.

We will explore two ways of doing this: one that's quick and simple, and the other would be to try to implement functionality similar to the gem.

### Pretty URLs using the `to_param` method

We know that we can pass a string that starts with an `id` to our params hash and retrieve the record with that `id` using the same code for the `show` action shown above.

So, if your `Article` model has a `title` we could pass it manually to the route helpers whenever we link to an `Article` instance:

```ruby
# Returns "10-the-article-title"
<%= link_to @article.title, article_path("#{@article.id}-{@article.parameterized_title}) %>
```

This would work. However, it can quickly become an inconvenience.

That's why Rails calls the `.to_param` method on the object it receives on the single-resource route helpers to construct a URL for the object.

Out of the box this method, which returns the `id` as a string by default, looks like this:

```ruby
# ActiveRecord::Integration concern
def to_param
  return unless id
  Array(id).join(self.class.param_delimiter)
end
```

This means that we can overwrite the method in our `Article` class so Rails can generate the pretty URL for us:

```ruby
# app/models/article.rb
def to_param
  "#{id}-#{parameterized_title}"
end

def parameterized_title
	title.parameterize # Or the logic to parameterize the title to our liking
end
```

Anytime we call the `article_path` with an `Article` instance it would generate the URL where the slug would be a string starting with the object's `id` followed by the parameterized title.

This would give us a limited but functional pretty URL functionality.

However, there are a couple of things to consider with this approach:

- **There's more than one way to access the resource:** even though the method works, we can pass an integer directly to the `post_path` which will in turn return the same view as before but with the `posts/10` URL. Be consistent in the way you construct URLs using this method to avoid issues like duplicate content penalization because of the indexing of two identical resources with different URLs.
- **The sequential `id` is still exposed to the user:** this defeats the purpose of having pretty URLs to hide some details about our data to the users.

{{info}} Even though this method of security is called **security through obscurity** and is not nearly enough to protect our application. Hiding the `id` from the DOM or our API responses can be a good way to deter users that might not be too technical. {{/info}}

### Without exposing the ID in the URL

The second way to implement pretty URLs without a gem in Rails is to add a `slug` attribute to the model and then querying using `.find_by`.

Let's assume we have an `Article` model with a unique `slug` attribute that we manually define when creating a record.

The first step would be to replace the `to_param` method for our model:

```ruby
# app/models/article.rb
def to_param
  slug
end
```

So, for our first article called “Welcome to my blog” we can have the `slug` set to “welcome-to-my-blog”, we can change our controller `show` action to use `find_by!(slug: slug)` or `find_by_slug!(slug)`

```ruby
# app/controllers/articles_controller
def show
  @article = Article.find_by!(slug: params[:id])
end
```

But now, we can no longer pass the article's `id` as a parameter because the finder is expecting a slug. And there are some use cases where we would like to pass either of them.

Maybe admin users require that we explicitly show the record `id` in the URL because it might be useful for them in some cases.

So, to build a minimal version of **friendly_id,** we could define the requirements as:

1. We should be able to find records by `slug` or by `id` and our finder method should return an article in both cases.
2. The `slug` should be generated before the record is saved to the database using an attribute we choose using the `sluggable` class method.
3. An `ActiveRecord::RecordNotFound` error should be raised if the record isn't found.
4. We should be able to decide which attribute to use to generate the `slug` and we should also be able to customize the slug column in case we want to give it another name.

So, we might write a simple `FriendlyUrlable` concern to handle this logic for us:

```ruby
# app/models/concerns/friendly_urlable.rb
module FriendlyUrlable
  extend ActiveSupport::Concern

  included do
    before_save :generate_slug
    class_attribute :slug_column, default: :slug
    class_attribute :sluggable_attribute

    class << self
      def sluggable(attribute, use: :slug)
        self.sluggable_attribute = attribute
        self.slug_column = use
      end

      def friendly
        self
      end

      def find(id)
        if is_slug?(id)
          resource = find_by(slug_column => id)
          raise ActiveRecord::RecordNotFound if resource.nil?
          resource
        else
          super
        end
      end

      def is_slug?(id)
        id.respond_to?(:to_i) && id.to_i.to_s != id.to_s
      end
    end
  end

  def to_param
    self[self.class.slug_column].to_s
  end

  private

  def generate_slug
    source = send(self.class.sluggable_attribute)
    self[self.class.slug_column] = source.present? ? source.parameterize : nil
  end
end
```

And then, we can include our concern in the `Article` class:

```ruby
# app/models/article.rb
class Article < ApplicationRecord
  include FriendlyUrlable
  sluggable :title, use: :slug
end
```

This implementation, as simple as it currently is, fulfills the requirements we laid out above. We added some tests to make sure it works correctly:

```ruby
require "test_helper"

class FriendlyUrlable < ActiveSupport::TestCase
  test "should find an article by slug" do
    article = Article.new(title: "Hello World")
    article.save
    assert_equal article, Article.friendly.find("hello-world")
  end

  test "should return 404 for non-existent slug" do
    assert_raises(ActiveRecord::RecordNotFound) do
      Article.friendly.find("non-existent-slug")
    end
  end

  test "should find an article by id when it's a string" do
    article = Article.new(title: "Hello World")
    article.save
    assert_equal article, Article.find(article.id.to_s)
  end
  
  test "should generate slug from title" do
    article = Article.new(title: "Hello World")
    article.save
    assert_equal "hello-world", article.slug
  end

  test "should return an article by id without using the friendly method with an integer id" do
    article = Article.new(title: "Hello World")
    article.save
    assert_equal article, Article.find(article.id)
  end

  test "the to_param method should return the slug" do
    article = Article.new(title: "Hello World")
    article.save
    assert_equal article.slug, article.to_param
  end

  test "should generate the slug from the excerpt if configured to do so" do
    Article.sluggable :excerpt, use: :slug
    article = Article.new(title: "Hello World", excerpt: "This is a test")
    article.save
    assert_equal "this-is-a-test", article.slug
  ensure
    Article.sluggable_attribute = :excerpt
    Article.slug_column = :slug
  end

  test "should not generate a slug if the sluggable attribute is nil" do
    original_sluggable_attribute = Article.sluggable_attribute
    original_slug_column = Article.slug_column

    Article.sluggable :excerpt, use: :slug
    article = Article.new(title: "Hello World", excerpt: nil)
    article.save
    assert_nil article.slug
  ensure
    Article.sluggable_attribute = original_sluggable_attribute
    Article.slug_column = original_slug_column
  end
end
```

This is certainly a naive implementation of what the [`friendly_id`](https://github.com/norman/friendly_id) gem does, but it's an exercise to see how a simple implementation can be started and improved upon.

Once we saw how we can implement a simple version of pretty URLs using a concern, let's see why using a gem is probably a good idea for your application:
## Why use a gem for friendly URLs?
Being cautious and selective about the third-party code we bring into our application is a good practice. Every line of code that we didn't write is a source of potential issues and technical debt.

Libraries may not be maintained long-term, so adding them without consideration can lead to problems later.

However, there are situations when a simple feature can be solved in three lines of code, like overwriting the `to_param` method, but the real challenges emerge when we become aware of edge cases or features we didn't know we needed.

The `friendly_id` gem handles the following for us:

- **Tried and tested finders:** edge cases are handled for us, and a handful of finder methods are available for us.
- **Parameterization edge cases:** handles the transliteration of non-Latin characters to create slugs. This can be complex to implement if your application needs to handle this use case.
- **Proper redirections for slug changes:** handling redirections automatically when a slug changes is an important feature for [Rails SEO](https://avohq.io/rails-seo). Error statuses for previously indexed pages affect our crawl budget and can certainly affect traffic until proper re-indexation happens. So, unless you have a good reason not to, using the 'history' feature of the `friendly_id` gem is recommendable.
- **Slug candidates:** it provides a way to generate a slug from multiple attributes.
- **Scoping:** we can generate unique slugs within a scope. For example, you can have two restaurants with the same name in different cities, and you might want to keep the slug clean. 
- **Reserved words:** allows us to configure a list of words that cannot be used as slugs. This is critical to avoid conflicts with our application routes or with names that we deem inappropriate.

Most of these features aren't rare edge cases; they're actually pretty common. Even if you don't need them now, there's a good chance you will later.

Having said this, let's explore how we can add pretty URLs to a Rails application using this gem: 

{{further_reading slug="meta-tags-rails" /}}
## Integrating the friendly_id gem into a Rails application
Adding the `friendly_id` gem to a Rails application is as simple as adding a model with a `slug` attribute and a couple of lines to the model file.

However, for this example, we will integrate the gem to fulfill a couple of requirements that are pretty common. We should:

- Have an `Article` model with title, excerpt, content, and slug attributes. It will also have an author of the class `User`.
- Allow for the `slug` to be automatically generated from the `Article` title or manually generated if needed.
- Have the slug uniquely scoped to an author. This means that two authors can have an article with the same name.
- Implement the history feature to handle redirections when slugs change.

We won't be showing the `User` implementation because it's not really pertinent to this tutorial, but we can assume we have a `User` model already.

First, let's get started by creating an `Article` model with the required fields:

```shell
bin/rails generate model Article title:string excerpt:string content:text slug:uniq
```

Next, we will create a migration to add a reference to the `User` using an `author_id` field to make it more semantic:

```shell
bin/rails generate migration add_author_to_articles
```

With the corresponding migration, which will add the `author_id` foreign key referencing the `users` table:

```ruby
# db/migrate/#{timestamp}_add_author_to_articles
class AddAuthorToArticles < ActiveRecord::Migration[7.2]
  def change
    add_reference :articles, :author, foreign_key: { to_table: :users }
  end
end
```

After this migration, we add the scoped index to the `slug` and `author_id` attributes:

```ruby
class UpdateArticlesSlugIndex < ActiveRecord::Migration[7.2]
  def change
    # Remove the existing unique index on slug
    remove_index :articles, :slug

    # Add a new composite index on slug and author_id
    add_index :articles, [:slug, :author_id], unique: true
  end
end
```

After we run this migration, we should set our associations. First, for the `User`:

```ruby
# app/models/user.rb
has_many :articles, class_name: "Article", foreign_key: :author_id
```

Next for the `Article`:

```ruby
# app/models/article.rb
belongs_to :author, class_name: "User"
```

After we set our associations, we can add the `friendly_id` gem to our Gemfile and install it using Bundler:

```ruby
# Add the gem to the production group
gem 'friendly_id', '~> 5.5.0'
```

```shell
bundle install
```

Now we have the `friendly_id` gem installed. However, we need to “install” the gem by generating its configuration file and the migration needed for the history feature:

```shell
bin/rails generate friendly_id
```

This command will generate a `config/initializers/friendly_id.rb` configuration file that allows us to personalize the gem's configuration in case we need to.

It will also add a migration that will create the `friendly_id_slugs` table that allows us to use the `history` feature of the gem.

When we proceed with the migration, the following table, which will be used to keep track of `slug` history and the corresponding redirections, will be added:

![friendly_id_slugs table visualization](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzM5LCJwdXIiOiJibG9iX2lkIn19--39c6b53f06a9b44cb9e9365cc39b630e3306bfcc/friendly_id_slugs_migration.webp)

Now, we need to add the configuration to the `Article` model to make it all work:

```ruby
# app/models/article.rb
class Article < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: [:slugged, :history, :scoped], scope: :author

  belongs_to :author, class_name: "User", foreign_key: :author_id

  private

  def should_generate_new_friendly_id?
    title_changed?
  end
end
```

- The `:title` argument along with the `slugged` mode means that the slug will be constructed from the `title` attribute. And the `should_generate_new_friendly_id?` decides when and how the slug is constructed.
- The `history` mode means that whenever the slug changes, a new entry to the `friendly_id_slugs` table will be generated with the corresponding redirection. This is very desirable from an SEO perspective: we don't need to worry about reindexing missing or changed slugs. It all happens automatically.
- The `scoped` mode means that the `title` uniqueness would be examined along the `author` scope. Meaning, an article `title` can be repeated within the `articles` table as long as it's not repeated for the same author.

After all this, we should be able to create a couple of users and create articles with matching titles and not have an issue because of the scopes set by the gem.

## Conclusions
Pretty URLs are important for Rails applications because they make our application more user and search engine friendly.

Plus, we can also protect against undesired data leaks by avoiding sequential resource creation exposure.

Adding them to a Rails application is as simple as overwriting the `to_param` method that every `ActiveRecord` instance defines.

However, a gem like `friendly_id` comes with many added features that are useful to build applications.

Even though this is definitely not the first `friendly_id` tutorial using Ruby on Rails, I hope you got something out of it.

Happy coding, and remember: Matz is nice, so our URLs are nice too!
