今回はshopifyで合わせ買いを実装した方法を書いていきたいと思います。
最初はアプリを探していたんですが、欲しい機能を持っているアプリがなかったので、自分で制作することにしました。
Google Analyticsの拡張eコマースに「商品がカートに追加された回数」を送信する方法も記載しています。
今回実装する方法を紹介する上で、以下のことが条件です。

  1. jQueryのライブラリが読み込まれていること
  2. MetaFields Editor」のアプリ(無料)がインストールされていること

表示イメージと制作コード

まず最初に制作後の表示イメージと、該当箇所のコードをご覧ください。
表示イメージにはshopifyデフォルトテーマ「Debut」を使用しています。

  • 商品画面
    商品画面
  • 商品画面(関連商品選択後)
    商品画面(関連商品選択後)
  • カート画面
    カート画面

liquidファイル

コードの解説はこちら
コードをポップアップで見る

コードをクリップボードにコピー
<div id="mainProducts">
  <h2 class="mainProducts-title">{{ product.title }}</h2>
  <div class="mainProducts-image"><img src="{{ product.featured_image | img_url:'300x' }}"></div>
  {% if product.variants.size > 1 %}
    <select id="mainProducts-select">
      {% for variant in product.variants %}
        <option class="mainProducts-option" data-sku="{{ variant.sku }}" value="{{ variant.id }}" data-price="{{ variant.price }}" data-title="{{ product.title }}-{{ variant.title }}">{{ variant.title }}</option>
      {% endfor %}
    </select>
  {% else %}
    <div class="mainProducts-price">{{ product.price | money }}</div>
  {% endif %}
</div>
<hr>

{% if product.metafields['bundle'].products != nil %}
{% assign bundle_products = product.metafields['bundle'].products | split:',' %}
<h4>関連商品</h4>
<ul id="bundleProducts">
  {% for handle in bundle_products %}
  {% assign bundle = all_products[handle] %}
  {% if bundle.available %}
  {% for variant in bundle.variants %}
    <li class="bundle-item" data-type="{{ bundle.type }}" data-sku="{{ variant.sku }}" data-variant="{{ variant.id }}" data-price="{{ variant.price }}">{% if bundle.variants.size > 1 %}{{ bundle.title }}{% else %}{{ bundle.title }}<br> - {{ variant.title }}{% endif %} (+{{ variant.price | money }})</li>
  {% endfor %}
  {% endif %}
  {% endfor %}
</ul>
{% endif %}

<div id="totalPriceBox">総額 <span id="totalPrice">{{ product.variants[0].price | money }}</span></div>

{% form 'product', product, class:form_classes %}
  <input name="id[]" value="{{product.variants[0].id}}" id="mainProductsItem"  type="hidden" data-sku="{{product.variants[0].sku}}" data-type="{{ product.type }}" data-title="{% if bundle.variants.size > 1 %}{{ bundle.title }}{% else %}{{ bundle.title }}-{{ product.variants[0].title }}{% endif %}" data-price="{{product.variants[0].price}}">
  <div id="PurchaseBundleItems"></div>
  <button type="submit" name="add" id="AddToCart" class="btn product-form__cart-submit"><span id="AddToCartText">{{ 'products.product.add_to_cart' | t }}</span></button>
{% endform %}

jsファイル

コードの解説はこちら
コードをポップアップで見る

コードをクリップボードにコピー
var ecArray = new Array;

$(function(){
  bundleItemClick();
  mainProductsSelect();
  // 拡張eコマースを使用して、「商品がカートに追加された回数」を送信
  $('#AddToCart').on('click',function(){
    ecAddCartSend();
  });
});

// 合わせ買い商品をクリックしたときの関数
function bundleItemClick(){
  $('.bundle-item').on('click',function(){
    $(this).toggleClass('selected');
    var variant = $(this).attr('data-variant');
    if( $(this).hasClass('selected') ){
      var sku = $(this).attr('data-sku');
      var price = $(this).attr('data-price');
      var type = $(this).attr('data-type');
      $('#PurchaseBundleItems').append('<input name="id[]" type="hidden" value="'+variant+'" data-sku="'+sku+'" data-price="'+price+'" data-type="'+type+'">');
    }else{
      $('input[value="'+variant+'"]').remove();
    }
    totalPrice();
  });
}

