サイトを多言語化するとき、HTTP_ACCEPT_LANGUAGEで動的に書き換えるんじゃなくてURLをごと変える設計を採用する場合、Railsでうまいことやれるのかが気になった。

言語や地域の URL に hreflang を使用する - Search Console ヘルプ

前提

仮にこんな感じのルーティングをしてる設定で考えてみる。

config/routes.rb

Rails.application.routes.draw do
  scope "(:locale)", locale: /en|ja/ do
    resources :articles
  end
end

守るべき仕様は、

  • canonicalは表示中のURLにlocaleが含まれるなら含み、含まれないなら含まない
  • x-defaultもちゃんと設定する
  • 下層ページも対応する
  • パラメータは維持する

こんな感じ。

だめなパターン

単純に考えてこんな実装にしたかった。

<% I18n.available_locales.each do |locale| %>
  <link rel="alternate" href="<%= url_for(**params.symbolize_keys, locale: locale, only_path: false) %>" hreflang="<%= locale %>">
<% end %>

しかしsymbolize_keysはdeprecatedになってるので使うべきじゃない・・・。

DEPRECATION WARNING: Method symbolize_keys is deprecated and will be removed in Rails 5.1, as `ActionController::Parameters` no longer inherits from hash. Using this deprecated
behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.0.1/classes/ActionController/Parameters.html (called from block in _app_views_layouts_application_html_erb___3051065901541225498_34487080 at /myapp/app/views/layouts/application.html.erb:21)

だし、そもそもpermitしてないActionController::Parametersを使うのも気持ち悪い。

Viewのyieldを使うパターン

とはいえ下層ページでもパラメータを考慮して設定しなきゃいけないので、各Viewで実装していくしかないんだろうか・・・。

とりあえずこういう実装はどうだろう。

application.html.erb

<% if yield(:alternate_locales).present? %>
  <%= yield(:alternate_locales) %>
<% else %>
  <link rel="alternate" href="<%= url_for(only_path: false) %>" hreflang="x-default">
  <% I18n.available_locales.each do |locale| %>
    <link rel="alternate" href="<%= url_for(locale: locale, only_path: false) %>" hreflang="<%= locale %>">
  <% end %>
<% end %>

articles/index.html.erb

<% content_for :alternate_locales do %>
  <link rel="alternate" href="<%= articles_url(q: @params_article.to_param) %>" hreflang="x-default">
  <% I18n.available_locales.each do |locale| %>
    <link rel="alternate" href="<%= articles_url(q: @params_article.to_param, locale: locale) %>" hreflang="<%= locale %>">
  <% end %>
<% end %>

うーん。

Controllerにメソッドを作るパターン

Viewにやらせるパターンだとちょっと冗長すぎるのでControllerにcanonicalのURLを返す & localeも設定できるようなメソッドを作って、 書き換える必要のあるディレクトリではオーバーライドするのはどうだろう。

application.html.erb

<link rel="canonical" href="<%= canonical(locale: params[:locale]) %>">

<link rel="alternate" href="<%= canonical %>" hreflang="x-default">
<% I18n.available_locales.each do |l| %>
  <link rel="alternate" href="<%= canonical(locale: l) %>" hreflang="<%= l %>">
<% end %>

application_controller.rb

helper_method :canonical
def canonical(locale: nil)
  url_for(locale: locale, only_path: false)
end

articles_controller.rb

def canonical(locale: nil)
  articles_url(q: @params_article.to_param, locale: locale)
end

パッと見イケてそう。 params[:locale]がちょっと嫌だけど。