I want to share a practical solution to a common challenge in Rails development I face it with almost every application I work with: combining UUID-based models with user-friendly URLs. If you've ever wondered how to maintain the security benefits of UUIDs while having clean, SEO-friendly URLs, you're in the right place!
The Challenge
Imagine you have a Rails app using UUIDs for your models (great choice for security and scalability!), but your URLs look like this:
https://yourapp.com/products/123e4567-e89b-12d3-a456-426614174000
Wouldn't it be nicer to have URLs like this?
https://yourapp.com/products/awesome-product-name
Let's make it happen! I'll walk you through the process step by step.
Prerequisites
Before we dive in, make sure you have:
A Rails application (I'm using Rails 7, but this works with Rails 5+ too)
PostgreSQL database (recommended for UUID support)
Models using UUID as primary keys
**
Step 1: Setting up your environment
**
First, let's add the friendly_id gem. It's a mature, well-maintained solution that makes our lives much easier, thanks Norman!
- Add this to your Gemfile:
gem "friendly_id", "~> 5.5.0" # Latest stable version as of 2024
- Run these commands in your terminal:
bundle install
rails generate friendly_id # Creates the friendly_id migration
rails db:migrate # Sets up the friendly_id_slugs table
- Now, let's add a slug column to your model. For example, if you have a Product model:
rails generate migration AddSlugToProducts slug:string:uniq
rails db:migrate
**
Step 2: Configuring our model
**
Here's where the magic happens. Lets see how to set up your model with different slug strategies:
class Product < ApplicationRecord
# Step 1: Enable friendly_id
extend FriendlyId
# Step 2: Configure slug generation
friendly_id :slug_candidates, use: [:slugged, :finders]
private
# Step 3: Define your slug candidates
def slug_candidates
[
:name, # First try: just the name
[:name, :category], # If that's taken: name-category
[:name, :category, :created_at] # Last resort: name-category-timestamp
]
end
# Step 4: Control when slugs should be regenerated
def should_generate_new_friendly_id?
name_changed? || category_changed? || super
end
end
Let's break down what's happening here:
extend FriendlyId adds the friendly_id functionality to your model
:slugged enables slug generation
:finders allows you to use find(params[:id]) with both slugs and UUIDs
slug_candidates provides fallback options if your first choice is taken
**
Step 3: The magic rake task**
For a quick slug generation for existing records I used use rails console. Quick but, it is not the Rails way.
Here's a super helpful rake task I've created to manage your slugs. Create lib/tasks/friendly_id.rake:
namespace :friendly_id do
desc 'Generate slugs for all your models'
task generate_slugs: :environment do
# Let's be informative about what we're doing
puts "π Starting slug generation..."
# Replace Product with your model name
Product.find_each do |record|
print "Processing #{record.name}... "
# Clear existing slug to force regeneration
record.slug = nil
if record.save(validate: false)
puts "β
Created slug: #{record.slug}"
else
puts "β Failed"
end
end
puts "\n⨠All done! Your URLs are now user-friendly!"
end
desc 'Check for any records missing slugs'
task check_slugs: :environment do
puts "π Checking for records without slugs..."
records_without_slugs = Product.where(slug: nil)
if records_without_slugs.any?
puts "Found #{records_without_slugs.count} records needing slugs:"
records_without_slugs.each do |record|
puts "- #{record.name} (ID: #{record.id})"
end
else
puts "π All records have slugs! You're good to go!"
end
end
end
Run these tasks in your terminal:
rake friendly_id:generate_slugs
rake friendly_id:check_slugs
Step 4: Implementation in your controllers
Update your controller to use friendly_id:
class ProductsController < ApplicationController
def show
# This will work with both slugs and UUIDs!
@product = Product.friendly.find(params[:id])
end
end
π Keep in mind
UUID compatibility
Your UUIDs are still there, working behind the scenes
Database relations still use UUIDs
URLs just look nicer now!
URL generation
# In your views, nothing changes! <%= link_to product.name, product_path(product) %>
Handling changes
Slugs automatically update when the source fields change
Old slugs can be preserved using the :history module
You can customize when slugs regenerate
Troubleshooting
Still seeing UUIDs in your URLs? Try these steps:Clear your browser cache
Restart your Rails server
Run rails friendly_id:generate_slugs
Check your controller uses friendly.find
Common gotchas I stumbled upon and solutions
Duplicate slugs
# Add a sequence for duplicates friendly_id :name, use: [:slugged, :sequence]
Special characters
friendly_id handles most special characters well
You can customize with your own normalizer
Performance
Slug lookups are indexed
UUID benefits remain for relationships
Best of both worlds! π
**
Wrapping up**
You now have user-friendly URLs without sacrificing the benefits of UUIDs! Your URLs are:
SEO-friendly β
Human-readable β
Secure (UUIDs still used internally) β
Easy to maintain β
Have questions or run into issues? Drop a comment below! I'd love to help you implement this in your Rails app.
Happy coding! π π»