// 商品のバリエーションを選択したときの関数
function mainProductsSelect(){
  $('#mainProducts-select').on('change',function(){
    var sku = $('.mainProducts-option:selected').attr('data-sku');
    var variant = $('.mainProducts-option:selected').val();
    var price = $('.mainProducts-option:selected').attr('data-price');
    var title = $('.mainProducts-option:selected').attr('data-title');
    $('#mainProductsItem').val(variant);
    $('#mainProductsItem').attr('data-sku',sku);
    $('#mainProductsItem').attr('data-price',price);
    $('#mainProductsItem').attr('data-title',title);
    totalPrice();
  });
}

// 総額を出力する関数
function totalPrice(){
  var total = 0;
  $('input[name="id[]"]').each(function(){
    total += Number($(this).attr('data-price'));
  });
  var totalPrice = (total/100).toString().replace(/(\d)(?=(\d{3})+$)/g , '$1,');
  $('#totalPrice').text('¥'+totalPrice);
  // ecArrayMakeを動かす
  ecArrayMake();
}

// ecArrayを作成する関数
function ecArrayMake(){
  ecArray = [];  
  $('input[name="id[]"]').each(function(){
    ecArray.push({
      sku: $(this).attr('data-sku'),
      title: $(this).attr('data-title'),
      price: Number($(this).attr('data-price'))/100,
      type: $(this).attr('data-type')
    });
  });
}

// google analytics 拡張eコーマス「商品がカートに追加された回数」を送信する関数
function ecAddCartSend(){
  ga("set", "&cu", "JPY");
  for( i in ecArray ){
    ga("ec:addProduct",{
      id: ecArray[i].sku ,
      name:ecArray[i].title ,
      category:ecArray[i].type ,
      quantity: 1,
      price:ecArray[i].price,
      brand: "brand",
      variant: null,
      currency: "JPY"
    });
  }   
  ga('ec:setAction', 'add');
}

scssファイル

コードをポップアップで見る

コードをクリップボードにコピー
#bundleProducts{
  display:flex;
  flex-wrap:wrap;
  .bundle-item{
    flex:0 0 23%;
    width:23%;
    margin:0 1% 15px;
    border: 1px solid #ddd;
    border-radius:3px;
    padding: 15px;
    cursor:pointer;
    &.selected{
      border-color: #000;
    }
  }
}
#totalPriceBox{
  margin:30px 0;
  font-weight:bold;
  font-size:24px;
}

管理画面での合わせ買いの設定方法

ここでは合わせ買い商品を管理画面で設定する方法を説明します。
最初に説明したとおりMetaFields Editorアプリがインストールされていることが前提で説明していきます。

「MetaFields Editor」をshopifyにインストールすると、商品管理画面の商品名の下に、以下のような「Edit Metafields」が追加されます。

MetaFields Editorインストール後の商品管理画面
MetaFields Editorインストール後の商品管理画面

「Edit Metafields」を押すと別ウィンドウでMetafieldsを設定できる画面が開きます。
この設定画面に遷移したら、ページ下部「Add New Metafield」で新しくmetafieldを追加します。

「Namespace」には「bundle
「Key」には「products
「Value type」は「string
にします。
「Value」には合わせ買いに設定したい商品のhandleを入れます。
複数の商品のhandleを入れる場合は、コンマ区切りでhandleを入れます。
ここで注意したいのは、コンマ前後に空白を入れないことです。

下記のスライダーに「商品のhandle確認箇所」「Add New Metafieldボタン」「Metafieldsの設定画面」のキャプチャを載せています。
商品のhandleは、商品管理画面の「ウェブサイトのSEOを編集する」を押すと確認できます。

  • 商品のhandle確認箇所
    商品のhandle確認箇所(商品管理画面)
  • Add New Metafieldボタン
    Add New Metafieldボタン(MetaFields Editorの管理画面)
  • Metafieldsの設定画面
    Metafieldsの設定画面(MetaFields Editorの管理画面)

これで商品の合わせ買いの設定は完了です。
商品にバリエーションがある場合や、合わせ買いに設定した商品にバリエーションがある場合でも全てが表示されるようにしています。


拡張eコマースについて

