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 path will not match the post_path, and we should do
    # a 301 redirect that uses the current friendly id.
    if request.path != post_path(@post)
      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 73
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 63
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 122
def create_slug
  return unless friendly_id
  return if slugs.first.try(:slug) == friendly_id
  # 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.delete_all
  slugs.create! do |record|
    record.slug = friendly_id
    record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
  end
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 112
def scope_for_slug_generator
  relation = super
  return relation if new_record?
  relation = relation.joins(:slugs).merge(Slug.where('sluggable_id <> ?', id))
  if friendly_id_config.uses?(:scoped)
    relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
  end
  relation
end