Die perfekte Slug - URL

Permalinks müssen eindeutig eine Ressource identifizieren. Allerdings sollten sie auch “sprechend” sein. Das ist wichtig für SEO. In Ruby on Rails ist der Teil einer URL, der eine Ressource identifiziert, oft lediglich die ID des Datenbankobjektes. Für SEO relevante Reesourcen macht es daher Sinn, Slugs zu bilden.
Eine naheliegende Lösung ist sicherlich, das Gem FriendlyID zu einzubinden. Es ist sehr mächtig. Etwas zu mächtig für viele Anwendungsfälle. Zumal eine eigene Implementierung sehr einfach ist.
Um die Eindeutigkeit eines Slugs zu garantieren, sollte es die Datenbank-ID des Objektes enthalten.
Das folgende Beispiel basiert auf diesem Muster.

Das Model

Ein klassisches Beispiel für Slugs sind Blog Posts. Artikel haben einen Titel und den Text:

rails g model Article title:string content:text

In Ruby on Rails generieren die Routes-Helper aus Objekten eine URL. Dabei greifen sie auf die Methode #to_param zu. Die Implementierung für ActiveRecord::Base#to_param gibt die ID des Objektes zurück.
Um den Slug zu generieren muss diese Methode überschrieben werden:

# app/models/article.rb
class Article < ApplicationRecord
  def to_param
    "#{id}-#{title.parameterize}"
  end
end

Für den sprechenden Teil wird hier der Titel verwendet. String#parameterize ersetzt alle Leerzeichen durch einen Bindestrich -.

Die Routen

Die Routen:

# config/routes.rb
Rails.application.routes.draw do
  resources :articles
end

Das Ergebnis kann auch in der REPL (Rails Konsole) überprüft werden:

# Rails console
app.article_path Article.last
# => "/article/79-custom-validator-testen"

Der Controller

Ein Vorteil bei diesem Ansatz ist, das der Controller genauso aussieht, als wenn lediglich die ID als Parameter übergeben werden würde:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find params[:id]
  end
end

Das funktioniert, weil Ruby String#to_i nur alle numerischen Zeichen (vom Stringbeginn bis zum ersten nicht-numerischen Zeichen) castet:

"79-custom-validator-testen".to_i
# => 79

Es ist also noch nicht einmal zwingend notwendig den Slug zu persistieren.
Wenn es dennoch Gründe für eine Persistierung des Slugs gibt:

rails g migration add_slug_to_articles slug:string

sollte in jedem Fall nur der sprechende Teil (d.h. ohne die ID) des Slugs gespeichert werden:

# app/models/article.rb
class Article < ApplicationRecord
  before_create :set_slug

  def to_param
    "#{id}-#{slug}"
  end

  private

  def set_slug
    self.slug = title.parameterize
  end
end