【Horizon】商品カードのバッジ表示をカスタマイズする

【Horizon】商品カードのバッジ表示をカスタマイズする ― ファイル追跡から設定項目の追加まで

「特集コレクションの商品カードに割引率を表示したい」

セール商品のバッジに「SALE」だけでなく「20% OFF」のように具体的な割引率や割引額を表示し、お客様の目に留まりやすくしたいというご要望をいただくことがあります。残念ながらHorizonには標準ではその機能はありません。

そこで、その機能を実装するための手順を具体的なファイル調査から含めて行ってみます。

Dawn/Riseテーマの場合、特集コレクションのセクションから描画を担当する snippets/card-product.liquid が読み込まれているだけなので、1ステップで変更対象先に到達できます。

一方、Horizonテーマでは修正するファイル自体は数ファイルで済むものの、そのファイルにたどり着くまでに複数のファイルを読み解くことになります。Horizonのブロックアーキテクチャならではのファイル追跡の流れと具体的なカスタマイズ方法について順を追って紹介します。

なお、本記事ではHorizonのカスタマイズ方針で推奨されている custom.* ファイルの新設は敢えて行わず、ベンダーファイルを直接編集する方法を掲載しています。その理由についても詳細に述べていますので、併せてご確認ください。

完成イメージ

変更前後の比較です。SALEバッジのテキストを「セール」以外に割引率や割引額を表示できるように変更します。なお、セール時の価格表示(取り消し線付きの元値と割引後価格の併記)はHorizonの標準機能として既に実装されているため、今回変更するのはバッジ部分のみとなります。

Horizonテーマ 割引額バッジの表示例

Dawnでの変更対象の探し方

まず比較のために、Dawn/Riseテーマでの対応を確認しておきます。

Dawn/Riseでは、特集コレクションのセクション sections/featured-collection.liquid を開くと、商品カードの描画を担当する snippets/card-product.liquid が直接呼び出されています。このスニペットを開けば、画像、バッジ、商品名、価格――すべてが1ファイルに書かれているので、バッジ部分のLiquidコードを数行書き換える形となります。

// Dawn/Riseの場合

sections/featured-collection.liquid    ← セクション
  └─ snippets/card-product.liquid        ← 画像・バッジ・商品名・価格すべてここ

セクションからスニペットへ1ステップ。影響範囲もそのファイル内で完結する形となっており、Dawnの設計思想である「1つのスニペットに商品カードの全責任を持たせる」アプローチが、カスタマイズの容易さに直結しています。

Horizonでの商品カード:ファイルの連鎖

Horizonでは、商品カードは「テーマブロック」のネスト構造として実装されています。1つの商品カードの表示を理解するだけで、以下のファイルチェーンを追いかける必要があります。

// トップページ「特集コレクション」→ 商品カードのファイル連鎖

sections/product-list.liquid             ← セクション本体
  └─ blocks/_product-card.liquid            ← 商品カードブロック(staticブロックとして配置)
       └─ snippets/product-card.liquid       ← カードの外殻(Web Component)
            ├─ blocks/_product-card-gallery.liquid  ← バッジはここ
            │    └─ snippets/card-gallery.liquid       ← 画像スライドショー
            ├─ blocks/product-title.liquid           ← 商品名
            └─ blocks/price.liquid                   ← 価格(標準でセール対応済み)
                 └─ snippets/price.liquid             ← 価格フォーマット

Dawnの card-product.liquid 1ファイルに収まっていた内容が、Horizonでは複数のファイルに分散しています。

なぜこうなっているのか

Horizonは「テーマブロックアーキテクチャ」を採用しており、各要素をブロック単位で独立させることで、テーマエディタ上での柔軟な並べ替えや表示切替を可能にしています。画像・タイトル・価格がそれぞれ独立したブロックなので、テーマエディタからドラッグ&ドロップで順序変更したり、不要なブロックを非表示にしたりできます。この柔軟性の副作用として、コードを直接編集する場合、ファイルを複数辿る必要があります。

ファイル追跡の実際

「SALEバッジ」の文言を変更するために、実際にどのような手順でファイルを追跡することになるかを追ってみましょう。

Step 1:セクションを開く → ブロックへの委任を確認

まず、トップページの「特集コレクション」を担当するセクションを探します。templates/index.json を確認すると、該当セクションは sections/product-list.liquid であることがわかります。開いてみると、商品カードのレンダリングは {% content_for 'block', type: '_product-card' %} でブロックに委任されています。商品カードの中身はブロック側に定義されています。

Step 2:_product-card.liquid → スニペットに委任

blocks/_product-card.liquid を開くと、たった3行のコードしかありません。