shopifyでGoogle Analytics、Google Analytics 拡張eコマースの設定ができている前提でこちらは説明していきます。

この合わせ買い設定で重要なことは、一度に複数の商品をカートに追加できることにあります。
しかしshopifyの設定に従い、設定したGoogle Analytics 拡張eコマースでは、1つの商品ページから「商品がカートに追加された回数」を1回しか送信できません。

これは何を意味しているかというと、合わせ買いのカスタマイズをして、一度に3つ商品をカートに追加しても「商品がカートに追加された回数」は1回になるのです。しかも商品名が「(not set)」になってしまい、分析が困難になってしまいます。

そこで、今回紹介しているjsでは、追加した商品分を「商品がカートに追加された回数」として送信する関数を記述しています。

しかしデフォルトで設定されている「(not set)」分も送信されているので、Google Analyticsで分析をするときはフィルタで「(not set)」を除外してください。

「商品がカートに追加された回数」以外の購入個数などは、決済画面からGoogle Analyticsに送信されている情報で、合わせ買いコードの影響は出ませんので、安心して合わせ買いコードをお使いください。



liquidファイルのコードの解説

ここからはコード解説です。liquidのタグ説明にもなってきますから、しっかりカスタマイズする人だけ読んでくれれば大丈夫です。

コード上部から必要そうなとこだけ説明していきますので、可能な人はこちらを別ウインドウで開いて読むと分かりやすいと思います。


<h2 class="mainProducts-title">{{ product.title }}</h2>
<div class="mainProducts-image"><img src="{{ product.featured_image | img_url:'300x' }}"></div>

まずは合わせ買いのところではなく、商品情報を出力している箇所です。

{{ product.title }}は商品名を、
{{ product.featured_image | img_url:'300x' }}は登録している商品の1枚目の画像パスを出力しています。
ここではwidthが300pxの画像を出力しています。


{% if product.variants.size > 1 %}

これは商品にバリエーションがあるかどうかを判定しています。
バリエーションがある場合は{% if product.variants.size > 1 %}が「true」を返します。


{% for variant in product.variants %}
<option class="mainProducts-option" data-sku="{{ variant.sku }}" value="{{ variant.id }}" data-price="{{ variant.price }}" data-title="{{ product.title }}-{{ variant.title }}">{{ variant.title }}</option>
{% endfor %}

商品のバリエーションを全てoptionタグで出力しています。

{{ product.variants }}は商品の全てのバリエーション情報が入っている配列です。
{% for variant in product.variants %}と書くことで、バリエーションを1つ1つ取り出しています。

ここで{{ variant.sku }}はバリエーションに設定したsku、{{ variant.price }}は金額です。

{{ variant.title }}はバリエーションのタイトルです。商品名ではないことに気をつけてください。

例えばバリエーションが「サイズ」しかない場合は、設定したサイズ(例えば S,M,L)がそれぞれバリエーションのタイトルです。

バリエーションが複数ある場合、例えばバリエーションが「サイズ」「カラー」の場合は、設定したサイズと設定したカラーの組み合わせ(例えば S / Black, M / Black, S / White, M / White ・・・)がそれぞれバリエーションのタイトルです。

{{ variant.id }}はバリエーションIDですが、この{{ variant.id }}によってshopifyは商品を判定していますので、非常に重要な値です。


{% else %}
  <div class="mainProducts-price">{{ product.price | money }}</div>
{% endif %}

商品にバリエーションがない場合は単純に商品金額を出力しています。


ここからが合わせ買い商品の出力関連のコードです。

{% if product.metafields['bundle'].products != nil %}

{% if product.metafields['bundle'].products != nil %}によって、商品のmetafieldsに「Namespace == bundle」かつ「Key == products」があるかどうかを判定しています。

metafieldを設定するように管理画面での合わせ買いの設定方法で説明したかと思います。ここで「Namespace」や「Key」の値を間違っていると正しく出力されません。


{% assign bundle_products = product.metafields['bundle'].products | split:',' %}

ここでは「bundle_products」に「product.metafields['bundle'].products」の値をコンマ区切りで、配列にして格納しています。
「| split:','」は指定した文字を区切りとして配列を生成するFilterです。

