#314 Pretty URLs with FriendlyId
- Download:
- source codeProject Files in Zip (79.8 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (9.46 MB)
- webmFull Size VP8 Video (10.5 MB)
- ogvFull Size Theora Video (20.9 MB)
ここに簡単なRailsのブログアプリケーションがあります。トップページには最近の記事のリストがあり、それぞれの記事へのリンクになっています。リンクの一つをクリックするとその記事のページが開きますが、そのURLはページの内容をよく表しているとは言えません。
記事はURLの中では内部的なid
だけで記述されていて、これがRailsのデフォルトの振る舞いです。もし何らかの形で記事の名前がURLに含まれるようにすれば、URLの表記が改善されるのではないでしょうか。
これを実現する一番簡単な方法は、URLを変更したいモデル(今回の場合はArticle
)のto_param
メソッドをオーバーライドする方法です。これはRailsがオブジェクトをURLパラメータに変換するために利用している内部的なメソッドです。
class Article < ActiveRecord::Base def to_param "#{id} #{name}".parameterize end end
これをオーバーライドして記事の名前も返すようにしました。この文字列に対してparameterize
を呼び出して、URLとして使いやすい値に変換します。これによって記事のURLは次のようになります。
http://localhost:3000/articles/1-superman
この場合にオブジェクトのid
が最初に付いていることが重要で、それによってActiveRecordのfind
メソッドがそのまま機能します。もしid
をURLに含めたくないという場合は、少し難しくはなりますがそれも可能です。
FriendlyIdを導入する
ここでFriendlyIdプラグインを利用します。これによって、頭にid
を付けずに、URLにモデルの名前を簡単に使用できるようになります。このgemには多くの機能が含まれていますが、その前にアプリケーションへの追加方法を見ていきましょう。いつもと同じようにまず最初にアプリケーションのgemfileに追加して、bundle
コマンドを実行してインストールします。
source 'http://rubygems.org' gem 'rails', '3.1.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'friendly_id'
モデルの中でto_param
をオーバーライドする代わりに、FriendlyId
でモデルを拡張し、URLで使用したい属性を指定します。
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name end
これで記事のURLに記事の名前だけが使われてid
は含まれなくなります。
http://localhost:3000/articles/Superman
この方法は、特殊な名前を持つ記事の場合にきれいではありません。例えば「Batman & Robin」という名前の記事は次のようなURLを持ちます。
http://localhost:3000/articles/Batman%20&%20Robin
Slugを使う
URLは、空白や区切り文字も含んだ記事の名前全体から生成されます。これは、FriendlyIdに渡されるパラメータがすでにURLとして見やすいものであれば問題にはなりませんが、例のような記事の名前の場合は問題です。そこで新たにslug属性を利用します。そのためにはfriendly_idのuseオプションを利用します。
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged end
これはデータベースのarticles
テーブルでslug
列を検索するので、それを作成する必要があります。ここでそのためのmigrationを作成します。
$ rails g migration add_slug_to_articles slug:string
この属性はレコードの検索に利用されるため、インデックスを追加しておくのがいいでしょう。
class AddSlugToArticles < ActiveRecord::Migration def change add_column :articles, :slug, :string add_index :articles, :slug end end
rake db:migrate
を実行して、データベースのテーブルに 列とインデックスを追加します。ここでおこなうことがもう一つあります。すでに記事のレコードがいくつか存在していますが、それらのslug列は空のままです。これを修正するためにRailsコンソールを開いて、各レコードを保存しなおします。
1.9.2-p290 :001 > Article.find_each(&:save)
これで「Batman & Robin」の記事には、新しいslugに基づいてずっときれいなURLが付きます。
http://localhost:3000/articles/batman-robin
修正された記事の名前の扱い
記事を編集して名前を変更した場合、slugも更新されます。「Batman & Robin」の記事を「Batman & Robin 2」に変更すると、slugがbatman-robin-2
に変わり、それに従ってURLも変わります。ここで記事の古いURLにアクセスすると、エラーメッセージが表示されます。
この問題を解決する方法はいくつかあります。一つはFriendlyIdに対して記事の名前が変わったときにもslugを更新しないように指示する方法で、そのためにはshould_generate_new_friendly_id?
メソッドをオーバーライドします。
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: :slugged def should_generate_new_friendly_id? new_record? end end
このコードによって、新規にArticle
が作成されたときにだけslugが生成されるようになります。「Batman & Robin 2」の記事を更新して名前が「Batman & Robin 3」になっても、slugはbatman-robin-2
のままです。
そこで両方のいいところをとって、名前が変更されたらslugは更新されながら、古いslugも認識されて古いURLも有効なままにしたい場合は、どうすればいいでしょうか?これはFriendlyIdのhistory
オプションを使用することで可能です。オーバーライドされたshould_generate_new_friendly_id?
メソッドを削除してhistory
オプションを追加すると、過去のslugの履歴を保存してくれます。
class Article < ActiveRecord::Base extend FriendlyId friendly_id :name, use: [:slugged, :history] end
この履歴をどこかに保存しなくてはいけないので、新規にデータベーステーブルを作成します。FriendlyIdにはこれを自動でおこなうジェネレーターが含まれています。
$ rails g friendly_id create db/migrate/20120101000001_create_friendly_id_slugs.rb
rake db:migrate
を実行するとfriendly_id_slugs
というテーブルが作成されます。
一つ注意しなくては点があります。履歴の機能は新規の作成されたレコードでのみ動作するようです。すでにレコードが存在する場合は、この機能を追加した後でそれらを再生成する必要があります。テストのために「Hello World」という記事を新たに作成します。この記事は次のようなURLを持ちます。
http://localhost:3000/articles/hello-world
記事を編集して名前を「Hello World 2」に設定するとslugはhello-world-2
に変わりますが、元のURLも引き続き機能します。
古いURLにアクセスした場合は、現在のURLにリダイレクトされる方がいいでしょう。そのためにコントローラを一部変更します。ArticlesController
は標準的なRESTfulなスタイルのコントローラで、通常の7つのアクションをサポートします。個別の記事を表示するアクションであるshowアクションに修正を加えます。現状は次のようになっています。
def show @article = Article.find(params[:id]) end
このアクションを修正して、ページにアクセスするのに使われたURLが現在のものでなかった場合に、記事の現在のURLにリダイレクトするようにします。これを実現するためには、ページにアクセスするのに使われたパスが現在の記事のパスと違うかどうかをチェックします。もし違う場合は、ユーザは古いslugか記事のid
を使用したことになります。これらの場合には、現在のURLにリダイレクトするようにします。
def show @article = Article.find(params[:id]) if request.path != article_path(@article) redirect_to @article, status: :moved_permanently end end
http://localhost:3000/articles/hello-worldにアクセスすると、その記事の現在のURLにリダイレクトされます。
今回はFriendlyIdの一部の機能しか紹介できなかったので、その他にどのようなことができるかを知るためにドキュメンテーションを読むことをお勧めします。例えばReservedモジュールを使うと、newやeditなどの特定のキーワードを予約語として除外(reserve)してslugで使われないようにします。Scopedモジュールを使うとslugのscopeをassociation内に限定します。SimpleI18nモジュールは複数の言語で国際化をサポートします。