{% capture children %}
  {% content_for 'blocks', closest.product: product %}
{% endcapture %}

{% render 'product-card', children: children, product: product %}

子ブロック(画像・タイトル・価格など)の出力をまとめて children 変数に格納し、product-card スニペットに渡しています。バッジのコードは子ブロック側にあるため、次のステップへ進みます。

Step 3:product-card.liquid → カードの外殻のみ

snippets/product-card.liquid には Web Component の <product-card> タグの外殻が定義されています。中身は {{ children }} で流し込まれる構造です。バッジのコードはこの children の中に含まれています。

ここまで3ファイル辿りました。では {{ children }} に何が入るのか――これを知るには、セクションファイルの別の場所を確認する必要があります。

Step 4:product-list.liquid に戻る → presetsで子ブロック構成が定義されている

{{ children }} に何が入るのかは、最初のセクションファイル sections/product-list.liquid に戻ると確認できます。schemaの presets に、商品カードの子ブロック構成が定義されています。

presetsの全体構造をまず把握しましょう。このセクションには3つのプリセットが定義されており、テーマエディタのセクション追加画面で選択できるレイアウトのバリエーションに対応しています。

// product-list.liquid schema の presets 全体像

"presets": [
  { "name": "products_grid",      ... },   ← グリッド表示
  { "name": "products_carousel",  ... },   ← カルーセル表示
  { "name": "products_editorial", ... }    ← エディトリアル表示
]

それぞれのプリセットの内部を見ると、settings(レイアウト設定)と blocks(ブロック構成)が定義されています。products_grid を例に見てみると、ブロックとして static-header(セクションヘッダー)と static-product-card(商品カード)の2つが定義されています。

// products_grid プリセットの構造

{
  "name": "products_grid",
  "settings": {
    "layout_type": "grid",
    "columns": 4,
    "max_products": 8,
    ...
  },
  "blocks": {

    // ① セクションヘッダー(タイトル+View Allボタン)
    "static-header": {
      "type": "_product-list-content",
      "blocks": {
        "product_list_text":   { "type": "_product-list-text" },    ← コレクションタイトル
        "product_list_button": { "type": "_product-list-button" }   ← 「View all」ボタン
      }
    },

    // ② 商品カード(各商品に対して繰り返し適用)
    "static-product-card": {
      "type": "_product-card",
      "blocks": {
        "product-card-gallery": { "type": "_product-card-gallery" }, ← 商品画像+バッジ
        "product_title":        { "type": "product-title" },        ← 商品タイトル
        "price":                { "type": "price" }                 ← 価格
      },
      "block_order": ["product-card-gallery", "product_title", "price"]
    }
  }
}

この構造を画面上のレイアウトと対応させると、以下のようになります。

プリセット構造と画面レイアウトの対応

これで {{ children }} の正体がわかりました。_product-card-gallery(商品画像+バッジ)、product-title(商品タイトル)、price(価格)の3つのブロックがこの順番で表示されています。バッジの変更先は _product-card-gallery だと判断できます。

3種類のプリセットはブロック構成そのものは同じで、各ブロックの settings の値(カラム数、間隔、ナビゲーションスタイルなど)がグリッド・カルーセル・エディトリアルごとに異なっています。そのため、今回のバッジ変更はどのプリセットで追加されたセクションにも共通で反映されます。

presetsは複数定義できる

ちなみに product-list.liquidpresets には3つのパターンが定義されています。products_grid(グリッド表示)、products_carousel(カルーセル表示)、products_editorial(エディトリアル表示)です。1つのセクションファイルに複数のpresetsを定義できるのがShopifyテーマの特徴で、テーマエディタのセクション追加画面で「特集コレクション:グリッド」「特集コレクション:カルーセル」「特集コレクション:エディトリアル」のように、同じセクションを異なる初期設定で追加できる仕組みです。レイアウトは違っても、中の商品カードブロック構成は共通なので、今回のバッジ変更はすべてのパターンに反映されます。

Step 5:_product-card-gallery.liquid → バッジのコードを発見

presetsで特定した blocks/_product-card-gallery.liquid を開くと、バッジのコードが見つかります。

{%- if product.available == false or product.compare_at_price > product.price -%}
  <div class="product-badges__badge product-badges__badge--rectangle ...">
    {%- if product.available == false -%}
      {{ 'content.product_badge_sold_out' | t }}
    {%- elsif product.compare_at_price > product.price -%}
      {{ 'content.product_badge_sale' | t }}   ← ここが「SALE」の文字列
    {%- endif -%}
  </div>
{%- endif -%}

5ファイル目で、変更すべき箇所に到達しました。Step 1のセクションに一度戻ってpresetsを確認するという流れが、Horizonならではの特徴的なポイントです。

