テクニック

Shopifyメタフィールドで商品ページに独自項目を追加する実装手順|定義からLiquid出力まで

この記事の対象: Shopifyテーマを自分で編集する制作者・フロントエンド担当
読了時間: 約12分

商品ページに「素材」「サイズ」「産地」を出したい。標準の商品情報にはその欄がなく、多くの現場では商品説明(description)のHTMLに直接書き込む方法が選ばれます。ところがこのやり方は、商品点数が増えてきたあたりから確実に破綻します。表記ゆれが出る、並び順が揃わない、レイアウトを変えたいときに全商品を開き直すことになる。

Shopifyのメタフィールドで商品ページに独自項目を持たせると、この3つが同時に解決します。管理画面には入力欄が並び、テーマ側は「値があれば出す」という1本のロジックで受ける。この記事では定義の作成からLiquidでの出力、空値の分岐、一括入力までを、そのままコピーして動くコードで通します。


前提と、この記事で作るもの

動作環境

項目 条件
テーマ Online Store 2.0 対応テーマ(Dawn 系推奨)
権限 ストアの管理者、または「設定の編集」「テーマの編集」権限を持つスタッフ
プラン メタフィールドの定義作成は標準機能。プラン別の可否は管理画面で確認
編集方法 管理画面のコードエディタ、または Shopify CLI

Online Store 2.0 かどうかは、管理画面の オンラインストア → テーマ で対象テーマの「カスタマイズ」を開き、左サイドバーにセクションのツリーが表示されるかで判断できます。ツリーが出てテンプレート単位でセクションを追加できれば 2.0 です。1.0 のテーマ(Debut など旧世代)でもメタフィールドの値は Liquid から読めますが、テーマエディタからブロックとして差し込む部分は動きません。

作業前にテーマの複製を取ってください。オンラインストア → テーマ → 対象テーマの「…」→ 複製。ライブテーマを直接編集して失敗した場合、この複製が唯一の戻り先になります。

完成形

管理画面の商品編集ページ下部に「素材」「サイズ」「産地」「お手入れ方法」の入力欄が並び、商品ページには入力済みの項目だけが表形式で出る。値が空の項目は行ごと消え、全項目が空なら見出しごと出ない。この状態をゴールにします。


メタフィールドの定義を作る

定義と値は別物

最初につまずくのがここです。メタフィールドには定義(definition)と値(value)の2段階があり、定義を作らずに値だけ入れることもできてしまいます。

定義を先に作る場合と、作らない場合の違い

定義を作ってから入力

  • 商品編集画面に入力欄が並ぶ
  • 型が決まるので不正な値を弾ける
  • Liquid で `product.metafields.custom.material` と書ける
  • 一括編集ツールの列に出る

定義なしで値だけ入れる

  • 入力欄が出ないのでAPI経由でしか入らない
  • 型が曖昧なまま保存される
  • 参照はできるが管理画面から見えない
  • 担当者が引き継げない

制作者が自分で入れて終わりならどちらでも動きますが、クライアントが商品を追加する運用なら定義は必須です。入力欄が出ないと、商品を足すたびに制作者が呼ばれます。

作成手順

設定 → カスタムデータ → 商品 → 定義を追加する から進みます。

メタフィールド定義の作成フロー
名前を決める管理画面に出るラベル。日本語でよい
名前空間とキーを決めるLiquidで参照する識別子。英小文字とアンダースコア
型を選ぶ単一行テキスト・整数・ファイルなど
検証を設定文字数上限や選択肢の制限

名前空間とキーは、名前を入力すると自動で custom.素材 のような候補が入りますが、必ず手で英字に直してください。日本語のままでも保存はできますが、Liquid の product.metafields.custom.素材 は解決に失敗することがあります。ここは事故が起きやすい箇所なので、最初に英字へ直す癖をつけたほうが安全です。

今回作る4つの定義は次のとおりです。

名前(表示名) 名前空間とキー 型 補足
素材 custom.material 単一行のテキスト 検証で最大文字数を設定
サイズ custom.size 単一行のテキスト 「幅◯cm × 高さ◯cm」形式
産地 custom.origin 単一行のテキスト 検証で選択肢を限定してもよい
お手入れ方法 custom.care 複数行のテキスト 改行を含むため複数行を選ぶ

