てくすた

ピクスタ株式会社のエンジニア・デザイナーがつづるよもやまテクニカルブログです

Gretel をよしなにハックしてパンくずと JSON-LD の生成管理を一元化したい人生だった

エンジニアの id:cheezenaan です。『劇場版 響け!ユーフォニアム〜届けたいメロディ〜』の Blu-ray が届いてはや数週間、心身ともに充実した日々を送っています。いやぁ、自分もこんな高校生活を過ごしてみたかったものです。。

さてピクスタでは、昨年秋から PIXTA サイトのモバイル対応をひっそりと進めてきました。「なぜ今モバイル対応なのか?」という背景や、具体的な取り組みについては、過去に開発部メンバーが執筆した記事をご覧ください。

これらの記事でも触れているとおり、サイト各ページへのモバイル対応はデザイナーさんが中心に行っていましたが、PC 版とモバイル版とで表示する項目そのものや挙動を変える場合は、エンジニアも力を貸すことが少なくありませんでした。

今回の記事では、エンジニアの取り組みのひとつである PC 版サイトとモバイル版サイトそれぞれの事情に適した構造化マークアップを、既存のライブラリをハックして対応した話 について、軽く話ができればと思います。

モバイル対応と構造化マークアップとさまざまな課題

PIXTA サイトのモバイル対応を進めるにあたり、次のような要望が上がってきました。

  • モバイル版サイトでは端末の画面幅を少しでも広く使いたいため、パンくずを非表示にしたい
  • パンくずを非表示にする代わりに、構造化マークアップを JSON-LD で行いたい1

f:id:cheezenaan:20180317174420p:plainf:id:cheezenaan:20180317173403p:plain:h320
PC 版サイト(左)で表示しているパンくずが、モバイル版サイト(右)では非表示になっているのがわかります。

こうした要望を実現するにあたり、現状のコードベースや既存ライブラリに考慮すべき課題が生じており、長く頭を悩ませることになりました。

現状のコードベースが密結合でつらい

現在動いているコードベースを読み込んでいくと、パンくずおよび microdata による構造化マークアップの生成処理が View にほぼベタ書きで行われていたことが判明しました。

具体的には、パンくずとして表示するためのデータを整形する処理と、整形したデータに基づくパンくず(および microdata)の生成処理がすべて View で行われていました。パンくずと microdata の内容が密接に関連することもあったのか、実装まで密結合になっていました2

その場の実装スピードを重視するなら、今のコードベースをそのままに JSON-LD の生成処理を新たに追加する選択肢も視野にありましたが、「構造化マークアップという同じ目的を実現するために似たような処理を追加してこの先メンテナンスしていくって、まぁつらいよね…」と、精神衛生上よろしくありません。

このような実装上の課題と先のビジネス要件をふまえて、「いっそのこと PC 版サイトからパンくずも microdata も取り去って、 JSON-LD に一本化できないだろうか…?」とビジネスサイドに相談してみたところ、

  • 構造化マークアップは、PC/モバイルの両サイトともに JSON-LD で統一していく(まずはモバイル対応の対象ページから)
  • ただし「ユーザーの使い勝手」という観点から、 PC 版サイトでのパンくずは引きつづき表示してほしい

という内容で合意を得ました。microdata と JSON-LD の二重管理を考える必要がなくなったので、まずは一歩前進です。

ビジネス要件を満たせる既存ライブラリが存在しない

しかし解決すべき課題はまだ残っていました。先に貼ったキャプチャのように、 PIXTA のサイトを眺めていると 1 ページ内で 2 つのパンくず生成および構造化マークアップを行う箇所が存在しているのです。

1 ページに 1 つの構造化マークアップを行えば済むようなシンプルなサイトであれば、既存のライブラリ(Gem)で問題なくカバーできるのですが、「現時点では、PIXTA サイトの要件をクリアする Gem が存在しないやんけ…/(^o^)\」ということが判明してしまいました。

参考までに、導入を検討したライブラリは以下の 2 つです。

Gretel

  • pros: 1 ページ内に複数のパンくずを生成できるように DSL が定義されている。パンくず用の HTML タグもユーザー側でカスタマイズできる
  • cons: JSON-LD の生成は公式ではサポートしておらず、ユーザーが独自に拡張した DSL を書いてやる必要がある

Breadcrumbs on Rails

  • pros: カスタムビルダーを駆使すると JSON-LD による構造化マークアップも可能
    • ピクスタの新規事業である fotowa では Breadcrumbs on Rails を採用しています
      • 詳細はこちら:
  • cons: 用意されている DSL だと 1 ページ 2 つ以上の構造化マークアップの生成ができない(カスタムビルダーを別途用意する必要あり?)

これらの調査結果から、ビジネス要件への適応ぐあいや、コードベースへの親和性を鑑みて、「Gretel をハックしてパンくずと JSON-LD の生成を一元化する」方針に決定しました。

実際にやってみた

方針が決まればあとは実行あるのみ。やることは大きく 2 つです。

  • パンくずと JSON-LD の生成に必要な情報(URL やタイトルなど)を Value Object に集約させる
  • ↑ の Value Object を Gretel に引き渡してパンくずと JSON-LD を生成できるようにする