実際の変更内容

ファイル追跡の結果、変更するのは blocks/_product-card-gallery.liquid になります。

ここで重要なのは、元のコードを直接書き換えるのではなく、schemaに設定項目を追加してテーマエディタから切り替えられるようにすることです。こうすることで、元の「セール」テキスト表示がデフォルト値として維持され、コードを触らずに元に戻すこともできます。テーマアップデート時のマージでも、元コードの削除より設定の追加の方が競合しにくくなります。

変更1:schemaに設定項目を追加

_product-card-gallery.liquid のschema(settings配列)に、バッジ表示形式の選択肢を追加します。

// schema の settings 配列に追加
{
  "type": "select",
  "id": "sale_badge_style",
  "label": "セールバッジの表示形式",
  "options": [
    { "value": "text",     "label": "テキスト(セール)" },
    { "value": "percent",  "label": "割引率(20% OFF)" },
    { "value": "amount",   "label": "割引額(¥1,000 OFF)" }
  ],
  "default": "text"
}

default"text" にすることで、何もしなければ元の動作(「セール」テキスト)がそのまま維持されます。

変更2:Liquidテンプレート部分を設定値で分岐

同じ _product-card-gallery.liquid のLiquidテンプレート部分(schema より上のコード)で、変更1で追加した sale_badge_style の値に応じてバッジのテキスト表示を切り替えます。

変更前:

{%- elsif product.compare_at_price > product.price -%}
  {{ 'content.product_badge_sale' | t }}

変更後:

{%- elsif product.compare_at_price > product.price -%}
  {%- case block.settings.sale_badge_style -%}
    {%- when 'percent' -%}
      {%- assign discount = product.compare_at_price
          | minus: product.price
          | times: 100.0
          | divided_by: product.compare_at_price
          | round -%}
      {{ discount }}% OFF
    {%- when 'amount' -%}
      {%- assign save_amount = product.compare_at_price
          | minus: product.price -%}
      {{ save_amount | money }} OFF
    {%- else -%}
      {{ 'content.product_badge_sale' | t }}
  {%- endcase -%}

else に元の翻訳キー表示を残しているので、デフォルト("text")のままなら動作は一切変わりません。テーマエディタの「商品画像」ブロック設定パネルから、「テキスト(セール)」「割引率(20% OFF)」「割引額(¥1,000 OFF)」を選択するだけで切り替えられます。

テーマエディタでのセールバッジ表示形式の設定切り替え画面

なぜ設定項目にするのか

元のコードを直接書き換えてしまうと、元に戻すにはコードを再度編集する必要があります。schema設定として追加すれば、テーマエディタのドロップダウンから誰でも切り替えられ、デフォルト値で元の動作が保たれます。テーマアップデート時のマージでも、コードの削除や書き換えより、設定の追加の方が競合が起きにくいというメリットもあります。

変更ファイルの全体像

ファイル 種別 変更内容
blocks/_product-card-gallery.liquid ベンダーファイル変更 SALEバッジのテキストを割引率に変更

加えて、これらの変更を行うために読み込んで理解する必要があったファイルは以下の通りです。

ファイル 理解が必要な理由
sections/product-list.liquid 起点。商品カードブロックへの委任を確認
blocks/_product-card.liquid 子ブロックの集約とスニペットへの受け渡しを確認
snippets/product-card.liquid Web ComponentとDOM構造を把握
snippets/card-gallery.liquid 画像とバッジの位置関係、スライドショー構造を把握

変更するファイルは1つだけですが、そこにたどり着くまでに合計5ファイルを読み解くことになります。Dawnの「セクション → スニペット」の1ステップと比べると、ファイル追跡の手順に大きな違いがあることがわかります。

「ベンダーファイルを触らない」方針と、触らざるを得ない現実

以前の記事で紹介した「安全なカスタマイズガイド」では、custom.* プレフィックス付きのファイルだけで完結させ、ベンダーファイルを変更しないことを推奨しています。CSS(assets/custom.css)やJS(assets/custom.js)、新規セクション(sections/custom.*.liquid)を追加する分にはこの方針に沿えます。

しかし今回のように既存ブロックの動作を変更するカスタマイズでは、この方針に沿うことが現実的に成り立ちません。

custom方式を試みるとどうなるか

方針に従うなら、_product-card-gallery.liquid を直接編集する代わりに、custom._product-card-gallery.liquid を新設してベンダーファイルを温存するはずです。しかし、このカスタムブロックを実際に使えるようにするには、参照元のファイルを全て書き換える必要があります

具体的に数えてみましょう。

Step 1:親ブロックのschemaに新typeを追加(2ファイル)