型は後から変更できません。 変更したい場合は定義を削除して作り直すことになり、入力済みの値も消えます。迷ったら「単一行のテキスト」ではなく、その項目が将来どう使われるかで決めてください。判断の目安を挙げます。

  • 絞り込みに使う可能性がある → 「単一行のテキスト」+検証で選択肢を列挙。表記ゆれが物理的に発生しなくなる
  • 数値で並べ替える可能性がある → 「整数」または「小数」。テキスト型だと 10 が 9 より前に並ぶ
  • 単位を扱う → 「重量」「寸法」など専用型。単位変換を Shopify 側が持つ
  • 画像や PDF を出す → 「ファイル」型。テキストにURLを書かせない

「ストアフロントでの表示」を必ずオンにする

定義作成画面の下部に ストアフロントでの表示に関する設定があります。ここがオフだと Liquid から値を読めず、テーマ側で何を書いても空になります。定義を作ったのに出ない、という相談の大半はここです。新しく作った定義は既定でオンになっていることが多いものの、古い定義や他店からコピーしてきた定義はオフのままのことがあるので、必ず現物で確認してください。


Liquidで出力する

まずスニペットとして切り出す

商品ページのテンプレートに直接書くのではなく、スニペットに分けます。あとで別の場所(クイックビュー、比較ページなど)から同じ表を呼べるようになるためです。

新規ファイルを作ります。オンラインストア → テーマ → コードを編集 → snippets → 新しいスニペットを追加する、ファイル名は product-specs とします。

snippets/product-specs.liquid:

{%- comment -%}
  商品の独自項目を表で出す
  使い方: {% render 'product-specs', product: product %}
{%- endcomment -%}

{%- liquid
  assign m = product.metafields.custom
  assign has_spec = false
  if m.material != blank or m.size != blank or m.origin != blank or m.care != blank
    assign has_spec = true
  endif
-%}

{%- if has_spec -%}
  <div class="ts-specs">
    <h2 class="ts-specs__title">商品仕様</h2>
    <dl class="ts-specs__list">
      {%- if m.material != blank -%}
        <div class="ts-specs__row">
          <dt class="ts-specs__key">素材</dt>
          <dd class="ts-specs__val">{{ m.material | escape }}</dd>
        </div>
      {%- endif -%}

      {%- if m.size != blank -%}
        <div class="ts-specs__row">
          <dt class="ts-specs__key">サイズ</dt>
          <dd class="ts-specs__val">{{ m.size | escape }}</dd>
        </div>
      {%- endif -%}

      {%- if m.origin != blank -%}
        <div class="ts-specs__row">
          <dt class="ts-specs__key">産地</dt>
          <dd class="ts-specs__val">{{ m.origin | escape }}</dd>
        </div>
      {%- endif -%}

      {%- if m.care != blank -%}
        <div class="ts-specs__row">
          <dt class="ts-specs__key">お手入れ</dt>
          <dd class="ts-specs__val">{{ m.care | escape | newline_to_br }}</dd>
        </div>
      {%- endif -%}
    </dl>
  </div>
{%- endif -%}

読者が変更する箇所は4つです。

  1. custom の部分 — 別の名前空間を使ったなら差し替える
  2. material size origin care — 自分が作ったキー名に合わせる
  3. 素材 サイズ 産地 お手入れ — 表示ラベル。管理画面の表示名と揃える必要はない
  4. ts-specs で始まるクラス名 — テーマ既存のクラスと衝突しない接頭辞にする

ts- という接頭辞は、テーマ本体のアップデートで上書きされないための自衛です。Dawn 系テーマは product__ や price__ といったクラスを持っているので、独自に足すものには一貫した接頭辞をつけてください。

なぜ != blank で判定するのか

{% if m.material %} でも動きそうに見えますが、これは避けてください。Liquid ではメタフィールドオブジェクトそのものが真になるため、値が空文字でも条件が通ってしまい、空の行が出ます。!= blank は空文字・空白のみ・nil のすべてを空として扱うので、意図どおりに消えます。

