Schöne URLs mit Friendly_Id

Schöne URLs mit Friendly_Id

Wenn Sie genug von Modell-IDs in der URL haben, kann die Überschreibung von to_param nur begrenzt helfen. Das FriendlyId-Plugin kann Ihnen helfen, indem es das Generieren eines URL-Slugs vereinfacht und eine Verlaufshistorie beibehält.

Vorbereitung

Eine einfache Rails-Blogging-Anwendung erstellen

In diesem Tutorial verbessern wir die URLs einer einfachen Rails-Blogging-Anwendung. Standardmäßig verwendet Rails die interne ID eines Datensatzes in der URL. Ein Artikel kann dadurch beispielsweise über folgende Adresse erreichbar sein:
http://localhost:8080/articles/1
Technisch funktioniert diese URL problemlos. Sie verrät jedoch nichts darüber, welcher Artikel sich dahinter verbirgt.

Unser Ziel ist es, daraus später eine aussagekräftigere URL zu machen, beispielsweise:
http://localhost:8080/articles/superman
Dafür sehen wir uns zunächst die Rails-eigene Methode to_param an und verwenden anschließend das Gem FriendlyId, um richtige Slugs für unsere Artikel zu erzeugen.

Zunächst erstellen wir eine neue Rails-Anwendung:
$ rails new friendlyid_app
$ cd friendlyid_app

Anschließend erzeugen wir mit dem Scaffold-Generator unser Article-Model zusammen mit Controller, Views und den entsprechenden RESTful-Routen:
$ rails g scaffold Article name content:text published_at:datetime

Unser Artikel besitzt damit die drei Attribute name, content und published_at.

Nun führen wir die erzeugte Datenbankmigration aus:
$ rails db:migrate

Damit steht das Grundgerüst unserer Blogging-Anwendung zur Verfügung. Wir können Artikel erstellen, bearbeiten, anzeigen und löschen.

Auf der Übersichtsseite werden die vorhandenen Artikel angezeigt. Öffnen wir einen einzelnen Artikel, verwendet Rails zunächst dessen interne Datenbank-ID als URL-Parameter.

Beispielsweise:
http://localhost:8080/articles/1

Die 1 identifiziert den Datensatz eindeutig, beschreibt den Inhalt des Artikels aber nicht. Eine URL, die zusätzlich den Namen des Artikels enthält, wäre für Menschen wesentlich aussagekräftiger.

Im nächsten Schritt sehen wir uns deshalb zunächst an, wie wir dieses Verhalten mit Rails selbst und der Methode to_param beeinflussen können.


1.webp 202 KB

Aussagekräftigere URLs

Die Rails-Methode to_param überschreiben

Standardmäßig verwendet Rails die ID eines Datensatzes als Parameter in der URL. Unser erster Artikel ist deshalb beispielsweise unter folgender Adresse erreichbar:
http://localhost:8080/articles/1

Die ID ist für Rails praktisch, für den Besucher sagt sie jedoch nichts über den Inhalt des Artikels aus. Eine URL wie:
http://localhost:8080/articles/1-superman

ist wesentlich aussagekräftiger.

Rails verwendet intern die Methode to_param, wenn ein Active-Record-Objekt in einen URL-Parameter umgewandelt wird. Dieses Verhalten können wir direkt in unserem Model überschreiben.

Dazu öffnen wir das Article-Model und ergänzen die Methode to_param:
# app/models/article.rb

class Article < ApplicationRecord
  def to_param
    "#{id} #{name}".parameterize
  end
end

Wir erzeugen zunächst einen String aus der ID und dem Namen des Artikels:
"#{id} #{name}"

Bei einem Artikel mit der ID 1 und dem Namen Superman entsteht zunächst:
1 Superman

Anschließend rufen wir darauf parameterize auf:
"#{id} #{name}".parameterize

Rails wandelt den String dadurch in eine Form um, die sich besser für eine URL eignet. Leerzeichen werden beispielsweise durch Bindestriche ersetzt und Großbuchstaben entsprechend normalisiert.

Unser Artikel ist anschließend über folgende URL erreichbar:
http://localhost:8080/articles/1-superman

Die ID steht dabei bewusst weiterhin am Anfang des Parameters.

Der von einem normalen Scaffold erzeugte Controller sucht einen Artikel üblicherweise mit:
Article.find(params[:id])

Active Record kann einen Parameter wie "1-superman" weiterhin anhand der führenden numerischen ID dem Datensatz 1 zuordnen.

Damit erhalten wir bereits mit Rails selbst besser lesbare URLs, ohne die grundlegende Suche nach der Datenbank-ID verändern zu müssen.

Möchten wir allerdings URLs vollständig ohne interne ID verwenden, beispielsweise:
http://localhost:8080/articles/superman

