紅孔雀 このページをアンテナに追加 RSSフィード

2009-11-03 ページごとに <title> の値を変更する方法

[] ページごとに <title> の値を変更する方法

Rails でページごとに <title> の値を変更する方法についてまとめます。

通常、ヘッダやフッタなどの共通部分は app/views/layouts に置かれるレイアウトファイルに記述し、ボディ部分を個々の View ファイルに記述します。

このとき問題となるのが、レイアウトファイルに共通部分として記述されている内容の一部を変更するにはどうするか、ということです。例えば、「ページごとに <title> で指定するページのタイトルをどのように変えたい」という場合です。

[] ページごとに <title> の値を変更する方法(content_for を使用する)

content_for を使用する

ページごとに <title> の値を変更するには content_for メソッドを使用することができます。

例として以下の構成を考えます。

  • app/views/layout/default.html.erb(レイアウト用のファイル)
  • app/views/users/index.html(ユーザ一覧ページ)
  • ...(他のページ)

この場合、app/views/layout/default.html.erb(レイアウト用のファイル)の内容は以下のようになります。

<!-- app/views/layouts/default.html.erb -->

<html>
<head>
<!-- この部分に「content_for :title」の内容が挿入される -->
<title><%= yield :title %></title>
</head>
<body>
<%= yield %>
</body>

そして、app/views/users/index.html(ユーザ一覧ページ)の内容は以下のようになります。

<!-- app/views/users/index.html -->

<!-- default.html.erb の <%= yield :title %> の部分に挿入される -->
<% content_for :title do %>
ユーザ一覧
<% end %>

<!-- 以下、default.html.erb の <%= yield %> の部分に挿入される -->
...

結果として、以下のような HTML が生成されます。

<html>
<head>
<title>ユーザ一覧</title>
</head>
<body>
...
</body>

同様に、他のページにも「content_for :title」を追加すれば、ページごとに <title> の値を変更することができます。

content_for を使用する方法の問題点

content_for を使用する方法でやりたいことは実現できるのですが、以下の点に不満が残ります。

  • ページのタイトル文字列が個々の View ファイルに分散してしまう。
  • 個々の View ファイルで「content_for :title」を忘れた場合に気付きにくい(「yield :title」の部分が空白になるだけ)。

[] ページごとに <title> の値を変更する方法(page_title メソッドを実装する)

やりたいこと

やりたいことをまとめると以下の通りです。

  • ページのタイトル文字列は一箇所で管理したい。
  • 個々の View ファイルに特別な処理は書きたくない。

方針

やりたいことを実現するために、以下の方針を採ることにします。

  1. 各ページのタイトルは config/locales/ja.yml に記述する*1
  2. 現在表示しているページを判別し、それに対応する値を config/locales/ja.yml から取得する。

具体的には config/locales/ja.yml には以下のように記述します。

# config/locales/ja.yml

ja:
  # このセクションに各ページのタイトル文字列を記述することにします
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録
      edit:   ユーザ編集
      update: ユーザ編集
  ...

そして、app/views/layout/default.html.erb(レイアウト用のファイル)は以下のように記述できる仕組みを作ります。

<!-- app/views/layout/default.html.erb -->

<html>
<head>
<!-- page_title は現在表示しているページのタイトル文字列を返すメソッドとします -->
<title><%= page_title %></title>
</head>
<body>
<%= yield %>
</body>

page_title メソッドを実装する

現在表示しているページのタイトル文字列を返す page_title メソッドを ApplicationHelper に実装します。

config/locales/ja.yml に記述したタイトル文字列の取得は、以下のように I18n.t メソッドで行うことが出来ます。

I18n.t('title.users.index') # => 'ユーザ一覧'

ですので、これから作成する page_title メソッドでは「I18n.t('title.XXX.YYY')」の「XXX」と「YYY」の部分に指定する値(コントローラ名とアクション名)を準備すれば良いことになります。

現在アクセスされているページのコントローラ名とアクション名はそれぞれ「controller_name」および「action_name」で取得することができる*2ので、page_title メソッドの実装は以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title
    t("title.#{controller_name}.#{action_name}")
  end

end

page_title メソッドの問題点

ここで実装した page_title メソッドにはいくつかの問題点(制限)があります。それは、config/locales/ja.yml の以下の部分です。

# config/locales/ja.yml

ja:
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録 # new と同じ値を指定する必要がある
      edit:   ユーザ編集
      update: ユーザ編集 # edit と同じ値を指定する必要がある
  ...

コメントに書いた通り、「# new と同じ値を指定する必要がある」と「# edit と同じ値を指定する必要がある」というのが制限になります。

理由は、render メソッドで描画するテンプレートを明示的に指定された場合に対応できていないためです。以下のコードを見てください。

class UsersController < ActionController::Base

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])

    if @user.save
      redirect_to :action => index
    else
      render :action => :new # 登録画面が表示されるが action_name の値はあくまで 'create'
    end
  end

  ...

end

コメントに書いたとおり、「render :action => :new」としても「action_name」の値はあくまで 'create' となるため、先ほど作成した page_title メソッドでは「ja: > :title > :users > :create」の値が返されます。

