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
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
# 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
# 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
# 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
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.
# 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