escape を通しているのは、管理画面から入力された値をそのままHTMLとして解釈させないためです。テキスト型のメタフィールドに <script> を入れられる状況は限られていますが、値の出どころが自分でない以上、素通しにする理由はありません。逆に、意図的にHTMLを入れたい項目がある場合は型を「リッチテキスト」にして {{ m.detail | metafield_tag }} で出します。この場合 escape は付けません。

テンプレートに差し込む

sections/main-product.liquid を開き、商品説明を出している部分を探します。Dawn 系なら次のようなブロックがあります。

{%- when 'description' -%}
  {%- if product.description != blank -%}
    <div class="product__description rte quick-add-hidden">
      {{ product.description }}
    </div>
  {%- endif -%}

この直後に1行足します。

{%- when 'description' -%}
  {%- if product.description != blank -%}
    <div class="product__description rte quick-add-hidden">
      {{ product.description }}
    </div>
  {%- endif -%}
  {% render 'product-specs', product: product %}

これで商品説明の下に仕様表が出ます。ただしこの方法だと表示位置がコードで固定され、テーマエディタから動かせません。位置を運用側で変えたいなら、次に説明するブロック化を選んでください。

テーマエディタから動かせるブロックにする

sections/main-product.liquid の末尾近くにある {% schema %} の blocks 配列に、次のオブジェクトを追加します。既存のブロック定義({"type": "description", ...} など)と同じ階層に、カンマ区切りで入れます。

{
  "type": "ts_specs",
  "name": "商品仕様",
  "limit": 1,
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "見出し",
      "default": "商品仕様"
    }
  ]
}

limit: 1 は同じブロックを2つ置けないようにする指定です。仕様表が2つ並ぶ意味はないので入れておきます。

続いて、テンプレート側の {% case block.type %} に分岐を足します。先ほど {% render %} を直接書いた場合は、それを消してこちらに移してください。

{%- when 'ts_specs' -%}
  {% render 'product-specs', product: product, heading: block.settings.heading %}

スニペット側も見出しを受け取れるようにします。snippets/product-specs.liquid の見出し行を差し替えます。

<h2 class="ts-specs__title">{{ heading | default: '商品仕様' | escape }}</h2>

default フィルタを挟んでいるのは、ブロック経由でない呼び出し(先ほどの直接 {% render %})でも見出しが消えないようにするためです。

保存後、テーマエディタで商品ページを開き、左サイドバーの「製品情報」セクション内で「ブロックを追加」を押すと「商品仕様」が出ます。ドラッグで位置を変えられる状態になっていれば成功です。

Shopifyのセクションとブロックの組み立て方をもう少し丁寧に追いたい場合は、同じ {% schema %} の書き方でスライダーを組んだ手順が参考になります。


CSSを当てる

CSSを当てる

外部ライブラリは不要です。定義リスト(dl)に Grid を当てるだけで、ラベルと値が揃った表になります。

assets/ に新規ファイル ts-specs.css を作ります。

.ts-specs {
  margin-block: 2.4rem;
}

.ts-specs__title {
  font-size: 1.6rem;
  font-weight: 600;
  margin-block-end: 1.2rem;
  padding-block-end: 0.8rem;
  border-block-end: 1px solid rgba(0, 0, 0, 0.12);
}

.ts-specs__list {
  margin: 0;
  display: grid;
  gap: 0;
}

.ts-specs__row {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 1.6rem;
  padding-block: 1rem;
  border-block-end: 1px solid rgba(0, 0, 0, 0.08);
  font-size: 1.4rem;
  line-height: 1.8;
}

.ts-specs__key {
  margin: 0;
  font-weight: 600;
  color: rgba(0, 0, 0, 0.62);
}

.ts-specs__val {
  margin: 0;
}

@media screen and (max-width: 600px) {
  .ts-specs__row {
    grid-template-columns: 1fr;
    gap: 0.4rem;
  }
}

