Module: Slugifiable::Model

Extended by:
ActiveSupport::Concern
Defined in:
lib/slugifiable/model.rb

Constant Summary collapse

DEFAULT_SLUG_GENERATION_STRATEGY =

This concern makes objects have a string slug based on their ID or another specified attribute

To use, include this in any ActiveRecord model: “‘ include Slugifiable::Model “`

By default all slugs will be a string computed from the record ID: “‘ generate_slug_based_on :id “`

but optionally, you can also specify to compute the slug as a number: “‘ generate_slug_based_on id: :number “`

or compute the slug based off any other attribute: “‘ generate_slug_based_on :name “`

:compute_slug_as_string
DEFAULT_SLUG_STRING_LENGTH =
11
DEFAULT_SLUG_NUMBER_LENGTH =
6
MAX_SLUG_GENERATION_ATTEMPTS =

Maximum number of attempts to generate a unique slug before falling back to timestamp-based suffix

10

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(missing_method, *args, &block) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/slugifiable/model.rb', line 53

def method_missing(missing_method, *args, &block)
  if missing_method.to_s == "slug" && !self.methods.include?(:slug)
    compute_slug
  else
    super
  end
end

Instance Method Details

#compute_slugObject



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/slugifiable/model.rb', line 61

def compute_slug
  strategy, options = determine_slug_generation_method

  length = options[:length] if options.is_a?(Hash)

  if strategy == :compute_slug_based_on_attribute
    self.send(strategy, options)
  else
    self.send(strategy, length)
  end
end

#compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH) ⇒ Object



78
79
80
81
# File 'lib/slugifiable/model.rb', line 78

def compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH)
  length ||= DEFAULT_SLUG_NUMBER_LENGTH
  generate_random_number_based_on_id_hex(length)
end

#compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH) ⇒ Object



73
74
75
76
# File 'lib/slugifiable/model.rb', line 73

def compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH)
  length ||= DEFAULT_SLUG_STRING_LENGTH
  (Digest::SHA2.hexdigest self.id.to_s).first(length)
end

#compute_slug_based_on_attribute(attribute_name) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/slugifiable/model.rb', line 83

def compute_slug_based_on_attribute(attribute_name)
  # This method generates a slug from either:
  # 1. A database column (e.g. generate_slug_based_on :title)
  # 2. An instance method (e.g. generate_slug_based_on :title_with_location)
  #
  # Priority:
  # - Database columns take precedence over methods with the same name
  # - Falls back to methods if no matching column exists
  # - Falls back to ID-based slug if neither exists
  #
  # Flow:
  # 1. Check if source exists (DB column first, then method)
  # 2. Get raw value
  # 3. Parameterize (convert "My Title" -> "my-title")
  # 4. Ensure uniqueness
  # 5. Fallback to random number if anything fails

  # First check if we can get a value from the database
  has_attribute = self.attributes.include?(attribute_name.to_s)

  # Only check for methods if no DB attribute exists
  # We check all method types to be thorough
  responds_to_method = !has_attribute && (
    self.class.method_defined?(attribute_name) ||
    self.class.private_method_defined?(attribute_name) ||
    self.class.protected_method_defined?(attribute_name)
  )

  # If we can't get a value from either source, fallback to using the record's ID
  return compute_slug_as_string unless has_attribute || responds_to_method

  # Get and clean the raw value (e.g. "  My Title  " -> "My Title")
  # Works for both DB attributes and methods thanks to Ruby's send
  raw_value = self.send(attribute_name)
  return generate_random_number_based_on_id_hex if raw_value.nil?

  # Convert to URL-friendly format
  # e.g. "My Title" -> "my-title"
  base_slug = raw_value.to_s.strip.parameterize
  return generate_random_number_based_on_id_hex if base_slug.blank?

  # Handle duplicate slugs by adding a random suffix if needed
  # e.g. "my-title" -> "my-title-123456"
  unique_slug = generate_unique_slug(base_slug)
  unique_slug.presence || generate_random_number_based_on_id_hex
end