Horizon全39セクションを解析して整理した呼び出しパターン3分類

Horizon全39セクションを解析して整理した呼び出しパターン3分類

Horizonで何が変わったのか

Shopifyの Summer '25 Edition で登場した新テーマ「Horizon」ですが、見た目だけでなく、テーマの内部構造——つまりアーキテクチャそのものが、従来のテーマとはかなり違っています。

その中心にあるのがテーマブロック(Theme Blocks)です。従来のテーマ(DawnやRiseなど)では、ブロックはセクションの中にローカル定義されていて、そのセクションの外には持ち出せませんでした。レイアウトもフラットな1階層構造で、入れ子にはできません。

テーマブロックではこのあたりが大きく変わっています。ブロックは/blocksフォルダに独立したLiquidファイルとして置かれ、セクションをまたいで使い回せます。さらに、ブロックの中にブロックを入れるネスト構造が最大8階層まで可能(セクションレベルを除く)になっていて、テーマエディタ上でかなり自由にレイアウトを組めるようになりました(公式ドキュメント: Theme blocks)。

セクションブロックとテーマブロックの違い

  • セクションブロック(従来): セクション内にローカル定義。そのセクションの中でしか使えず、ネスト不可
  • テーマブロック(Horizon): /blocksフォルダに独立ファイルとして配置。セクション横断で再利用可能、最大8階層ネスト対応(セクションレベル除く)

Shopifyのエディションカンファレンスでは、この設計思想を「anything anywhere」——あらゆるコンポーネントをどこにでも配置できる——と表現していました。実際にHorizonのテーマエディタを触ってみると、従来テーマとは自由度がまったく違うことがわかります。まだこのアーキテクチャを採用しているテーマはごく一部ですが、Shopifyがこの方向に力を入れていることは伝わってきます。

ただ、この自由度の裏側には、コード構造の複雑さがあります。Horizonテーマにはv3.3.1時点で39のセクション94のブロックが存在し、ブロックの呼び出し方も複数のパターンが混在しています。カスタマイズしようとしたとき、「このブロックはどうやって呼ばれているのか」がまず気になるところでした。

本記事では、Horizonの全セクションコードと公式ドキュメントをもとに、ブロックの呼び出しパターンを自分なりに整理してみました。同じようにHorizonのコードを追っている方の参考になればうれしいです。(また、もしここが違う、という点などありましたらご指摘いただけるとありがたいです)

本記事の分類について

本記事で使用している「動的ブロック」「静的ブロック」「セクションブロック(従来方式)」の3分類は、Shopify公式ドキュメントの用語ではなく、筆者がHorizonの全セクションコードを読み解く中で整理した個人的な分類です。公式ドキュメントでは theme blocks / section blocks / app blocks という分け方をしており、呼び出しパターンによる分類は行っていません。本記事では理解しやすさを優先して独自の分類を採用していますので、あらかじめご了承ください。

ブロック呼び出しの3つの分類

ブロックの呼び出し方は大きく 3つの分類 に分かれます。

分類 Liquid構文 特徴
A. 動的ブロック {% content_for 'blocks' %}(複数形) マーチャントが自由に追加・削除・並べ替え可能
B. 静的ブロック {% content_for "block", type: "...", id: "..." %}(単数形) テーマ開発者がLiquidで配置を固定。削除・並べ替え不可
C. セクションブロック(従来方式) {% for block in section.blocks %} セクション内でローカル定義。テーマブロックとの併用不可

以降、それぞれのパターンを詳しく見ていきます。

前提知識: schemaとは何か

本題に入る前に、この記事で頻出する「schema」について簡単に確認しておきます。

Shopifyのテーマでは、各セクションファイル(/sections/○○.liquid)の末尾に {% schema %} というタグでJSON形式の定義を書きます。これが「schema」です。セクションの名前、受け入れるブロックの種類、テーマエディタに表示される設定項目など、そのセクションの振る舞いを決める設計図のような役割を持っています。

// sections/example-section.liquid の末尾

{%- schema -%}
{
  "name": "セクション名",
  "blocks": [            ← どのブロックを受け入れるか
    { "type": "@theme" }
  ],
  "settings": [ ... ]    ← テーマエディタに表示される設定項目
}
{%- endschema -%}

Horizonのテーマブロックでも同じ仕組みです。ブロックファイル(/blocks/○○.liquid)にもschemaを書くことができ、そのブロックが子ブロックを受け入れるかどうかもここで定義します。つまり、セクションもブロックも「schemaでブロックの受け入れルールを宣言する」という点では共通です。

以降は、このschemaの blocks 配列の書き方によって呼び出しパターンが変わる、という流れで見ていきます。

