feat: Implement SEO-friendly slug generation and improve geocoding UX
- Add Rails parameterize for server-side slug generation (name-venue-city format) - Configure client-side slug library with RFC3986 mode for consistency - Remove slug field from edit forms to prevent URL changes after publication - Enable image_processing gem for Active Storage variants - Make geocoding notifications visible indefinitely on promoter event forms - Add server-side slug generation fallback with uniqueness validation - Update promoter controller to allow slug only for new events 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ class Event < ApplicationRecord
|
||||
|
||||
|
||||
# === Callbacks ===
|
||||
before_validation :generate_slug, if: :should_generate_slug?
|
||||
before_validation :geocode_address, if: :should_geocode_address?
|
||||
before_update :handle_image_replacement, if: :image_attached?
|
||||
|
||||
@@ -62,6 +63,71 @@ class Event < ApplicationRecord
|
||||
|
||||
# === Instance Methods ===
|
||||
|
||||
# Generate SEO-friendly slug from name, venue name, and city
|
||||
def generate_slug
|
||||
return if name.blank? && venue_name.blank? && venue_address.blank?
|
||||
|
||||
# Extract city from venue address
|
||||
city = extract_city_from_address(venue_address)
|
||||
|
||||
# Build slug parts
|
||||
slug_parts = []
|
||||
slug_parts << name if name.present?
|
||||
slug_parts << venue_name if venue_name.present?
|
||||
slug_parts << city if city.present?
|
||||
|
||||
# Generate slug using Rails' parameterize
|
||||
slug_value = slug_parts.join('-').parameterize
|
||||
|
||||
# Ensure minimum length
|
||||
if slug_value.length < 3
|
||||
slug_value = "event-#{Time.current.to_i}".parameterize
|
||||
end
|
||||
|
||||
# Make sure slug is unique
|
||||
base_slug = slug_value
|
||||
counter = 1
|
||||
while Event.where.not(id: id).where(slug: slug_value).exists?
|
||||
slug_value = "#{base_slug}-#{counter}".parameterize
|
||||
counter += 1
|
||||
end
|
||||
|
||||
self.slug = slug_value
|
||||
end
|
||||
|
||||
# Check if slug should be generated
|
||||
def should_generate_slug?
|
||||
# Generate slug if it's blank or if it's a new record
|
||||
slug.blank? || new_record?
|
||||
end
|
||||
|
||||
# Extract city from address
|
||||
def extract_city_from_address(address)
|
||||
return "" if address.blank?
|
||||
|
||||
# Look for French postal code pattern (5 digits) + city
|
||||
match = address.match(/(\d{5})\s+([^,]+)/)
|
||||
if match
|
||||
return match[2].strip
|
||||
end
|
||||
|
||||
# Fallback: extract last part after comma (assume it's city)
|
||||
parts = address.split(',')
|
||||
if parts.length > 1
|
||||
return parts[parts.length - 1].strip
|
||||
end
|
||||
|
||||
# Another fallback: look for common French city indicators
|
||||
city_indicators = ["Paris", "Lyon", "Marseille", "Toulouse", "Nice", "Nantes", "Strasbourg", "Montpellier", "Bordeaux", "Lille"]
|
||||
for city in city_indicators
|
||||
if address.downcase.include?(city.downcase)
|
||||
return city
|
||||
end
|
||||
end
|
||||
|
||||
""
|
||||
end
|
||||
|
||||
# Get image URL prioritizing old image field if it exists
|
||||
def display_image_url
|
||||
# First check if old image field exists and has a value
|
||||
|
||||
Reference in New Issue
Block a user