変更する箇所: 8rem(ラベル列の幅。日本語ラベルが長いなら広げる)、rgba(0, 0, 0, 0.12) と 0.08(罫線の濃さ)、600px(1カラムに落とすブレイクポイント)。色をテーマのカラースキームに合わせたい場合は、Dawn 系なら rgba(var(--color-foreground), 0.12) と書けます。

読み込みはスニペットの先頭に1行足します。

{{ 'ts-specs.css' | asset_url | stylesheet_tag }}

Shopify は同じアセットを複数回読み込んでも重複して出力しないため、スニペット内に置いて問題ありません。theme.liquid に書くと仕様表のない商品ページでも読み込まれるので、スニペット側に置くほうが無駄がありません。

px ではなく rem を使い、margin-block padding-block などの論理プロパティを使っているのは、Dawn 系テーマのルート設定(font-size: 62.5%)と揃えるためです。テーマによっては rem の基準が違うので、数値がおかしく見えたら実際の描画で調整してください。


値を一括で入れる

定義を作っても、値を入れる作業が残ります。数点なら手入力で終わりますが、数百点あるストアで手作業は現実的ではありません。点数と更新頻度で方法を選びます。

方法1: 一括編集(数十点まで)

商品一覧で対象にチェック → 一括編集。表の右上「列」から追加した定義を選ぶと、表計算ソフトのような画面で横断入力できます。列は表示のたびに選び直す必要があるので、頻繁にやるなら次の方法へ。

方法2: CSVインポート(点数が多いとき)

商品CSVのエクスポート・インポートでメタフィールドを扱えます。列名の形式は決まっていて、次のように書きます。

Metafield: custom.material [single_line_text_field]
Metafield: custom.size [single_line_text_field]
Metafield: custom.origin [single_line_text_field]
Metafield: custom.care [multi_line_text_field]

[ ] の中は型の識別子です。ここが定義の型と食い違うとその列は無視されます。確実なやり方は、先に1〜2商品だけ手で値を入れてからCSVをエクスポートすることです。正しい列名がヘッダーに出てくるので、それをコピーして使えば型の記述ミスがなくなります。

インポート時は「既存の商品を上書きする」にチェックを入れてください。外すと新規商品として重複登録されます。ここは戻すのが面倒な事故なので、最初は3行だけのCSVで試してから全件を流します。

日本語を含むCSVは文字コードでも事故ります。Shopify が読むのは UTF-8 で、Excel で開いて保存し直すと Shift_JIS に変わることがあります。編集後は必ず「UTF-8(BOM付き)」で保存し直すか、テキストエディタ側で文字コードを固定してください。インポート後に産地が文字化けしていたら、まずここを疑います。

方法3: 選択肢を持たせて入力ミスを封じる

産地のように値のバリエーションが決まっている項目は、定義の検証設定で「選択肢」を指定できます(単一行テキスト型の場合)。管理画面がプルダウンになり、「岐阜県」と「岐阜」が混在する事故が起きなくなります。

商品ごとに手で書かせない仕組みにする理由は、見た目の統一だけではありません。値が揃っていれば、あとから「産地で絞り込む一覧ページ」や「同じ素材の商品を出す」といった機能を、既存データのまま追加できます。逆に商品説明のHTMLに書き込んでいた場合、その時点で全商品を開き直す作業が発生します。入力の仕組みを決めるタイミングが、あとから何ができるかを決めています。


表示以外にも使う

検索結果とコレクションページに出す

メタフィールドは商品ページ専用ではありません。sections/main-collection-product-grid.liquid の商品カード内、あるいは snippets/card-product.liquid で同じ値を参照できます。

{%- if card_product.metafields.custom.origin != blank -%}
  <p class="ts-card-origin">{{ card_product.metafields.custom.origin | escape }}</p>
{%- endif -%}

変数名がテーマによって card_product だったり product だったりするので、そのファイル内で使われている変数名に合わせてください。ファイル冒頭のコメントか {% liquid %} ブロックに定義があります。

コレクションページや記事一覧の見せ方を変える手順は、こちらでカード表示と絞り込みを扱っています。

絞り込み条件として使う