A: 動的ブロック — content_for 'blocks'

マーチャントがテーマエディタから自由にブロックを追加・削除・並べ替えできる方式です。schemaの blocks 配列で、受け入れるブロックの種類を制御します。

パターンA-1: すべてのテーマブロックを受け入れる

@theme を指定すると、/blocks/ フォルダ内のすべての公開テーマブロック(アンダースコアプレフィックスなし)がブロックピッカーに表示されます。

{%- content_for 'blocks' -%}

{%- schema -%}
{
  "name": "Section",
  "blocks": [{"type": "@theme"}, {"type": "@app"}]
}
{%- endschema -%}

テーマエディタのブロックピッカーには、すべての公開テーマブロックがカテゴリ別に表示されます。

@themeを指定したセクションのブロックピッカー。カスタム、コレクション、フォーム、フッターなどカテゴリ別にすべての公開テーマブロックが表示されている

Horizonでの実例:

  • _blocks.liquid@theme, @app, _divider
  • section.liquid, main-page.liquid, main-404.liquid, password.liquid — 同上

ポイント: @app を併記するとアプリブロックも受け入れ可能になります。また、_divider のようなプライベートブロックを @theme と併記することで、推奨ブロック(Recommended blocks)としてピッカー上部に表示できます。この仕組みについては後述の「パターンA-4: @theme + 推奨ブロック併記」で詳しく解説しています。

パターンA-2: 特定のテーマブロックのみ受け入れる(公開ブロック指定)

schemaで特定のブロックtypeを明示指定すると、そのブロックのみがピッカーに表示されます。ただし、そのブロック自体は /blocks/ に通常のファイル名で存在するため、@theme を持つ他のセクションのピッカーにも表示されます。

{%- content_for 'blocks' -%}

{%- schema -%}
{
  "name": "Slideshow",
  "blocks": [{"type": "slide"}]
}
{%- endschema -%}

補足: Horizonテーマでは、この「公開ブロック名指定」パターンは単独では使われておらず、大半がプライベートブロック(A-3)で実装されています。

パターンA-3: プライベートブロックのみ受け入れる

ブロックファイル名にアンダースコア(_)プレフィックスを付けるとプライベートブロックになります。プライベートブロックは @theme を持つセクションのブロックピッカーには表示されず、明示的に指定したセクションでのみ使用可能です。

{%- content_for 'blocks' -%}

{%- schema -%}
{
  "name": "Slideshow",
  "blocks": [{"type": "_slide"}]
}
{%- endschema -%}

スライドショーセクションのブロックピッカーでは、プライベートブロックの「スライド」のみが選択肢として表示されます。

スライドショーセクションのブロックピッカー。プライベートブロックの「スライド」のみが表示されている

Horizonでの実例:

  • slideshow.liquid_slide のみ受け入れ
  • layered-slideshow.liquid_layered-slide のみ受け入れ
  • header-announcements.liquid_announcement のみ受け入れ
  • marquee.liquidtext, icon, logo, _divider(公開+プライベート混在)

ポイント: スライドショーやアナウンスメントバーなど、特定の構造を強制したいセクションに最適です。マーチャントはブロックの追加・削除・並べ替えは可能ですが、ピッカーに表示される選択肢が限定されます。

パターンA-4: @theme + 推奨ブロック併記

@theme で全テーマブロックを受け入れつつ、特定のブロックを併記するとピッカー上部に「推奨」として表示されます。他のブロックは "Show all" から選択可能です。

{%- content_for 'blocks' -%}

{%- schema -%}
{
  "name": "Hero",
  "blocks": [
    {"type": "@theme"},
    {"type": "text"},
    {"type": "button"},
    {"type": "_marquee"}
  ]
}
{%- endschema -%}

ヒーローセクションのブロックピッカーでは、推奨ブロック(グループ、スペーサー、テキスト、ボタン等)がカテゴリ別に上部に表示され、下部の「すべて表示」から全テーマブロックにアクセスできます。

ヒーローセクションのブロックピッカー。推奨ブロックがカテゴリ別に上部に表示され、下部に「すべて表示」リンクがある

Horizonでの実例:

  • hero.liquid@theme, text, button, logo
  • footer.liquid_divider, @app, button, menu, text
  • collection-list.liquid@theme, @app, text, icon, image

パターンA-5: コンテキスト付き動的ブロック

content_for 'blocks' にリソースを渡し、子ブロックが closest でアクセスできるようにするパターンです。

{%- content_for 'blocks', closest.product: product -%}

