Railsアプリでクロールディレクティブを安全・効率的に設定する仕組み

こんにちは、フロントエンジニアの茨木です。
本記事ではRailsアプリでクロールディレクティブを安全・効率的に設定する仕組みをご紹介したいと思います。

Web上にあるページは、クローラーと呼ばれるロボットに巡回されて検索エンジンにインデックス登録されます。大規模なサイトにおいてはページを効率よくインデックス登録させる必要があります。その際にクロールディレクティブと呼ばれる様々な設定が必要ですが、管理が複雑になってきます。この問題に対して、VASILYでの解決方法をご紹介します。同じような境遇の方々の参考になれば幸いです。

クロールディレクティブとは

クロールディレクティブは、クローラーにサイト巡回の仕方を伝えるための設定です。これにより、クローラーに対してページをインデックス登録させない、ページ内のリンクを辿らせないといった設定を行うことができます。クロールディレクティブによりどのような設定ができるかはGoogle公式ページにも記載があります。
ここでは、弊社で重要視している3つのクロールディレクティブをご紹介します。

noindex

noindexはページをインデックス登録させないことを示すディレクティブです。これを重複コンテンツや低品質コンテンツのページに指定することで、サイトの評価低下やそれによるクローラーリソースの割当減少を防止できます。noindexをhtml内で設定する場合にはheadタグ内に以下のタグを挿入します。

<meta name="robots" content="noindex">

nofollow

nofollowは、クローラーにページ内のリンクを辿らせないことを示すディレクティブです。noindexを指定したページへのリンクにnofollowを指定することで、クローラーリソースの無駄遣いを防止できます。nofollowはheadタグ内のmetaタグか任意のaタグで指定が可能です。metaタグで指定した場合にはページ内の全リンクに、aタグで指定した場合には自身のリンクにnofollowが設定されます。

例1) metaタグでのnofollow設定

<meta name="robots" content="noindex">

例2) aタグでのnofollow設定

<a href="hogehoge.html" rel="nofollow">リンク</a>

canonical

canonicalは正規URLを示すディレクティブです。同じコンテンツに複数のURLでアクセスできる場合、非正規URLのページにcanonicalを設定することでクローラーに正規のURLを通知することができます。canonicalはheadタグ内のlinkタグで指定が可能で、href属性で正規URLを設定します。

例) linkタグによるcanonical設定

<link rel="canonical" href="https://www.iqon.jp/">

IQONにおけるクロールディレクティブと課題

クロールディレクティブの仕様

IQONは、アイテム・コーディネート・相談・記事といった多くのコンテンツを持っています。そして、それらの各ページにクロールディレクティブを設定しています。コンテンツの中には検索エンジンからのランディングに適さないページもあり、そのようなページにはnoindexを指定しています。 以下の表はURLとそれに対応するクロールディレクティブの例です。

URL index/noindex canonical
https://www.iqon.jp/
(トップページ)
index -
https://www.iqon.jp/ask/
(相談ページ)
noindex -
https://www.iqon.jp/ask/solved/
(解決済み相談ページ)
index -
https://item.iqon.jp/20219512/
(アイテム詳細ページ)
index https://item.iqon.jp/20219511/
https://item.iqon.jp/brand/a.v.v/18/ジャケット/?price_max=30000
(アイテム検索ページ)
noindex -

アイテム検索ページにはカテゴリ、価格、袖丈といった様々な絞込条件があります。絞込条件によっては検索エンジンからのランディングに適さない場合もあるので、絞込条件に応じてnoindexを設定しています。「a.v.v、ジャケット、長袖、5000円以下、セール」で検索した場合は、以下のようなURLになります。

https://item.iqon.jp/brand/a.v.v/18/ジャケット/?price_max=5000&sleeve_length=長袖

各絞込条件に応じてnoindexを制御しているので、以下の例のようにロジックが複雑になっています。

uri = URI.parse(request.original_url)
path = uri.path
query_params = URI.decode_www_form(uri.query).to_h
if path !~ /^\/brand\//
  if query_params.price_max.to_i > 50000 || query_params.sleeve_length.present?
    @noindex = true
  end
elsif query_params.price_max.to_i > 100000
  @noindex = true
end
if @items.length < 10
  @noindex = true
end

課題

これまでに述べたように、IQONでは複雑なクロールディレクティブを設定しています。そのため、以下の課題がありました。

  • noindexページへのリンクにnofollowを効率よく設定できない
  • クロールディレクティブが複雑なために設定ミスが発生する

nofollowは先に述べた通り、各noindexページへのリンクに対して設定するのが望ましいです。しかし、そのためにはそれぞれのリンクにおいてnoindex設定条件を考慮しなければなりません。これを愚直にやるのは非効率なだけでなく、メンテナンス性の観点でも良くありません。
また、複雑なクロールディレクティブは設定ミスも引き起こすことがあります。特にnoindexの設定ミスは、インデックス数減少に伴うPV数減少など、致命的な問題を引き起こしかねません。 そこで、上記の課題を解決するための仕組みをご紹介します。

noindex・nofollow設定を効率化する仕組み

設定ロジックの共通化

noindex・nofollow設定の効率化のために、まずnoindex設定ロジックの共通化が必要です。IQONではnoindex設定ロジックを管理するNoindexManagerというクラスを定義し、それを任意の場所で利用できるようにしました。