例えばproduct.metafields['bundle'].productsの中身が「pants,jacket」とすると、
bundle_products = ["pants","jacket"]です。
ここで、もしproduct.metafields['bundle'].productsの中身が「pants,jacket」だと、
bundle_products = ["pants"," jacket"]となってしまいますので、metafieldでの設定値に気をつけてください。


{% for handle in bundle_products %}

for文で、bundle_productsの中身を1つずつ取り出して、handleに格納して回しています。


{% assign bundle = all_products[handle] %}

「all_products[商品のhandle]」で特定の商品情報を取得できます。
{% assign bundle = all_products[handle] %}で、bundleにall_products[handle]を格納しています。

ここでbundleオブジェクトはproductオブジェクトと同じようなオブジェクトです。
bundle.titleで合わせ買い商品の商品名を出力できます。他の金額などの情報についても同様です。


{% if bundle.available %}

合わせ買い商品が利用できるかどうかを判別しています。
詳細に言うと、商品管理画面の販売チャネルの「Online Store」にチェックがついているかどうかを判定しています。


{% for variant in bundle.variants %}
<li class="bundle-item" data-type="{{ bundle.type }}" data-sku="{{ variant.sku }}" data-variant="{{ variant.id }}" data-price="{{ variant.price }}">{% if bundle.variants.size > 1 %}{{ bundle.title }}{% else %}{{ bundle.title }}<br> - {{ variant.title }}{% endif %} (+{{ variant.price | money }})</li>
{% endfor %}

合わせ買い商品をバリエーションごとに出力しています。さきほど説明した商品のバリエーションとほとんど説明がかぶりますが、違う箇所だけ説明します。

{{ bundle.type }}は合わせ買い商品のProduct typeを取得できます。商品管理画面の「Product type」の値です。

{% if bundle.variants.size > 1 %}がありますが、これは合わせ買い商品にバリエーションがない場合は、「variant.title」が「Default Title」になってしまうので、この場合は「variant.title」を出力しないようにしています。


<div id="totalPriceBox">総額 <span id="totalPrice">{{ product.variants[0].price | money }}</span></div>

このコードは、総額を表示する箇所です。総額を動的に変更するにはjavascriptでします。
ここでは初期値として商品バリエーション1つ目の金額を{{ product.variants[0].price | money }}で出力しています。

ます。


{% form 'product', product, class:form_classes %}
  <input name="id[]" value="{{product.variants[0].id}}" id="mainProductsItem" type="hidden" data-sku="{{product.variants[0].sku}}" data-type="{{ product.type }}" data-title="{% if bundle.variants.size > 1 %}{{ bundle.title }}{% else %}{{ bundle.title }}-{{ product.variants[0].title }}{% endif %}" data-price="{{product.variants[0].price}}">
  <div id="PurchaseBundleItems"></div>
  <button type="submit" name="add" id="AddToCart" class="btn product-form__cart-submit"><span id="AddToCartText">{{ 'products.product.add_to_cart' | t }}</span></button>
{% endform %}

ここは「カートに追加する」ボタンの表示箇所です。
{% form 'product', product, class:form_classes %}と{% endform %}(以下、formオブジェクト)は呪文のようなもので、特別説明することがありません。

このformオブジェクトの間に、inputタグやselectタグなどで「name="id"」、「value="商品のvariantID"」を設置すれば目的の商品をカートに追加できます。

しかし、「name="id"」のままだと、いくつinputタグなどを記載しても1つしかカートに追加されません。
そこで「name="id[]"」として、複数商品を一度にカート追加できるようにしています。

id名「PurchaseBundleItems」の空の要素がありますが、ここに選択した合わせ買い商品のinputタグを入れていきます。

初期値として、商品バリエーション1つ目をformオブジェクトの中に入れています。上記の2行目の箇所です。

{{ 'products.product.add_to_cart' | t }}はshopify管理画面の「オンラインストア」→「テーマ」→「現在のテーマ/アクション/言語を編集する」→「Products」→「Add to cart」で設定しているテキストを出力します。


以上で、liquidファイルのコードの解説終了です。


jsファイルのコードの解説

次にjavascript(jQuery)のコード解説です。
これも可能な人はこちらを別ウィンドウで開いて読むと分かりやすいと思います

ここでは関数ごとに説明しています。


関数 bundleItemClick