Horizonでの実例:

  • blocks/_product-card.liquid — 親から受け取った closest.product をネストした動的ブロックに引き継ぎ
  • blocks/_collection-card.liquid — 親から受け取った closest.collection をネストした動的ブロックに引き継ぎ

ポイント: Horizonではこのパターンはセクションではなくブロックファイル内で使われています。ブロック自身がさらにネストした動的ブロックを展開する際に、親から受け取ったリソースを子ブロックへ渡す用途です。

対応リソースタイプ:

リソース 構文
Product closest.product: <ProductDrop>
Collection closest.collection: <CollectionDrop>
Article closest.article: <ArticleDrop>
Blog closest.blog: <BlogDrop>
Page closest.page: <PageDrop>
Metaobject closest.metaobject.<definition_type>: <MetaobjectDrop>

B: 静的ブロック — content_for "block"(単数形)

テーマ開発者がLiquid内で配置場所を固定する方式です。マーチャントは設定の変更と非表示は可能ですが、削除・並べ替え・複製はできません

基本構文

{%- content_for "block", type: "<type>", id: "<id>" -%}
パラメータ 説明
type /blocks/ フォルダ内のテーマブロックのtype名
id(必須) 親セクション/ブロック内で一意な文字列ID。開発者が設定する(Shopifyは自動生成しない)

動的ブロックとの比較

静的ブロック 動的ブロック
非表示/カスタマイズ 可能 可能
並べ替え(D&D) 不可 可能
削除/複製 不可 可能
条件付きレンダリング 可能 不可
forループ内レンダリング 可能 不可
max_blocks 制限 カウントされない カウントされる

パターンB-1: 基本的な静的ブロック

固定位置に特定のブロックを配置する最もシンプルなパターンです。

{%- content_for "block", type: "_cart-title", id: "cart-page-title" -%}
{%- content_for "block", type: "_cart-products", id: "cart-page-items" -%}
{%- content_for "block", type: "_cart-summary", id: "cart-page-summary" -%}

Horizonでの実例:

  • main-cart.liquid — カートページのタイトル、商品リスト、サマリーを固定配置
  • featured-blog-posts.liquid — タイトルブロックを固定配置
  • search-header.liquid — 見出しと検索入力を固定配置

パターンB-2: コンテキスト(closest)を渡す静的ブロック

静的ブロックにリソースデータを渡し、ブロック内で closest 経由でアクセス可能にするパターンです。

{%- content_for 'block',
  type: '_product-media-gallery',
  id: 'media-gallery',
  closest.product: closest.product
-%}

Horizonでの実例:

  • product-information.liquidclosest.product をメディアギャラリーと商品詳細に渡す
  • featured-product.liquidsection.settings.productclosest.product として渡す
  • collection-list.liquidclosest.collection をコレクションカードに渡す

パターンB-3: forループ内での静的ブロック

ループ内で静的ブロックを呼び出し、各反復ごとに異なるリソースを渡します。IDは同じですが、各ループ反復で別のコンテキストが適用されます。

{%- for product in collection.products -%}
  {%- content_for 'block',
    type: '_product-card',
    id: 'product-card',
    closest.product: product
  -%}
{%- endfor -%}

Horizonでの実例:

  • main-collection.liquid — 商品一覧のループで _product-card を繰り返し呼び出し
  • main-blog.liquid — 記事一覧のループで _blog-post-card を繰り返し呼び出し
  • product-recommendations.liquid — レコメンド商品のループ