lib/noindex_manager.rb

class NoindexManager
  def initialize(url, context)
    @url = url
    @context = context
  end

  # 実際に判定を行うインスタンスメソッド。noindexの場合にtrueを返す。
  def noindex?
    # 末尾にスラッシュがあるとうまく認識できないので予め正規表現で削る
    path = Rails.application.routes.recognize_path(@url.sub(/\/$|\/\?.*/, ''))

    # アクションに対応する判定ロジックを呼び出す
    send("check_#{path[:controller]}_#{path[:action]}".to_sym)
  end

  # アイテム検索ページのnoindex判定ロジック
  def check_item_index
    uri = URI.parse(@url)
    path = uri.path
    query_params = URI::decode_www_form(uri.query).to_h
    if path !~ /^\/brand\//
      if query_params.price_max.to_i > 50000 || query_params.sleeve_length.present?
        return true
      end
    elsif query_params.price_max.to_i > 100000
      return true
    end
    if @context[:item_count] < 10
      return true
    end

    return false
  end
end

NoindexManagerはURLとコンテキストを用いてnoindexの判定を行います。コンテキストには「URLに含まれないがnoindex判定に用いられる値」を渡します。これにより、URLに含まれないアイテム件数などを判定条件に含めることができます。 NoindexManagerはURLとコンテキストによってインスタンスが生成され、インスタンスメソッドNoindexManager#noindex?によって実際の判定が行われます。

NoindexManager.new('https://item.iqon.jp/', {item_count: 150}).noindex?

ここでは説明のため省略していますが、実際には簡単な判定ロジックの場合にYAMLファイルで設定できるような仕組みがあります。

nofollowを考慮したリンク生成ヘルパー

前述のNoindexManagerを更に便利に使うために、nofollowを考慮できるリンク生成ヘルパーを定義しています。Rails標準のヘルパーであるlink_toと同様のインターフェースで使用できるようにしています。NoindexManagerで用いられるコンテキストは、オプションのcontextキーで設定できるようになっています。

application_helper.rb

def nofollow_link_to(body, url, options = {}, &block)
  url = url_for(url)
  html_options = options.reject { |k, _v| k == :context }
  nofollow = NoindexManager.new(url, options[:context]).noindex? ? 'nofollow' : nil
  html_options[:rel] ||= nofollow

  if block.present?
    link_to(url, html_options, &block)
  else
    link_to(body, url, html_options)
  end
end

application_helper.rbに一度定義すると、ビューにおいてlink_toと同様に使うことができます。

index.html.slim

ul.items
  - @items.each |item|
    li.item
      = nofollow_link_to(item.name, item.url, {context: item.count})

クロールディレクティブの正確性を担保する仕組み

クロールディレクティブの設定ミスを防止するために、クロールディレクティブをテストできる仕組みを整備しました。IQONではcontrollerテストで出力HTMLを解析することで、クロールディレクティブの設定に問題ないかテストしています。クロールディレクティブの確認にユーザーアクションは不要なので、Selenium等を用いたクライアントテストは実施していません。 これから、実際にnoindexやnofollow、canonicalをどのようにテストしているのかをご紹介します。

ページのnoindex設定のテスト

noindexのテストでは、まず出力HTMLからname=“robots"を持つmetaタグを抽出します。そして、その抽出されたタグのcontent属性に「noindex」「nofollow」という文字列があるかどうかを判定します。判定のためのメソッドをspec_helper.rbに定義しておくことで、各controllerのテストでnoindexやnofollowの設定を確認できるようになります。

spec_helper.rb

def meta_robots
  meta_tag = response.body.slice(/(<meta[^>]+?name="robots".+?\/>)/)
  content = meta_tag.slice(/(?<=content=")(.+?)(?=")/)
  index = content =~ /noindex/ ? :noindex : :index
  follow = content =~ /nofollow/ ? :nofollow : :follow

  {index: index, follow: follow}
end

item_controller_spec.rb

describe 'GET :index' do
  context 'meta robots content is `noindex,follow`' do
    it do
      response_robots = meta_robots
      expect(response_robots[:index]).to be_falsey
      expect(response_robots[:follow]).to be_truthy
    end
  end
end

ページのcanonical設定のテスト

canonicalのテストもnoindexのテストと同様に行っています。

spec_helper.rb

def meta_canonical
  link_tag = response.body.slice(/(<link[^>]+?rel="canonical".+?\/>)/)
  link_tag.slice(/(?<=href=")(.+?)(?=")/)
end

item_controller_spec.rb

describe 'GET :index' do
  context 'meta canonical content is `https://item.iqon.jp/1000000/`' do
    it { expect(meta_canonical).to eq('https://item.iqon.jp/1000000/') }
  end
end

まとめ

noindex設定ロジックの共通化や、nofollowを考慮できるリンク生成ヘルパーの導入により、noindex・nofollow設定を効率よく管理できるようになりました。 また、クロールディレクティブをテストできる仕組みを導入したことにより、クロールディレクティブの正確性を担保できるようになりました。

最後に

VASILYでは新技術でWebサービスを進化させたいエンジニアを募集しています。
興味がある方は以下のリンクをご覧ください。