ruby 97 lines · 3 tabs

Generate Unique URL Slugs in Rails with before_validation and a friendly Controller Lookup

Shared by codesnips Aug 2026
3 tabs
class AddSlugToArticles < ActiveRecord::Migration[7.1]
  def change
    add_column :articles, :slug, :string, null: false, default: ""
    add_index :articles, :slug, unique: true

    # Backfill existing rows before the unique index is relied upon in code.
    reversible do |dir|
      dir.up do
        Article.reset_column_information
        Article.find_each do |article|
          article.update_column(:slug, article.send(:slugify, article.title))
        end
      end
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows the common Rails pattern for giving records a stable, human-readable URL slug that is guaranteed unique, without pulling in a gem. It spans three collaborating files: the migration that shapes the column, the model that fills it in, and the controller that reads it back.

In db/migrate slug migration, the slug is treated as first-class data rather than a derived string. The column is null: false and backed by a unique index, so the database itself is the final arbiter of uniqueness. Enforcing this at the storage layer matters because application-level checks alone are racy: two concurrent inserts can both pass an ActiveRecord uniqueness validation and then collide. The unique: true index turns that race into a catchable RecordNotUnique rather than duplicate rows.

Article model builds the slug in a before_validation callback, assign_slug, that only fires on: :create or when the title changes, so editing unrelated fields never churns the URL. The slugify helper parameterizes the title into a lowercase, hyphenated base. ensure_unique_slug then probes the table with where.not(id: id) — excluding the current record so re-saving is a no-op — and appends -2, -3, and so on until it finds a free value. Doing this in before_validation means the presence and uniqueness validators see the final slug, giving clean error messages instead of a raw database exception in the normal case.

Because a generated suffix can still lose a race under real concurrency, the create_with_slug class method wraps the save and retries on ActiveRecord::RecordNotUnique, regenerating the slug on the next attempt. This combines the friendly UX of pre-computed slugs with the hard guarantee of the unique index — the belt-and-suspenders approach worth reaching for whenever a URL segment must be both readable and collision-free.

ArticlesController closes the loop. Its routes use :slug as the identifier, so set_article calls find_by!(slug: params[:slug]), which raises RecordNotFound and yields a proper 404 for unknown slugs. Overriding to_param in the model keeps article_path(@article) emitting the slug automatically, so links and lookups stay in sync without manual URL building anywhere in the app.


Related snips

Share this code

Here's the card — post it anywhere.

Generate Unique URL Slugs in Rails with before_validation and a friendly Controller Lookup — share card
Link copied