Ahmed Nadar

November 30, 2024

Implementing Friendly URLs with UUID-Based Models

frindlyID.jpeg

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!

  1. Add this to your Gemfile:
gem "friendly_id", "~> 5.5.0"  # Latest stable version as of 2024

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

  1. 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:

  1. extend FriendlyId adds the friendly_id functionality to your model
  2. :slugged enables slug generation
  3. :finders allows you to use find(params[:id]) with both slugs and UUIDs
  4. 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

  1. UUID compatibility
    • Your UUIDs are still there, working behind the scenes
    • Database relations still use UUIDs
    • URLs just look nicer now!
  2. URL generation
  3. # In your views, nothing changes!
    <%= link_to product.name, product_path(product) %>
  4. 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
  5. Troubleshooting
    Still seeing UUIDs in your URLs? Try these steps:
    1. Clear your browser cache
    2. Restart your Rails server
    3. Run rails friendly_id:generate_slugs
    4. Check your controller uses friendly.find

Common gotchas I stumbled upon and solutions 

  1. Duplicate slugs
  2. # Add a sequence for duplicates
    friendly_id :name, use: [:slugged, :sequence]
  3. Special characters
    • friendly_id handles most special characters well
    • You can customize with your own normalizer
  4. 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

About Ahmed Nadar

Ruby on Rails enthusiast at heart. I run RapidRails agency focus on Rails development & UI stack. Maker of RapidRails UI component for Ruby on Rails.
Find me on Twitter – @ahmednadar