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:
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? Send me your a comment ! I'd love to help you implement this in your Rails app.
Happy coding! 🚀
Ahmed Nadar