Module: FriendlyId::History

Defined in:
lib/friendly_id/history.rb

Overview

History: Avoiding 404's When Slugs Change

FriendlyId's 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_globalize
rake db:migrate

This will add a table named friendly_id_slugs, used by the 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.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

Defined Under Namespace

Modules: FinderMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(model_class) ⇒ Object

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



71
72
73
74
75
76
77
78
79
80
# File 'lib/friendly_id/history.rb', line 71

def self.included(model_class)
  model_class.class_eval do
    has_many :slugs, -> {order(Slug.arel_table[:id].desc)}, 
      as:         :sluggable,
      dependent:  :destroy,
      class_name: Slug.to_s
    
    after_save :create_slug
  end
end

.setup(model_class) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/friendly_id/history.rb', line 57

def self.setup(model_class)
  model_class.instance_eval do
    friendly_id_config.use :slugged
    friendly_id_config.finder_methods = FriendlyId::History::FinderMethods
    if friendly_id_config.uses? :finders
      relation.class.send(:include, friendly_id_config.finder_methods)
      if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 2
        model_class.send(:extend, friendly_id_config.finder_methods)
      end
    end
  end
end

Instance Method Details

#create_slugObject (private)



120
121
122
123
124
# File 'lib/friendly_id/history.rb', line 120

def create_slug
  translations.map(&:locale).each do |locale|
    ::Globalize.with_locale(locale) { super_create_slug(locale) }
  end
end

#scope_for_slug_generatorObject (private)

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.



110
111
112
113
114
115
116
117
118
# File 'lib/friendly_id/history.rb', line 110

def scope_for_slug_generator
  relation = super
  return relation if new_record?
  relation = relation.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

#super_create_slug(locale) ⇒ Object (private)



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/friendly_id/history.rb', line 126

def super_create_slug(locale)
  return unless friendly_id
  return if slugs.where(locale: locale).first.try(:slug) == friendly_id
  # Allow reversion back to a previously used slug
  relation = slugs.where(slug: friendly_id, locale: locale)
  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.locale = locale
    record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
  end
end