Shopify の検索・絞り込み機能では、メタフィールドを絞り込み条件に追加できます。検索と絞り込みの設定画面でフィルターとして追加すると、コレクションページの絞り込みに「産地」を出せます。ただし対象になるのは型と設定の条件を満たしたメタフィールドだけで、選択肢を持たない自由入力のテキストは、表記ゆれがそのままフィルターの選択肢として並びます。「岐阜県」「岐阜」「ぎふ」が3つ別々に出る状態になるので、絞り込みに使う項目は最初から選択肢型で作るのが前提になります。方法3を先にやっておく理由はここにもあります。

構造化データに載せる

素材や産地を JSON-LD の Product に含めると、検索結果に出る情報の解像度が上がります。既存の構造化データ出力(Dawn 系なら sections/main-product.liquid の末尾に application/ld+json のブロックがあります)に追記します。

{%- if product.metafields.custom.material != blank -%}
  ,"material": {{ product.metafields.custom.material | json }}
{%- endif -%}

json フィルタを使うと引用符とエスケープを Shopify 側が処理するため、値に " が入っていても壊れません。escape ではなく json を使うことが要点です。追記位置は既存のプロパティの後ろ、閉じ波括弧の前です。カンマの位置を間違えると JSON 全体が無効になるので、保存後に Google のリッチリザルトテストで通るか確認してください。

Shopify の構造化データやURL構造まわりの制約は、次の記事で整理しています。

バリエーションごとに値を変える

商品全体ではなく、サイズ違いごとに異なる値を持たせたい場合は、設定 → カスタムデータ → バリエーションで定義を作ります。参照は variant.metafields.custom.xxx です。

ただしバリエーション切り替え時に表示を更新するには JavaScript が必要になり、実装量が一段上がります。Dawn 系ならバリエーション選択時に商品セクションを取得し直す仕組みがあるので、その戻り値から仕様表の要素だけを差し替える形が扱いやすいです。値がバリエーションごとに変わらないなら、商品メタフィールドで済ませてください。カスタマイズの範囲を判断する材料としては、こちらも参考になります。


うまくいかないとき

値を入れたのに何も出ない

上から順に確認します。

症状 確認する場所
全商品で出ない 定義の「ストアフロントでの表示」がオンか
キーが違う 設定 → カスタムデータ で名前空間とキーの綴りを確認
名前空間の間違い custom 以外にしていないか。自動生成で別名になることがある
描画位置が違う テーマエディタでブロックが追加されているか
キャッシュ プレビューURLに ?preview_theme_id= を付けて確認

キーの綴り確認には、テンプレートに一時的に次を貼ると早いです。

<pre style="white-space:pre-wrap;font-size:12px">{{ product.metafields.custom | json }}</pre>

その商品が持つ custom 名前空間の中身がまるごと出ます。確認が終わったら必ず削除してください。

空の行が消えない

{% if m.material %} になっていないか確認します。前述のとおり、メタフィールドオブジェクトは値が空でも真になります。!= blank に統一してください。

もう一つ多いのが、管理画面で値を消したつもりが半角スペースが1つ残っているケース。!= blank は空白のみの文字列も空として扱うので、これは正しく消えます。それでも出るなら全角スペースを疑ってください。全角スペースは blank 判定を通りません。除去するなら次のように書きます。

{%- assign material_clean = m.material | replace: ' ', '' | strip -%}
{%- if material_clean != blank -%}

改行が反映されない

複数行テキストに入れた改行はそのままではHTMLに出ません。newline_to_br フィルタを通します。順序が重要で、escape を先、newline_to_br を後にします。逆にすると <br> タグまでエスケープされて文字として表示されます。

{{ m.care | escape | newline_to_br }}

テーマエディタにブロックが出ない

{% schema %} 内の JSON に構文エラーがあると、ブロックが1つも出なくなるか、保存時にエラーが表示されます。最も多いのはカンマの過不足です。追加したオブジェクトの前後にカンマがあるか、配列の最後の要素の後ろに余計なカンマがないかを確認してください。JSON は末尾カンマを許しません。

定義を消したら値も消えた