そのため、config/locales/ja.yml の「new と create」および「edit と update」に異なる値を指定すると、validation エラーが発生したときだけページのタイトル文字列が変わってしまうという問題が発生します。

page_title メソッドの問題点 (2)

今度は「登録画面」の次に「確認画面」を入れる場合を考えます。

config/locales/ja.yml の内容は以下のようになります。

# config/locales/ja.yml

ja:
  title:
    users:
      index:        ユーザ一覧
      show:         ユーザ詳細
      new:          ユーザ登録
      confirm_new:  ユーザ登録確認 # この場合はどうしよう?
      create:       ユーザ登録
      edit:         ユーザ編集
      confirm_edit: ユーザ編集確認 # この場合はどうしよう?
      update:       ユーザ編集
  ...

すると、「new と confirm_new に同じ値を指定する」というアプローチでは苦しくなります。登録確認画面のタイトルは「ユーザ登録」ではなく「ユーザ登録確認」としたいのですが、そうしてしまうと登録画面で validation エラーが発生した場合に、ページのタイトルが「ユーザ登録確認」となってしまいます。

[] ページごとに <title> の値を変更する方法(page_title メソッドを改良する)

方針

render メソッドでテンプレート名が明示的に指定された場合に問題があることが分かったので、以下の方針で対応します。

  1. render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持する。
  2. page_title メソッドでは @current_render_target の値からページのタイトル文字列を取得する。

render メソッドをオーバーライドする

render メソッドは ApplicationController でオーバーライドします。

render メソッドが呼び出されるパターンと @current_render_target に設定する値をまとめます。

  • 引数がハッシュの場合
    • (A) 「render :action => :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
    • (B) テンプレート名が指定されない場合は action_name の値を設定する。
  • 引数がハッシュではない場合
    • (C) 「render :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
    • (D) テンプレート名が指定されない場合(明示的に render メソッドが呼ばれない場合)は action_name の値を設定する。

これを実装すると以下のようになります。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  def render(options = nil, extra_options = {}, &block)

    # レンダリング対象のテンプレート名を取得する
    if render_options.kind_of?(Hash)
      # (A) または (B) の場合
      @current_render_target = render_options[:action] || action_name
    else
      # (C) または (D) の場合
      @current_render_target = render_options || action_name
    end

    # レンダリング処理自体は ActionController::Base.render に任せる
    super
  end

  ...

end

page_title メソッドを改良する

ApplicationHelper に作成した page_title メソッドは以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title
    # 変更前
    # t("title.#{controller_name}.#{action_name}")

    # 変更後
    t("title.#{controller_name}.#{@current_render_target}")
  end

end

これで render メソッドでテンプレート名が指定された場合にも正しく動作するようになりました。

page_title メソッドをもう少し改良する

page_title メソッドをもう少し改良して、

  • 引数で指定されたページのタイトルを返す
  • 引数が省略された場合は現在のページのタイトルを返す

とすれば、より便利になります。

実装は以下のようになります。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title(controller=controller_name, action=@current_render_target)
    t("title.#{controller}.#{action}")
  end

end

[] ページごとに <title> の値を変更する方法(まとめ)

長くなりましたので、最終的に行ったことをまとめます。

メッセージリソース(config/locales/ja.yml)

メッセージリソース(config/locales/ja.yml)には各ページのタイトル文字列を記述します。

# config/locales/ja.yml

ja:
  # 各ページのタイトル文字列
  title:
    users:
      index:  ユーザ一覧
      show:   ユーザ詳細
      new:    ユーザ登録
      create: ユーザ登録
      edit:   ユーザ編集
      update: ユーザ編集
  ...

レイアウト用のファイル(app/views/layout/default.html.erb)

レイアウト用のファイル(app/views/layout/default.html.erb)では page_title メソッドで取得した値を <title> に指定します。

<!-- app/views/layout/default.html.erb -->

<html>
<head>
<!-- page_title は現在表示しているページのタイトル文字列を返すメソッドとします -->
<title><%= page_title %></title>
</head>
<body>
<%= yield %>
</body>

ApplicationController(app/controllers/application_controller.rb)

ApplicationController では render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持します。

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  def render(options = nil, extra_options = {}, &block)

    # レンダリング対象のテンプレート名を取得する
    if render_options.kind_of?(Hash)
      @current_render_target = render_options[:action] || action_name
    else
      @current_render_target = render_options || action_name
    end

    # レンダリング処理自体は ActionController::Base.render に任せる
    super
  end

  ...

end

ApplicationHelper(app/helpers/application_helper.rb)

ApplicationHelper では @current_render_target に保持されているレンダリング対象のテンプレート(または引数で指定されたページ)のタイトル文字列を取得します。

# app/helpers/application_helper.rb

module ApplicationHelper

  # 現在のページのタイトルを取得する
  def page_title(controller=controller_name, action=@current_render_target)
    t("title.#{controller}.#{action}")
  end

end

*1:config/locales/ja.yml に記述することで簡単に国際化に対応することができます。

*2:「controller_name」、「action_name」はどちらも ActionController::Base で定義されています。