reicht diese einfache to_param-Lösung nicht mehr aus.

Genau dafür verwenden wir im nächsten Schritt FriendlyId.

Einführung von FriendlyId

FriendlyId installieren und im Model einrichten

Mit unserer überschriebenen to_param-Methode haben wir bereits eine besser lesbare URL erhalten:
http://localhost:8080/articles/1-superman

Die interne Datenbank-ID ist allerdings weiterhin Bestandteil der URL. Möchten wir stattdessen einen Artikel ausschließlich über einen sprechenden Namen beziehungsweise einen Slug aufrufen, können wir das Gem friendly_idverwenden.

Zunächst ergänzen wir FriendlyId in unserer Gemfile:
# Gemfile

...
gem "friendly_id"

Anschließend installieren wir das neue Gem:
$ bundle install

Da FriendlyId nun die Erzeugung unserer URL-Parameter übernehmen soll, entfernen wir die zuvor angelegte to_param-Methode wieder aus dem Article-Model.

Stattdessen erweitern wir das Model zunächst um FriendlyId:
# app/models/article.rb

class Article < ApplicationRecord
  extend FriendlyId

  friendly_id :name
end

Mit
extend FriendlyId

stellen wir dem Model die Funktionen von FriendlyId zur Verfügung.

Anschließend legen wir fest, welches Attribut für die URL verwendet werden soll:
friendly_id :name

In unserem Fall verwenden wir dafür den Namen des Artikels.

Ein Artikel mit dem Namen Superman kann damit anstelle seiner numerischen ID über seinen Namen angesprochen werden.

Eine entsprechende URL sieht beispielsweise so aus:
http://localhost:8080/articles/Superman

Damit haben wir die interne ID zunächst aus der URL entfernt.

Diese einfache Variante hat allerdings noch einen Nachteil. Enthält der Artikelname Leerzeichen oder Sonderzeichen, erhalten wir noch keinen sauber erzeugten Slug.

Bei einem Artikel mit dem Namen:
Batman & Robin

kann der Name in der URL entsprechend codiert werden, anstatt eine typische Slug-Form wie batman-robin zu erhalten.

Für dauerhaft saubere und URL-freundliche Werte verwenden wir deshalb im nächsten Schritt die Slug-Funktion von FriendlyId.

Verwendung von Slugs

URL-freundliche Slugs für unsere Artikel erzeugen

Bisher verwendet FriendlyId direkt den Wert aus name für unsere URL. Bei einfachen Namen wie Superman funktioniert das bereits, bei Namen mit Leerzeichen und Sonderzeichen möchten wir jedoch einen richtigen URL-freundlichen Slug erzeugen.

Dafür aktivieren wir in unserem Article-Model das FriendlyId-Modul slugged:
# app/models/article.rb

class Article < ApplicationRecord
  extend FriendlyId

  friendly_id :name, use: :slugged
end

Mit:
use: :slugged

weisen wir FriendlyId an, aus dem Namen einen URL-freundlichen Slug zu erzeugen.

Aus einem Artikelnamen wie:
Batman & Robin

wird dadurch beispielsweise:
batman-robin

Damit FriendlyId diesen Wert dauerhaft speichern kann, benötigt unsere articles-Tabelle eine zusätzliche Spalte namens slug.

Wir erzeugen dafür eine Migration:
$ rails g migration AddSlugToArticles slug:string:uniq

Zusätzlich führen wir den FriendlyId-Generator aus:
$ rails g friendly_id

Der FriendlyId-Generator legt die grundlegende FriendlyId-Konfiguration an. Je nach verwendeter FriendlyId-Version können dabei auch weitere Dateien beziehungsweise Migrationen für zusätzliche Funktionen von FriendlyId erzeugt werden.

Unsere Migration für die slug-Spalte sieht anschließend folgendermaßen aus:
# db/migrate/XXXXXXXXXXXXXX_add_slug_to_articles.rb

class AddSlugToArticles < ActiveRecord::Migration[8.0]
  def change
    add_column :articles, :slug, :string
    add_index :articles, :slug, unique: true
  end
end

Der Zeitstempel am Anfang des Dateinamens wird von Rails automatisch erzeugt und ist deshalb bei jedem Projekt unterschiedlich.

Der eindeutige Index:
add_index :articles, :slug, unique: true

ist wichtig, da der Slug zur Identifikation unserer Artikel verwendet wird und nicht mehrfach vergeben werden soll.

Anschließend führen wir die Migrationen aus:
$ rails db:migrate

Damit besitzt die Tabelle articles nun eine slug-Spalte.


Slugs für bereits vorhandene Artikel erzeugen

