module FriendlyId::History

History: Avoiding 404's When Slugs Change

FriendlyId's {FriendlyId::History History} module adds the ability to store a log of a model's slugs, so that when its friendly id changes, it's still possible to perform finds by the old id.

The primary use case for this is avoiding broken URLs.

Setup

In order to use this module, you must add a table to your database schema to store the slug records. FriendlyId provides a generator for this purpose:

rails generate friendly_id
rake db:migrate

This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug} model.

Considerations

Because recording slug history requires creating additional database records, this module has an impact on the performance of the associated model's `create` method.

Example

  class Post < ActiveRecord::Base
    extend FriendlyId
    friendly_id :title, :use => :history
  end

  class PostsController < ApplicationController

    before_filter :find_post

    ...

    def find_post
      @post = Post.friendly.find params[:id]

      # If an old id or a numeric id was used to find the record, then
      # the request slug will not match the current slug, and we should do
      # a 301 redirect to the new path
      if params[:id] != @post.slug
        return redirect_to @post, :status => :moved_permanently
      end
    end
  end

Public Class Methods

included(model_class) click to toggle source

Configures the model instance to use the History add-on.

# File lib/friendly_id/history.rb, line 69
def self.included(model_class)
  model_class.class_eval do
    has_many :slugs, -> { order(id: :desc) }, **{
      as: :sluggable,
      dependent: @friendly_id_config.dependent_value,
      class_name: Slug.to_s
    }

    after_save :create_slug
  end
end
setup(model_class) click to toggle source
# File lib/friendly_id/history.rb, line 59
def self.setup(model_class)
  model_class.instance_eval do
    friendly_id_config.use :slugged
    friendly_id_config.class.send :include, History::Configuration
    friendly_id_config.finder_methods = FriendlyId::History::FinderMethods
    FriendlyId::Finders.setup(model_class) if friendly_id_config.uses? :finders
  end
end

Private Instance Methods

create_slug() click to toggle source
# File lib/friendly_id/history.rb, line 119
def create_slug
  return unless friendly_id
  return if history_is_up_to_date?
  # Allow reversion back to a previously used slug
  relation = slugs.where(slug: friendly_id)
  if friendly_id_config.uses?(:scoped)
    relation = relation.where(scope: serialized_scope)
  end
  relation.destroy_all unless relation.empty?
  slugs.create! do |record|
    record.slug = friendly_id
    record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
  end
end
history_is_up_to_date?() click to toggle source
# File lib/friendly_id/history.rb, line 134
def history_is_up_to_date?
  latest_history = slugs.first
  check = latest_history.try(:slug) == friendly_id
  if friendly_id_config.uses?(:scoped)
    check &&= latest_history.scope == serialized_scope
  end
  check
end
scope_for_slug_generator() click to toggle source

If we're updating, don't consider historic slugs for the same record to be conflicts. This will allow a record to revert to a previously used slug.

Calls superclass method
# File lib/friendly_id/history.rb, line 108
def scope_for_slug_generator
  relation = super.joins(:slugs)
  unless new_record?
    relation = relation.merge(Slug.where("sluggable_id <> ?", id))
  end
  if friendly_id_config.uses?(:scoped)
    relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
  end
  relation
end