function bundleItemClick(){
  $('.bundle-item').on('click',function(){
    $(this).toggleClass('selected');
    var variant = $(this).attr('data-variant');
    if( $(this).hasClass('selected') ){
      var sku = $(this).attr('data-sku');
      var price = $(this).attr('data-price');
      var type = $(this).attr('data-type');
      $('#PurchaseBundleItems').append('<input name="id[]" type="hidden" value="'+variant+'" data-sku="'+sku+'" data-price="'+price+'" data-type="'+type+'">');
    }else{
      $('input[value="'+variant+'"]').remove();
    }
    totalPrice();
  });
}

これは合わせ買い商品をクリックしたときに発火する関数です。
簡単に言うとやっていることは以下のことです。

  1. クリックした要素へのクラス「selected」の付与または除去
  2. クリックした要素がクラス「selected」を持っている場合は、formオブジェクト内にクリックした合わせ買い商品の情報を入れます。(formオブジェクトに関してはこちら
  3. クリックした要素がクラス「selected」を持っていない場合は、formオブジェクト内にあるクリックした合わせ買い商品の情報を削除します。

関数の中で、「data-sku」「data-type」がありますが、これは拡張eコマースで送信する値として使用しています。
「data-price」は拡張eコマース、総額を計算するために使用しています。

カートに追加するだけでいいのなら、必須は「data-variant」のみです。この値をinputタグのvalue値として入れて、カートに追加するボタンを押せば、カートに追加されます。

この関数の最後に、商品総額を計算する関数 totalPriceを入れています。


関数 mainProductsSelect

function mainProductsSelect(){
  $('#mainProducts-select').on('change',function(){
    var sku = $('.mainProducts-option:selected').attr('data-sku');
    var variant = $('.mainProducts-option:selected').val();
    var price = $('.mainProducts-option:selected').attr('data-price');
    var title = $('.mainProducts-option:selected').attr('data-title');
    $('#mainProductsItem').val(variant);
    $('#mainProductsItem').attr('data-sku',sku);
    $('#mainProductsItem').attr('data-price',price);
    $('#mainProductsItem').attr('data-title',title);
    totalPrice();
  });
}

商品のバリエーションを選択したときに発火する関数です。

これは以下のようなことをやっています。

  1. 選択した商品のバリエーションをformオブジェクト内のinputタグのid「mainProductsItem」に上書きして入れる

こちらも関数の最後に、商品総額を計算する関数 totalPriceを入れています。


関数 totalPrice

function totalPrice(){
  var total = 0;
  $('input[name="id[]"]').each(function(){
    total += Number($(this).attr('data-price'));
  });
  var totalPrice = (total/100).toString().replace(/(\d)(?=(\d{3})+$)/g , '$1,');
  $('#totalPrice').text('¥'+totalPrice);
  // ecArrayMakeを動かす
  ecArrayMake();
}

これはid「totalPrice」内に選択した商品、合わせ買い商品の総額を出力する関数です。

inputタグの「data-price」から金額を取得して計算しています。


関数 ecArrayMake

function ecArrayMake(){
  ecArray = [];  
  $('input[name="id[]"]').each(function(){
    ecArray.push({
      sku: $(this).attr('data-sku'),
      title: $(this).attr('data-title'),
      price: Number($(this).attr('data-price'))/100,
      type: $(this).attr('data-type')
    });
  });
}

これは拡張eコマースへ「商品がカートに追加された回数」を送信するための配列「ecArray」を作成するための関数です。

inputタグの、name="id[]"から必要な情報を取得しています。


関数 ecAddCartSend

function ecAddCartSend(){
  ga("set", "&cu", "JPY");
  for( i in ecArray ){
    ga("ec:addProduct",{
      id: ecArray[i].sku ,
      name:ecArray[i].title ,
      category:ecArray[i].type ,
      quantity: 1,
      price:ecArray[i].price,
      brand: "brand",
      variant: null,
      currency: "JPY"
    });
  }   
  ga('ec:setAction', 'add');
}

Google analyticsへ「商品がカートに追加された回数」を送信する関数です。

id「AddToCart」をクリックした時に発火するように設定しています。カートへ追加するボタンのid名が違う場合は適宜こちらを変更してください。


shopifyをカスタマイズするときはぜひ、使ってください〜。