Rails で category.stg.example.com のような URL に対応する

きっかけ

ステージング環境上のランディングページ(LP)を操作中、知らない間に本番環境のページに移動し、誤ってコンバージョンしてしまった。ビューに本番環境の URL が直書きされていたことが原因だった。

課題

文字列で直書きされていた URL を、URL ヘルパーに置き換えればいいが、現在の実装ではサブドメインを考慮して URL ヘルパーを利用しなければならず、少し面倒である。

サブドメインの考慮について説明するため、LP で使われる URL のパターンを以下に列挙する。

ステージング環境は、category.stg.example.com のように、カテゴリ(category)と環境(stg)のサブドメインから構成されているが、それ以外の環境は、環境を表すサブドメインはなく、カテゴリを表すサブドメインだけを持つ。

そうしたことから、ルーティングでは、環境を示すサブドメインは無視し、カテゴリのサブドメインだけをチェックする必要がある。これは以下のルーティングで実現できる。

# routes.rb

# 環境を表すサブドメインが category の後ろに付いていても一致する(先頭一致)
constraints ->(req) { req.subdomain.start_with?('category-a') } do
  # ...
end

constraints ->(req) { req.subdomain.start_with?('category-b') } do
  # ...
end

URL ヘルパーでは、このようにサブドメインを指定する。

link_to form_index_url(subdomain: 'category.stg')

しかし、このように URL ヘルパーを使うたび、subdomain オプションを指定するのは面倒である。

以下のようにサブドメインの文字列を返すヘルパーを定義し、URL の生成に使うこともできるが、面倒さはあまり緩和されない*1

# app/helpers/application_helper.rb
module ApplicationHelper
  def subdomain_for(category)
    "#{category}#{Rails.env.staging? ? '.stg' : ''}"
  end
end
<%= link_to form_index_url(subdomain: subdomain_for('category')) %>

解決方法

そもそも Rails がパースしたサブドメインに環境名(stg など)が入ってしまうことが問題なので、そうならないようにドメインのパース方法を変更すれば良い。

これには config.action_dispatch.tld_length を利用できる。Ralis がドメインサブドメインをパースするのに使用する設定である*2

例えば、ホスト名が category.stg.example.comtld_length が 1(デフォルト値)の場合

とパースされる。tld_length が 2 の場合は

とパースされる。

この性質を利用し、ステージング環境の場合に

config.action_dispatch.tld_length = 2

とすることで、ルーティングを以下のようにシンプルに書けるようになる。これは Rails がパースしたリクエストのサブドメインと、constraintssubdomain オプションで渡した文字列と完全一致するためだ。

constraints subdomain: 'category' do
  #  ブロック内のルーティング
end

同様に開発環境では

config.action_dispatch.tld_length = 0

にすることで、

とパースされるので、categoryサブドメインと認識させることができる。

副作用

config.action_dispatch.tld_length はアプリケーション中の URL のパースに関わる箇所すべてに適用されるため注意が必要である。

代替手段

副作用が気になる場合は、こういう代替手段を取ることができる。

# lib/routing_helper.rb
module RoutingHelper
  def subdomain_for(category)
    "#{category}#{Rails.env.staging? ? '.stg' : ''}"
  end
end
# config/routes.rb
include RoutingHelper

namespace :category, path: '' do
  constraints subdomain: subdomain_for(category) do
    # ...
  end
end
<%= link_to category_form_index_url %>

*1:URL ヘルパーをオーバーライドする方法も考えられるが、Rails のバージョンアップで動かなくなるリスクを避けたいので、採用しなかった

*2:tld_length はトップレベルドメインの長さを表す。ドメイン部をドットで区切った配列の末尾から tld_length - 1 までがトップレベルドメイン、それ以降がサブドメインとしてパースされる