定義の削除時に「メタフィールドの値も削除する」という選択肢が出ます。ここで値を残す選択をしても、定義がないと管理画面から編集できなくなります。型を変えたい場合は、キー名を変えた新しい定義を作り、CSVで値を移してから旧定義を消すのが安全な順序です。

テーマ更新で消えた

sections/main-product.liquid に加えた変更は、テーマ本体をアップデートすると上書きされます。スニペット(snippets/product-specs.liquid)とCSS(assets/ts-specs.css)は独自ファイルなので残りますが、それを呼ぶ1行と schema への追記は消えます。テーマ更新のたびに再適用が必要な箇所として、変更内容をテキストに残しておいてください。

開発ストアで動いたのに本番で出ない

定義はストアごとに持つデータなので、テーマファイルをコピーしても定義は付いてきません。移行先のストアで同じ名前空間・同じキー・同じ型の定義を作り直す必要があります。綴りが1文字違うだけで全商品が空になるので、移行時は開発ストア側で {{ product.metafields.custom | json }} を出力して、キー名を目で突き合わせてから作るのが確実です。


よくある質問

メタフィールドを使うのに追加費用はかかりますか

Shopify 標準の機能なので、定義の作成・値の入力・Liquid からの参照に追加のアプリ費用はかかりません。有料アプリが必要になるのは、一括入力の効率化や複雑な条件表示など、標準機能の外側を求める場合だけです。プラン別の利用条件は、契約中のプランの管理画面で確認してください。

商品説明に書くのとメタフィールドで、SEO上の違いはありますか

どちらもHTMLとして出力されるため、テキストとしてクロールされる点は同じです。違いが出るのは構造化データで、メタフィールドなら material などのプロパティに値を正確にマッピングできます。商品説明のHTMLからそれを機械的に抜き出すことはできません。

定義の名前空間は custom 以外にしてもいいですか

問題ありません。ただし Shopify が予約している名前空間は使えないため、保存時にエラーが出たら別名にしてください。アプリが作るメタフィールドと混ざらないよう、独自の名前空間(例: spec)を切るのも有効です。名前空間を変えたら Liquid の参照も product.metafields.spec.material のように合わせてください。

既存商品の説明文に書いた仕様を、メタフィールドへ移せますか

商品CSVをエクスポートして、説明文から該当箇所を切り出し、メタフィールド列に入れてインポートし直す形になります。説明文の書式が商品ごとにバラバラだと機械的な変換ができないため、点数が少なければ手作業のほうが早いこともあります。どちらにしても、移行前のCSVは必ず保管してください。

バリエーションごとに違う値を出すにはどうしますか

バリエーション用のメタフィールド定義を作り、variant.metafields で参照します。ただし選択を切り替えたときに表示を差し替えるには JavaScript の実装が必要です。値がバリエーションで変わらないなら、商品メタフィールドで組むほうが保守が軽くなります。

メタフィールドが多すぎると商品ページは重くなりますか

テキスト型の項目を数個から十数個出す程度なら、体感できる差は出ません。重くなるのは、参照型のメタフィールドで別の商品やメタオブジェクトを大量に引くとき、あるいは画像ファイル型を何枚も出すときです。表示の遅さを疑う前に、まず何を参照しているかを確認してください。


まとめ

この記事でやったことを、作業順に整理します。

  1. テーマを複製して戻り先を作る
  2. 設定 → カスタムデータ → 商品で定義を4つ作る(名前空間とキーは英字に直す/ストアフロントでの表示をオン)
  3. snippets/product-specs.liquid を新規作成し、!= blank で空値を分岐させる
  4. sections/main-product.liquid の schema にブロックを追加し、テーマエディタから位置を動かせるようにする
  5. assets/ts-specs.css を作り、Grid で表組みを当てる
  6. 値は一括編集かCSVで入れる。選択肢を設定して表記ゆれを封じる

要点は2つだけです。空値の分岐を != blank で書くことと、入力の型を最初に決めきること。この2つを外すと、あとから直すコストが実装コストを上回ります。

Shopify でどこまでカスタマイズするか、どこから別の手段を考えるかの判断は、次の記事にまとめています。