今回の対応にあたり、技術推進チームの id:Yasaichi が Gretel を JSON-LD に対応させた Gem をシュッと用意してくれたので、こちらを使用します。

先の Gem で 1 ページに 2 つ以上の JSON-LD を生成できるようにパッチを作成します。

module Gretel::JSONLD::ViewHelpersPatch
  def breadcrumb(*args)
    super
    gretel_renderer_args << args
  end

  def jsonld_breadcrumbs(options = {})
   return super if gretel_renderer_args.empty? || !options.delete(:multiple)

    script_tags = gretel_renderer_args.each_with_object([]) do |args, array|
      with_breadcrumb(*args) do
        array << super(options)
      end
    end

    script_tags.join.html_safe
  end

  private

  # Cache all arguments passed to #breadcrumb and #with_breadcrumb
  def gretel_renderer_args
    @_gretel_renderer_args ||= Set.new
  end
end

ActionView::Base.prepend(Gretel::JSONLD::ViewHelpersPatch)

次に Gretel 公式の README を読みつつ config/breadcrumbs.rb に設定ファイルを用意します。

# config/breadcrumbs.rb
crumb :root do
  link I18n.t("breadcrumb.home"), root_path
end

crumb :sample_breadcrumbs do |breadcrumbs|
  breadcrumbs.each do |breadcrumb|
    link breadcrumb.name, breadcrumb.path
  end
  parent :root
end

設定ファイルを用意したら、Gretel へ渡しやすいインターフェースを意識しながら、 Controller と View のリファクタリングを進めていきます(以降のサンプルコードは本記事用にいろいろと端折っています)。

class SampleController < ApplicationController
  def show
    @item = Item.find_by(id: params[:id])
    # ...
    @category_based_breadcrumbs = Breadcrumbs::Category.new(item: @item, categories: @item.categories).to_a
    @content_based_breadcrumbs = Breadcrumbs::Content.new(item: @item, categories: @item.categories).to_a
  end

  # ...
end

Controller 内で Breadcrumbs::CategoryBreadcrumbs::Content という Value Object を生成して View へ渡します。 Value Object の実装イメージは以下の Gist を参照ください(ちょっと長いです)。

あとは View に渡ってきたインスタンス変数を Gretel へつないでやれば OK です。パンくずはページによって表示位置が異なるのでパーシャルで、 JSON-LD は <body> タグの最下部に置きたい3ので app/views/layouts/application.html.erb で定義してあげます。

<!-- app/views/sample/show.html.erb --!>

<!-- モバイル版ではパンくずが非表示になるように別途スタイルを適用する -->
<div class="breadcrumbs">
  <%= render "shared/breadcrumbs", breadrumbs: @category_based_breadcrumbs %>
  <%= render "shared/breadcrumbs", breadrumbs: @content_based_breadcrumbs %>
</div>
<!-- app/views/shared/_breadcrumbs.html.erb -->
<% breadcrumb :sample_breadcrumbs, breadcrumbs %>
<ul>
  <% breadcrumbs(autoroot: false).tap do |links| %>
    <% links.each do |link| %>
      <%= content_tag(:li) do %>
        <% if link.current? %>
          <%= content_tag :strong, link.text %>
        <% else %>
          <%= link_to link.text, link.url %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>
</ul>
<!-- app/views/layouts/application.html.erb -->
<%= jsonld_breadcrumbs multiple: true %>

これで PC 版サイトのみにパンくずを表示しつつ、JSON-LD による構造化マークアップを PC 版/モバイル版の両サイトに適用できました。

さらに今回のリファクタリングによって、構造化マークアップのためのデータ整形処理と、データに基づいたパンくずや JSON-LD の生成処理とを分割できたおかげで、それぞれの処理に対してテストを書けるようになりました。後者の処理に関するテストをあらたに追加して、振る舞いの正しさを担保しておきました。

まとめ

だいぶ長くなりましたが、 『PIXTA のサイトモバイル化に際して発生した構造化マークアップ対応』という課題を、ビジネス要件とコードベースを勘案しつつ、既存のライブラリではカバーしきれないところは自分たちでハックして対応してみたよという話をご紹介してきました。本記事で取りあげた構造化マークアップをはじめとした SEO 施策を自社サービスでどう運用していくかお悩みのエンジニアのみなさんが、少しでも参考にしていただければ幸いです。

エンジニア募集中!

ピクスタでは、エンジニアリングの力でよりよい落としどころを見つながらサービスを改善していきたいエンジニアを募集しています。


  1. モバイル対応以前は microdata による構造化マークアップを行っていました。

  2. たとえば、構造化マークアップを microdata から JSON-LD へ切り替える際に、パンくずの生成処理にも影響が出てしまう。オブジェクト指向設計原則でいうところの「オープン/クローズドの法則」に著しく違反していました。

  3. JSON-LD は HTML 内のどこに記述しても問題ないのですが、 <script type="application/ld+json">{ ... }</script> のように <script>タグ内に定義するため、 <body> タグの最下部に置くのがよさそう…という判断をしています。