_product-card-gallery を子ブロックとして許可しているのは、プライベートブロック _product-card.liquid とテーマブロック product-card.liquid の2ファイルです。それぞれのschemaのblocks配列に custom._product-card-gallery を追加する必要があります。→ ベンダーファイル2つを変更

Step 2:presetsのデフォルト構成を書き換え(3ファイル)

各セクションのschema内presetsで "type": "_product-card-gallery" が定義されている箇所を全て新typeに書き換える必要があります。product-list.liquid(3プリセット分)、product-recommendations.liquid(セクション・ブロック)。→ ベンダーファイル3つを変更

Step 3:テンプレートJSONの実行時構成を書き換え(6ファイル)

テーマエディタで配置済みの構成が保存されているテンプレートJSON(index.json, collection.json, product.json, search.json, cart.json, 404.json)も全て書き換えが必要です。→ ベンダーファイル6つを変更

方式 変更するベンダーファイル数
_product-card-gallery.liquid を直接編集 1ファイル
custom._product-card-gallery.liquid を新設 11ファイル(親schema 2 + presets 3 + テンプレートJSON 6)

1ファイルのベンダー変更を避けるために、11ファイルのベンダー変更が必要になります。

custom方式の変更連鎖図

これはHorizonのブロックアーキテクチャに起因する構造的な特性です。ブロックファイルはschemaのblocks配列で「許可するタイプ」として参照され、presetsとテンプレートJSONで「実際に使うタイプ」として指定されています。ブロックのタイプ名を変えるということは、その参照元を全て書き換えることを意味します。

さらにcustom方式を徹底すると

もし、参照元のファイルに変更をかけること自体もcustom方式で対応するとすれば、変更はさらに拡大します。Step 1で _product-card.liquidcustom._product-card.liquid に置き換えるなら、今度はその親である product-list.liquid のschemaやpresetsも書き換えが必要になります。そこもcustom化するなら、テンプレートJSONも書き換え...という形で、変更が再帰的に上位レイヤーへ波及していきます。最終的には、テーマ全体をフォークするのとほぼ変わらない状態に近づいてしまいます。

custom.*方式が有効な範囲と、そうでない範囲

カスタマイズの種類 custom.*方式 備考
CSS/JSの追加 有効 custom.css / custom.js で完結
新しいセクション/スニペットの追加 有効 sections/custom.*.liquid で新規追加
既存ブロックの動作変更 適用が現実的でない 参照元の連鎖により変更が11ファイルに拡大

今回のケースでは、_product-card-gallery.liquid を直接編集する方式を選択しています。その代わりに、元のコードを削除するのではなくschemaに設定項目を追加して条件分岐させることで、デフォルト値で元の動作が維持される形にしています。これにより、テーマアップデート時のマージでも競合が起きにくくなります。

Git管理の活用

ベンダーファイルを変更するカスタマイズを行う場合、GitHub(またはBitbucket等)によるバージョン管理を活用するのがおすすめです。upstream/horizon ブランチでオリジナルテーマを追跡し、main ブランチでカスタマイズを管理する。こうしておけば、テーマアップデート時に git merge upstream/horizon で差分を確認しながらマージできます。具体的な方法は「Horizonテーマの安全なカスタマイズガイド」で詳しく紹介しています。

DawnとHorizon、カスタマイズのアプローチの違い

今回の体験を通じて見えてくるのは、DawnとHorizonではカスタマイズの「進め方」そのものが異なるということです。

Horizonのブロックアーキテクチャは、テーマエディタ(ノーコード)での柔軟性を最大化するための設計です。ブロック単位で独立しているからこそ、テーマエディタ上で直感的に並び替えたり、表示/非表示を切り替えたりできます。

しかしながら、コードを直接編集するカスタマイズでは、ファイルの追跡にDawnより多くの手順が必要になります。また、テーマエディタの設定項目を追加したり変更を行う場合はベンダーファイルの編集を伴うため、テーマアップデート時のマージ運用も併せて考えておくことが大切です。

Horizonでカスタマイズを行う際は、以下のことを事前に意識しておくと良いでしょう。

  1. まずテーマエディタで実現できないか確認する。Horizonのブロックシステムは予想以上に柔軟なので、エディタの設定だけで要望を満たせるケースも多いです
  2. CSSだけで対応できるなら custom.css に書く。ベンダーファイルへの変更は最小限に
  3. ベンダーファイルを触る場合は必ずGit管理する。変更箇所のドキュメント化も忘れずに
  4. ファイル連鎖を事前に把握するHorizonセクション構成一覧ツールを使うと、セクション→ブロック→スニペットの依存関係を視覚的に確認できます

関連記事

ブログに戻る