Neu angelegte Artikel erhalten ihren Slug automatisch. Unsere bereits vorhandenen Artikel wurden jedoch erstellt, bevor die slug-Spalte existierte. 
Für diese Datensätze müssen wir die Slugs einmal erzeugen.

Dazu öffnen wir die Rails-Konsole:
$ rails console

Dort speichern wir alle vorhandenen Artikel erneut:
Article.find_each(&:save)

Anschließend verlassen wir die Konsole:
exit

FriendlyId kann beim Speichern nun aus den vorhandenen Artikelnamen die entsprechenden Slugs erzeugen.

Aus:
Batman & Robin

wird beispielsweise:
batman-robin

und damit eine URL wie:
http://localhost:8080/articles/batman-robin


Artikel über FriendlyId finden

Eine Änderung benötigen wir noch in unserem ArticlesController.

Der von Rails erzeugte Scaffold-Controller sucht einen Artikel standardmäßig anhand seiner numerischen ID. In set_article steht deshalb zunächst sinngemäß:
@article = Article.find(params[:id])

Da unsere URLs jetzt Slugs enthalten, verwenden wir stattdessen den FriendlyId-Finder:
# app/controllers/articles_controller.rb

# Use callbacks to share common setup or constraints between actions.
def set_article
  @article = Article.friendly.find(params[:id])
end

Article.friendly.find kann den von FriendlyId erzeugten Slug auflösen und den dazugehörigen Artikel laden.

Damit können unsere Artikel nun über lesbare URLs ohne vorangestellte Datenbank-ID aufgerufen werden.

Umgang mit geänderten Artikelnamen

Alte Slugs mit FriendlyId History weiterhin erreichbar machen

Unsere Artikel besitzen nun lesbare URLs auf Basis ihres Namens. Ändern wir allerdings später den Namen eines Artikels, kann sich dadurch auch sein Slug und damit seine URL ändern.

Nehmen wir als Beispiel unseren Artikel:
Batman & Robin

mit dem Slug:
batman-robin

Wird der Artikel später in:
Batman & Robin 2

umbenannt und dadurch ein neuer Slug erzeugt, lautet dieser beispielsweise:
batman-robin-2

Die neue URL lautet entsprechend:
http://localhost:8080/articles/batman-robin-2

Das kann problematisch sein, wenn die bisherige URL bereits verlinkt, als Lesezeichen gespeichert oder von einer Suchmaschine erfasst wurde.

FriendlyId bietet dafür das Modul history. Es speichert frühere Slugs und ermöglicht es dadurch, einen Artikel weiterhin über einen alten Slug zu finden.

Den benötigten FriendlyId-Generator haben wir bereits im vorherigen Abschnitt ausgeführt:
$ rails g friendly_id

Die dabei erzeugte FriendlyId-Migration stellt die Tabelle zur Verfügung, die unter anderem für die Slug-Historie benötigt wird. Da wir anschließend bereits:
$ rails db:migrate

ausgeführt haben, müssen wir an dieser Stelle keine weitere Migration erstellen.

Nun aktivieren wir zusätzlich history in unserem Article-Model:
# app/models/article.rb

class Article < ApplicationRecord
  extend FriendlyId

  friendly_id :name, use: %i[slugged history]
end

Bisher hatten wir lediglich:
use: :slugged

Nun verwenden wir mehrere FriendlyId-Module. Deshalb geben wir sie als Array an:
use: %i[slugged history]

slugged kümmert sich weiterhin um die Erzeugung unserer URL-freundlichen Slugs. history sorgt zusätzlich dafür, dass vorherige Slugs gespeichert werden, wenn sich ein Slug ändert.


Einen geänderten Slug testen

Wir können das Verhalten beispielsweise mit einem Artikel namens:
Hello World

testen.

Nach dem Erstellen besitzt dieser Artikel zunächst den Slug:
hello-world

Ändern wir anschließend den Namen so, dass FriendlyId einen neuen Slug erzeugt, kann der vorherige Slug in der History erhalten bleiben.

Dadurch kann FriendlyId den Artikel später nicht nur über seinen aktuellen Slug, sondern auch über einen gespeicherten früheren Slug finden.

Wichtig ist dabei: Die History kann natürlich nur Slug-Änderungen speichern, die ab dem Zeitpunkt stattfinden, an dem die History-Funktion eingerichtet ist. Slugs, die bereits vorher geändert und nirgendwo gespeichert wurden, können nachträglich nicht automatisch rekonstruiert werden.


Alte URLs auf die aktuelle URL umleiten

FriendlyId kann einen Artikel über einen historischen Slug finden. Für eine saubere URL möchten wir den Besucher anschließend jedoch auf die aktuelle URL des Artikels weiterleiten.

Dafür passen wir die show-Action in unserem ArticlesController an:
# app/controllers/articles_controller.rb