注意: IDは直接の親に対して一意であればよく、ルートセクション全体で一意である必要はありません。theme-checkの警告が出る場合は {% # theme-check-disable UniqueStaticBlockId %} で抑制できます。

パターンB-4: 追加パラメータを渡す静的ブロック

closest 以外にも、任意のデータをパラメータとして渡すことができます。

{% comment %} variant パラメータで表示モードを切り替え {% endcomment %}
{%- content_for 'block', type: '_header-menu', id: 'header-menu', variant: 'desktop' -%}
{%- content_for 'block', type: '_header-menu', id: 'header-menu', variant: 'mobile' -%}

{% comment %} テキストを直接渡す {% endcomment %}
{%- content_for 'block', id: 'heading', type: '_heading', text: heading_text -%}

{% comment %} force_emptyで空状態の表示を制御 {% endcomment %}
{%- content_for 'block', id: 'cart-page-title', type: '_cart-title', force_empty: true -%}

Horizonで確認されたパラメータ一覧:

パラメータ 用途 使用セクション
closest.product 商品リソースの受け渡し product-information, main-collection 等
closest.collection コレクションリソースの受け渡し collection-list, collection-links 等
closest.article 記事リソースの受け渡し featured-blog-posts, main-blog
variant 表示バリエーションの指定 header(desktop/mobile/navigation_bar)
text テキストの直接受け渡し search-header
results / results_size フィルタ対象のリソースと件数 main-collection, search-results
index / current ループインデックスと現在選択状態 collection-links
force_empty 空状態のレンダリング強制 main-cart

パターンB-5: 条件付きレンダリング

静的ブロックをLiquidの条件分岐内で呼び出すと、テーマエディタのサイドバーで点線の目アイコンが表示されます。

{%- unless cart.empty? -%}
  {%- content_for 'block', id: 'cart-page-summary', type: '_cart-summary' -%}
{%- endunless -%}

Horizonでの実例: main-cart.liquid — カートが空でない場合のみサマリーを表示

以下はカートページの実際のテーマエディタ画面です。左のサイドバーで「サマリー」ブロックに歯車アイコンが付いており、条件付きで表示されていることがわかります。

カートページのテーマエディタ。サイドバーで「サマリー」ブロックに条件付きレンダリングのアイコンが表示されている

Bの重要な補足: presetsとの連携

静的ブロックはLiquidで typeid を指定して呼び出しますが、presetsで同じidを使用して static: true と設定値を記述すると、セクションが最初に追加された際にそのデフォルト設定が適用されます。

{% comment %} Liquid側: 静的ブロックの呼び出し {% endcomment %}
{%- content_for "block", type: "collapsible-row-summary", id: "collapsible-row" -%}

{%- schema -%}
{
  "name": "Collapsible row",
  "blocks": [{"type": "@theme"}, {"type": "@app"}],
  "presets": [
    {
      "name": "Collapsible row",
      "blocks": [
        {
          "type": "collapsible-row-summary",
          "static": true,
          "id": "collapsible-row",          ← Liquid側のidと一致させる
          "settings": {
            "summary": "Collapsible row"    ← デフォルト設定値
          }
        }
      ]
    }
  ]
}
{%- endschema -%}

presetsに含めない場合の挙動

静的ブロックはプリセットに明示されなくても、セクション/ブロック追加時にデフォルト設定値で自動追加されます。presetsに含めるのは「デフォルト設定を上書きしたい場合」のみ必要です。

JSONデータ内では static: true フラグが付与され、block_order 配列には含まれません(順序はLiquidコード内での出現順で決定)。

A + B 混在パターン

Horizonテーマでは、1つのセクション内で動的ブロックと静的ブロックを組み合わせて使うのが最も一般的なパターンです。

パターンAB-1: 静的ブロック(固定部分)+ 動的ブロック(自由部分)

{% comment %} 固定: タイトル {% endcomment %}
{%- content_for 'block', id: 'blog-post-title', type: 'text' -%}

{% comment %} 固定: アイキャッチ画像 {% endcomment %}
{%- content_for 'block', id: 'blog-post-featured-image', type: '_blog-post-featured-image' -%}

{% comment %} 固定: 本文 {% endcomment %}
{%- content_for 'block', id: 'blog-post-content', type: '_blog-post-content' -%}

{% comment %} 自由: マーチャントが追加するブロック {% endcomment %}
{%- content_for 'blocks' -%}

Horizonでの実例:

  • main-blog-post.liquid — ブログ記事の構造部分は静的、追加コンテンツは動的
  • product-information.liquid — メディアギャラリーと商品詳細は静的、追加ブロック(@app)は動的
  • main-cart.liquid — カート構造は静的、追加ブロックは動的

パターンAB-2: section.blocks イテレーション + 動的ブロック

section.blocks でブロック数を取得してUIを構築しつつ、実際のブロックレンダリングは content_for 'blocks' で行うパターンです。

{% comment %} section.blocksでタブボタンを生成 {% endcomment %}
<div role="tablist">
  {%- for block in section.blocks -%}
    <button role="tab" id="Tab-{{ block.id }}"></button>
  {%- endfor -%}
</div>

{% comment %} 実際のコンテンツは動的ブロックとして展開 {% endcomment %}
<div class="panels">
  {%- content_for 'blocks' -%}
</div>

Horizonでの実例:

  • layered-slideshow.liquid — タブボタンを生成し、パネル部分は動的ブロックで展開
  • slideshow.liquid — スライド数を取得してコントロールを生成

パターンAB-3: capture + content_for 'blocks'

動的ブロックの出力を一度captureし、加工してからレンダリングするパターンです。

{%- capture slides -%}
  {%- content_for 'blocks' -%}
{%- endcapture -%}

{%- render 'slideshow', slides: slides, slide_count: section.blocks.size -%}

Horizonでの実例:

  • slideshow.liquid — スライドをcaptureしてスニペットに渡す
  • product-information.liquid — メディアギャラリー、商品詳細をそれぞれcaptureして条件分岐で配置
  • main-cart.liquid — 追加ブロックをcaptureして、通常表示とemptyテンプレートの両方で使用

C: セクションブロック(従来の方法)

セクションのschema内で直接ブロックを定義し、section.blocks をループで処理する方式です。テーマブロックとの併用はできません

{%- for block in section.blocks -%}
  {%- case block.type -%}
    {%- when "heading" -%}
      <h1>{{ block.settings.heading }}</h1>
  {%- endcase -%}
{%- endfor -%}

{%- schema -%}
{
  "name": "Example section",
  "blocks": [
    {
      "type": "heading",
      "name": "Heading",
      "settings": [
        {
          "type": "text",
          "id": "heading",
          "label": "Heading",
          "default": "Hello, world!"
        }
      ]
    }
  ]
}
{%- endschema -%}

制限事項:

  • 定義されたセクション内でしか使えない(再利用不可)
  • 1階層のみ(ネスト不可)
  • テーマブロック(@theme / @app)との同一セクション内での併用不可

Horizonでの使用: Horizonテーマはテーマブロック(A/B)を全面的に採用しており、Cのセクションブロックは使用されていません。今後テーマを作る場合はA/Bが基本になっていきそうです。

パターン早見表(Horizonテーマ全セクション)

Horizonテーマの各セクションがどのブロック呼び出しパターンを使用しているかを一覧にまとめました。

セクション 動的ブロック 静的ブロック schema blocks
_blocks.liquid blocks - @theme, @app, _divider
carousel.liquid - group / _carousel-content (presetsで定義)
collection-links.liquid - _collection-link(ループ) (presetsで定義)
collection-list.liquid blocks _collection-card(ループ) @theme, @app, text, icon
featured-blog-posts.liquid - _featured-blog-posts-title, _featured-blog-posts-card(ループ) _featured-blog-posts-*
featured-product.liquid - _media-without-appearance, _featured-product (presetsで定義)
featured-product-information.liquid blocks _featured-product-information-carousel, _product-details @app
footer.liquid blocks - _divider, @app, 各種公開ブロック
footer-utilities.liquid blocks - footer-copyright
header.liquid - _header-logo, _header-menu(複数variant) (presetsなし)
header-announcements.liquid blocks - _announcement
hero.liquid blocks - @theme, text, button
layered-slideshow.liquid blocks - _layered-slide
main-404.liquid blocks - @theme, @app, _divider
main-blog-post.liquid blocks text, _blog-post-featured-image, _blog-post-content @theme, @app
main-blog.liquid blocks _blog-post-card(ループ) @theme, @app, text
main-cart.liquid blocks _cart-title, _cart-products, _cart-summary @theme, @app, text
main-collection.liquid - filters, _product-card(ループ) (presetsなし)
main-collection-list.liquid blocks _collection-card(ループ) @theme, @app, text
main-page.liquid blocks - @theme, @app, _divider
marquee.liquid blocks - text, icon, logo, _divider
media-with-content.liquid - _media-without-appearance, _content-without-appearance (presetsで定義)
password.liquid blocks - @theme, @app, _divider
product-hotspots.liquid blocks text(見出し) _hotspot-product
product-information.liquid blocks _product-media-gallery, _product-details @app
product-list.liquid blocks _product-list-content, _product-card(ループ) @theme, @app, _divider
product-recommendations.liquid blocks _product-card(ループ) @theme, @app, text
search-header.liquid - _heading, _search-input (presetsなし)
search-results.liquid - filters, _product-card(ループ) (presetsなし)
section.liquid blocks - @theme, @app, _divider
slideshow.liquid blocks(capture) - _slide

まとめ

  1. content_for 'blocks'(複数形)は動的ブロックの展開。schemaの blocks 配列で @theme / @app / 個別type を指定して受け入れ範囲を制御する。
  2. content_for "block"(単数形)は静的ブロック。typeid が必須。Liquidの任意の位置・条件分岐・ループ内で呼び出せる。
  3. 静的ブロックの presetsid を一致させ static: true を指定すると、デフォルト設定値を上書きできる。presetsに含めなくてもデフォルト値で自動追加される。
  4. 任意の 追加パラメータclosest.product, variant, text, force_empty 等)を渡すことで、同じブロックtypeでも異なる表示・動作を実現できる。
  5. Horizonテーマでは A+B混在パターン が最も多く、固定構造は静的ブロック、自由領域は動的ブロックで実現している。
ブログに戻る