# GET /articles/1 or /articles/1.json
def show
  return unless request.path != article_path(@article)

  redirect_to @article, status: :moved_permanently
end

Zunächst vergleichen wir den aktuell aufgerufenen Pfad:
request.path

mit dem Pfad, den Rails für den aktuell geladenen Artikel erzeugt:
article_path(@article)

Sind beide Pfade identisch, passiert nichts und die normale show-View wird angezeigt.

Unterscheiden sich die Pfade dagegen, wurde der Artikel beispielsweise über einen alten Slug aufgerufen. In diesem Fall leiten wir auf seine aktuelle URL weiter:
redirect_to @article, status: :moved_permanently

Mit :moved_permanently sendet Rails dabei den HTTP-Status 301 Moved Permanently.

Aus einem Aufruf der alten URL:
http://localhost:8080/articles/batman-robin

kann damit automatisch eine Weiterleitung auf die aktuelle URL erfolgen:
http://localhost:8080/articles/batman-robin-2

Der Artikel bleibt dadurch über seinen alten Slug erreichbar, während Besucher anschließend die aktuelle URL verwenden.

Weitere Möglichkeiten mit FriendlyId

FriendlyId um zusätzliche Funktionen erweitern

Damit haben wir die grundlegenden Funktionen von FriendlyId für unsere Blogging-Anwendung eingerichtet. Unsere Artikel besitzen nun lesbare Slugs, können über diese gefunden werden und frühere Slugs lassen sich über die History weiterhin auflösen.
FriendlyId bietet darüber hinaus weitere Module, mit denen wir das Verhalten der Slugs an die Anforderungen einer Anwendung anpassen können.


Bestimmte Slugs reservieren

Mit dem Modul reserved können bestimmte Begriffe von der Verwendung als Slug ausgeschlossen werden.
Das ist beispielsweise für Begriffe sinnvoll, die bereits eine besondere Bedeutung innerhalb unserer Rails-Routen besitzen. Typische Beispiele sind:
new
edit

Dadurch können wir verhindern, dass ein Artikel einen Slug erhält, der mit einer vorhandenen Route kollidieren könnte.

Das Modul kann zusammen mit unseren bisherigen FriendlyId-Funktionen verwendet werden:
# app/models/article.rb

class Article < ApplicationRecord
  extend FriendlyId

  friendly_id :name, use: %i[slugged history reserved]
end

Welche Begriffe reserviert werden, lässt sich über die FriendlyId-Konfiguration festlegen.

Slugs auf einen bestimmten Bereich beschränken

Mit scoped können Slugs innerhalb eines bestimmten Zusammenhangs eindeutig gehalten werden, anstatt sie für die gesamte Tabelle eindeutig machen zu müssen.

Das ist beispielsweise praktisch, wenn Artikel zu unterschiedlichen Kategorien gehören und derselbe Slug innerhalb verschiedener Kategorien vorkommen darf.

Ein konzeptionelles Beispiel wäre:
news/hello-world
tutorials/hello-world

Beide Einträge verwenden hello-world, befinden sich jedoch in unterschiedlichen Bereichen.

Für solche Datenstrukturen kann FriendlyId das scoped-Modul verwenden. Dafür ist allerdings eine entsprechende Assoziation beziehungsweise Spalte im Datenmodell erforderlich.


Mehrsprachige Slugs

Auch bei mehrsprachigen Anwendungen kann FriendlyId erweitert werden. Abhängig von der eingesetzten FriendlyId-Version und der gewählten Internationalisierungslösung können Slugs für unterschiedliche Sprachen beziehungsweise Locales verwaltet werden.

Das ist beispielsweise interessant, wenn eine Anwendung ihre Inhalte in mehreren Sprachen anbietet und auch die URLs entsprechend lokalisiert werden sollen.

Fazit

Mit FriendlyId können wir die standardmäßigen numerischen Rails-URLs durch besser lesbare und aussagekräftigere Slugs ersetzen.
In unserer Blogging-Anwendung haben wir dazu:
  • FriendlyId installiert und in unser Article-Model eingebunden,
  • mit slugged URL-freundliche Slugs erzeugt,
  • die Artikel über Article.friendly.find anhand ihres Slugs geladen,
  • mit history frühere Slugs gespeichert und
  • alte URLs dauerhaft auf die aktuelle Artikel-URL weitergeleitet.
Damit erhalten wir URLs wie:
http://localhost:8080/articles/batman-robin-2

anstelle einer rein numerischen URL wie:
http://localhost:8080/articles/1

FriendlyId bietet darüber hinaus noch weitere Konfigurationsmöglichkeiten. Welche davon sinnvoll sind, hängt von der Struktur und den Anforderungen der jeweiligen Rails-Anwendung ab.
Meld dich an und schreibe ein Kommentar