見出し画像

「Shopify Cart API 」カート追加可能な商品紹介セクションを作成する

お世話になっております。ShopifyエンジニアのRyoと申します。
今回はShopifyのCart APIを使用し、以下の画像のようなカート追加機能のある商品紹介セクションを実装いたします!
使用テーマ:Dawn

ベースはDawnの「テキスト付き画像」を使用

本記事を書いた理由

Shopifyサイト内でLPを作成する際、「カート追加ボタンを単体で実装し、LPからカート追加できるようにしたい」という依頼をたまに受けるということと、日本語でのCart APIに関する記事がまだまだ少ないと感じたので今回アウトプットを兼ねて記事を書いてみることにしました。

仕様

  • ブロックとして「カート追加ボタン」を作成

  • ボタンテキストは管理画面から変更可能

  • 商品を管理画面から選択し、選択した商品をカートに追加する事ができるボタンを作成

  • 単一バリエーションを想定

  • 在庫がない場合はボタンをdisableにし、Sold out表示

実装してみよう!

1,product-introduce-add-cart.liquidを作成し、image-with-text.liquidのコードを貼り付け

まずはコード編集画面からproduct-introduce-add-cart.liquidを作成し、「image-with-text.liquid」のコードを貼り付けます。
※ファイル名は任意でOK

{{ 'component-image-with-text.css' | asset_url | stylesheet_tag }}

{%- style -%}
  .section-{{ section.id }}-padding {
    padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
    padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
  }

  @media screen and (min-width: 750px) {
    .section-{{ section.id }}-padding {
      padding-top: {{ section.settings.padding_top }}px;
      padding-bottom: {{ section.settings.padding_bottom }}px;
    }
  }
{%- endstyle -%}

{%- liquid
  assign fetch_priority = 'auto'
  if section.index == 1
    assign fetch_priority = 'high'
  endif
  if section.settings.color_scheme == section.settings.section_color_scheme and section.settings.content_layout == 'no-overlap'
    assign remove_color_classes = true
  endif
-%}

<div class="section-{{ section.id }}-padding gradient color-{{ section.settings.section_color_scheme }}">
  <div class="page-width">
    <div class="image-with-text image-with-text--{{ section.settings.content_layout }} isolate{% if settings.text_boxes_border_thickness > 0 and settings.text_boxes_border_opacity > 0 and settings.media_border_thickness > 0 and settings.media_border_opacity > 0 %} collapse-borders{% endif %}{% unless section.settings.color_scheme == section.settings.section_color_scheme and settings.media_border_thickness > 0 and settings.text_boxes_shadow_opacity == 0 and settings.text_boxes_border_thickness == 0 or settings.text_boxes_border_opacity == 0 %} collapse-corners{% endunless %}{% if settings.animations_reveal_on_scroll %} scroll-trigger animate--slide-in{% endif %}">
      <div class="image-with-text__grid grid grid--gapless grid--1-col grid--{% if section.settings.desktop_image_width == 'medium' %}2-col-tablet{% else %}3-col-tablet{% endif %}{% if section.settings.layout == 'text_first' %} image-with-text__grid--reverse{% endif %}">
        <div class="image-with-text__media-item image-with-text__media-item--{{ section.settings.desktop_image_width }} image-with-text__media-item--{{ section.settings.desktop_content_position }} grid__item">
          <div
            class="image-with-text__media image-with-text__media--{{ section.settings.height }} global-media-settings{% unless remove_color_classes %} gradient color-{{ section.settings.color_scheme }}{% else %} background-transparent{% endunless %}{% if section.settings.image != blank %} media{% else %} image-with-text__media--placeholder placeholder{% endif %}{% if section.settings.image_behavior != 'none' %} animate--{{ section.settings.image_behavior }}{% endif %}"
            {% if section.settings.height == 'adapt' and section.settings.image != blank %}
              style="padding-bottom: {{ 1 | divided_by: section.settings.image.aspect_ratio | times: 100 }}%;"
            {% endif %}
          >
            {%- if section.settings.image != blank -%}
              {%- if section.settings.image_behavior == 'ambient' or section.settings.image_behavior == 'zoom-in' -%}
                {%- assign widths = '198, 432, 642, 900, 1284, 1800' -%}
                {%- capture sizes -%}
              (min-width: {{ settings.page_width }}px) {{ settings.page_width | minus: 100 | divided_by: 1.6667 }}px,
              (min-width: 750px) calc((100vw - 130px) / 1.667), calc((100vw - 50px) / 1.667)
            {%- endcapture -%}
              {%- else -%}
                {%- assign widths = '165, 360, 535, 750, 1070, 1500' -%}
                {%- capture sizes -%}
              (min-width: {{ settings.page_width }}px) {{ settings.page_width | minus: 100 | divided_by: 2 }}px,
              (min-width: 750px) calc((100vw - 130px) / 2), calc((100vw - 50px) / 2)
            {%- endcapture -%}
              {%- endif -%}
              {{
                section.settings.image
                | image_url: width: 1500
                | image_tag: sizes: sizes, widths: widths, fetchpriority: fetch_priority
              }}
            {%- else -%}
              {{ 'detailed-apparel-1' | placeholder_svg_tag: 'placeholder-svg' }}
            {%- endif -%}
          </div>
        </div>
        <div class="image-with-text__text-item grid__item">
          <div
            id="ImageWithText--{{ section.id }}"
            class="image-with-text__content image-with-text__content--{{ section.settings.desktop_content_position }} image-with-text__content--desktop-{{ section.settings.desktop_content_alignment }} image-with-text__content--mobile-{{ section.settings.mobile_content_alignment }} image-with-text__content--{{ section.settings.height }} content-container{% unless remove_color_classes %} gradient color-{{ section.settings.color_scheme }}{% else %} background-transparent{% endunless %}"
          >
            {%- for block in section.blocks -%}
              {% case block.type %}
                {%- when 'heading' -%}
                  <h2
                    class="image-with-text__heading inline-richtext {{ block.settings.heading_size }}"
                    {{ block.shopify_attributes }}
                  >
                    {{ block.settings.heading }}
                  </h2>
                {%- when 'caption' -%}
                  <p
                    class="image-with-text__text image-with-text__text--caption {{ block.settings.text_style }} {{ block.settings.text_style }}--{{ block.settings.text_size }} {{ block.settings.text_style }}"
                    {{ block.shopify_attributes }}
                  >
                    {{ block.settings.caption | escape }}
                  </p>
                {%- when 'text' -%}
                  <div class="image-with-text__text rte {{ block.settings.text_style }}" {{ block.shopify_attributes }}>
                    {{ block.settings.text }}
                  </div>
                {%- when 'button' -%}
                  {%- if block.settings.button_label != blank -%}
                    <a
                      {% if block.settings.button_link == blank %}
                        role="link" aria-disabled="true"
                      {% else %}
                        href="{{ block.settings.button_link }}"
                      {% endif %}
                      class="button{% if block.settings.button_style_secondary %} button--secondary{% else %} button--primary{% endif %}"
                      {{ block.shopify_attributes }}
                    >
                      {{ block.settings.button_label | escape }}
                    </a>
                  {%- endif -%}
              {%- endcase -%}
            {%- endfor -%}
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

{% schema %}
{
  "name": "t:sections.image-with-text.name",
  "class": "section",
  "disabled_on": {
    "groups": ["header", "footer"]
  },
  "settings": [
    {
      "type": "image_picker",
      "id": "image",
      "label": "t:sections.image-with-text.settings.image.label"
    },
    {
      "type": "select",
      "id": "height",
      "options": [
        {
          "value": "adapt",
          "label": "t:sections.image-with-text.settings.height.options__1.label"
        },
        {
          "value": "small",
          "label": "t:sections.image-with-text.settings.height.options__2.label"
        },
        {
          "value": "medium",
          "label": "t:sections.image-with-text.settings.height.options__3.label"
        },
        {
          "value": "large",
          "label": "t:sections.image-with-text.settings.height.options__4.label"
        }
      ],
      "default": "adapt",
      "label": "t:sections.image-with-text.settings.height.label"
    },
    {
      "type": "select",
      "id": "desktop_image_width",
      "options": [
        {
          "value": "small",
          "label": "t:sections.image-with-text.settings.desktop_image_width.options__1.label"
        },
        {
          "value": "medium",
          "label": "t:sections.image-with-text.settings.desktop_image_width.options__2.label"
        },
        {
          "value": "large",
          "label": "t:sections.image-with-text.settings.desktop_image_width.options__3.label"
        }
      ],
      "default": "medium",
      "label": "t:sections.image-with-text.settings.desktop_image_width.label",
      "info": "t:sections.image-with-text.settings.desktop_image_width.info"
    },
    {
      "type": "select",
      "id": "layout",
      "options": [
        {
          "value": "image_first",
          "label": "t:sections.image-with-text.settings.layout.options__1.label"
        },
        {
          "value": "text_first",
          "label": "t:sections.image-with-text.settings.layout.options__2.label"
        }
      ],
      "default": "image_first",
      "label": "t:sections.image-with-text.settings.layout.label",
      "info": "t:sections.image-with-text.settings.layout.info"
    },
    {
      "type": "select",
      "id": "desktop_content_position",
      "options": [
        {
          "value": "top",
          "label": "t:sections.image-with-text.settings.desktop_content_position.options__1.label"
        },
        {
          "value": "middle",
          "label": "t:sections.image-with-text.settings.desktop_content_position.options__2.label"
        },
        {
          "value": "bottom",
          "label": "t:sections.image-with-text.settings.desktop_content_position.options__3.label"
        }
      ],
      "default": "top",
      "label": "t:sections.image-with-text.settings.desktop_content_position.label"
    },
    {
      "type": "select",
      "id": "desktop_content_alignment",
      "options": [
        {
          "value": "left",
          "label": "t:sections.image-with-text.settings.desktop_content_alignment.options__1.label"
        },
        {
          "value": "center",
          "label": "t:sections.image-with-text.settings.desktop_content_alignment.options__2.label"
        },
        {
          "value": "right",
          "label": "t:sections.image-with-text.settings.desktop_content_alignment.options__3.label"
        }
      ],
      "default": "left",
      "label": "t:sections.image-with-text.settings.desktop_content_alignment.label"
    },
    {
      "type": "select",
      "id": "content_layout",
      "options": [
        {
          "value": "no-overlap",
          "label": "t:sections.image-with-text.settings.content_layout.options__1.label"
        },
        {
          "value": "overlap",
          "label": "t:sections.image-with-text.settings.content_layout.options__2.label"
        }
      ],
      "default": "no-overlap",
      "label": "t:sections.image-with-text.settings.content_layout.label"
    },
    {
      "type": "color_scheme",
      "id": "section_color_scheme",
      "label": "t:sections.all.colors.label",
      "default": "background-1"
    },
    {
      "type": "color_scheme",
      "id": "color_scheme",
      "label": "t:sections.multirow.settings.container_color_scheme.label",
      "default": "background-1"
    },
    {
      "type": "header",
      "content": "t:sections.all.animation.content"
    },
    {
      "type": "select",
      "id": "image_behavior",
      "options": [
        {
          "value": "none",
          "label": "t:sections.all.animation.image_behavior.options__1.label"
        },
        {
          "value": "ambient",
          "label": "t:sections.all.animation.image_behavior.options__2.label"
        },
        {
          "value": "zoom-in",
          "label": "t:sections.all.animation.image_behavior.options__4.label"
        }
      ],
      "default": "none",
      "label": "t:sections.all.animation.image_behavior.label"
    },
    {
      "type": "header",
      "content": "Mobile layout"
    },
    {
      "type": "select",
      "id": "mobile_content_alignment",
      "options": [
        {
          "value": "left",
          "label": "t:sections.image-with-text.settings.mobile_content_alignment.options__1.label"
        },
        {
          "value": "center",
          "label": "t:sections.image-with-text.settings.mobile_content_alignment.options__2.label"
        },
        {
          "value": "right",
          "label": "t:sections.image-with-text.settings.mobile_content_alignment.options__3.label"
        }
      ],
      "default": "left",
      "label": "t:sections.image-with-text.settings.mobile_content_alignment.label"
    },
    {
      "type": "header",
      "content": "t:sections.all.padding.section_padding_heading"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "label": "t:sections.all.padding.padding_top",
      "default": 36
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "label": "t:sections.all.padding.padding_bottom",
      "default": 36
    }
  ],
  "blocks": [
    {
      "type": "heading",
      "name": "t:sections.image-with-text.blocks.heading.name",
      "limit": 1,
      "settings": [
        {
          "type": "inline_richtext",
          "id": "heading",
          "default": "Image with text",
          "label": "t:sections.image-with-text.blocks.heading.settings.heading.label"
        },
        {
          "type": "select",
          "id": "heading_size",
          "options": [
            {
              "value": "h2",
              "label": "t:sections.all.heading_size.options__1.label"
            },
            {
              "value": "h1",
              "label": "t:sections.all.heading_size.options__2.label"
            },
            {
              "value": "h0",
              "label": "t:sections.all.heading_size.options__3.label"
            }
          ],
          "default": "h1",
          "label": "t:sections.all.heading_size.label"
        }
      ]
    },
    {
      "type": "caption",
      "name": "t:sections.image-with-text.blocks.caption.name",
      "limit": 1,
      "settings": [
        {
          "type": "text",
          "id": "caption",
          "default": "Add a tagline",
          "label": "t:sections.image-with-text.blocks.caption.settings.text.label"
        },
        {
          "type": "select",
          "id": "text_style",
          "options": [
            {
              "value": "subtitle",
              "label": "t:sections.image-with-text.blocks.caption.settings.text_style.options__1.label"
            },
            {
              "value": "caption-with-letter-spacing",
              "label": "t:sections.image-with-text.blocks.caption.settings.text_style.options__2.label"
            }
          ],
          "default": "caption-with-letter-spacing",
          "label": "t:sections.image-with-text.blocks.caption.settings.text_style.label"
        },
        {
          "type": "select",
          "id": "text_size",
          "options": [
            {
              "value": "small",
              "label": "t:sections.image-with-text.blocks.caption.settings.caption_size.options__1.label"
            },
            {
              "value": "medium",
              "label": "t:sections.image-with-text.blocks.caption.settings.caption_size.options__2.label"
            },
            {
              "value": "large",
              "label": "t:sections.image-with-text.blocks.caption.settings.caption_size.options__3.label"
            }
          ],
          "default": "medium",
          "label": "t:sections.image-with-text.blocks.caption.settings.caption_size.label"
        }
      ]
    },
    {
      "type": "text",
      "name": "t:sections.image-with-text.blocks.text.name",
      "limit": 1,
      "settings": [
        {
          "type": "richtext",
          "id": "text",
          "default": "<p>Pair text with an image to focus on your chosen product, collection, or blog post. Add details on availability, style, or even provide a review.</p>",
          "label": "t:sections.image-with-text.blocks.text.settings.text.label"
        },
        {
          "type": "select",
          "id": "text_style",
          "options": [
            {
              "value": "body",
              "label": "t:sections.image-with-text.blocks.text.settings.text_style.options__1.label"
            },
            {
              "value": "subtitle",
              "label": "t:sections.image-with-text.blocks.text.settings.text_style.options__2.label"
            }
          ],
          "default": "body",
          "label": "t:sections.image-with-text.blocks.text.settings.text_style.label"
        }
      ]
    },
    {
      "type": "button",
      "name": "t:sections.image-with-text.blocks.button.name",
      "limit": 1,
      "settings": [
        {
          "type": "text",
          "id": "button_label",
          "default": "Button label",
          "label": "t:sections.image-with-text.blocks.button.settings.button_label.label",
          "info": "t:sections.image-with-text.blocks.button.settings.button_label.info"
        },
        {
          "type": "url",
          "id": "button_link",
          "label": "t:sections.image-with-text.blocks.button.settings.button_link.label"
        },
        {
          "type": "checkbox",
          "id": "button_style_secondary",
          "default": false,
          "label": "t:sections.image-with-text.blocks.button.settings.outline_button.label"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "t:sections.image-with-text.presets.name",
      "blocks": [
        {
          "type": "heading"
        },
        {
          "type": "text"
        },
        {
          "type": "button"
        }
      ]
    }
  ]
}
{% endschema %}

2,カート追加ボタンブロックの作成

カート追加ボタンブロックの作成をしていきます。
コマンド+Fを押し、「"type": "button"」を検索し、こちらのブロックの下に以下のコードを書きます。

    {
      "type": "cart_add_button",
      "name": "カート追加ボタン",
      "limit": 1,
      "settings": [
        {
          "type": "text",
          "id": "cart_add_label",
          "default": "カートに追加",
          "label": "ボタンテキスト",
          "info": "ボタンのテキストを入力してください"
        },
    {
      "type": "product",
      "id": "featured_product",
      "label": "カートに追加したい商品",
      "info":"バリエーションが複数ある場合エラーになります。バリエーションがない商品を選択してください"
    }

次に、先程と同じようにコマンド+Fを押し、「{%- endcase -%}」を検索し、その上に以下のコードを貼り付けます。

{%- when 'cart_add_button' -%}
{% if block.settings.featured_product %}
{% assign product = all_products[block.settings.featured_product] %}
{% if product.available %}
<button  class="button" id="add-to-cart-btn" data-variant-id="{{ product.variants.first.id }}">{{ block.settings.cart_add_label }}</button>
{% else %}
<button class="button" id="add-to-cart-btn" data-variant-id="{{ product.variants.first.id }}" disabled>Sold Out</button>
{% endif %}
{% endif %}

これでデザインとしては完成なので、次はカートに商品を追加できるようにCart APIを用いてJavascriptを書いていきます

3,Javascript・Cart APIを使用してカート追加機能を実装

アセットファイルにc-add-cart.jsを作成し、product-introduce-add-cart.liquidに<script src="{{ 'c-add-cart.js' | asset_url }}" defer></script>を書きます
※ファイル名は任意でOK


c-add-cart.jsに以下のコードを貼り付けます。

// ドキュメントが読み込まれた後に実行するイベントリスナーを設定します。
document.addEventListener('DOMContentLoaded', function() {
  // 'add-to-cart-btn' IDを持つ要素(ボタン)を取得し、変数に割り当てます。
  const addToCartButton = document.getElementById('add-to-cart-btn');
  
  // ボタンにクリックイベントリスナーを追加します。
  addToCartButton.addEventListener('click', function() {
    // ボタンの'data-variant-id'属性からバリアントIDを取得します。
    var variantId = this.getAttribute('data-variant-id');

    // '/cart/add.js'エンドポイントにPOSTリクエストを送信して商品をカートに追加します。
    fetch('/cart/add.js', {
      method: 'POST',  // POSTメソッドを使用
      headers: {
        'Content-Type': 'application/json'  // ヘッダーにコンテンツタイプを設定
      },
      body: JSON.stringify({
        id: variantId,  // 商品のバリアントID
        quantity: 1     // 数量
      })
    })
    // レスポンスをJSON形式で受け取ります。
    .then(response => response.json())
    // レスポンスが正常に受け取られた後、ページを再読み込みします。
    .then(data => {
      location.reload();
    })
    // エラーが発生した場合の処理を記述します。
    .catch(error => {
      console.error('Error adding item to cart:', error);  // コンソールにエラーメッセージを出力
      alert('カート追加の際にエラーが起きました');  // ユーザーにアラートを表示
    });
  })
})

動作確認

コードが書けたので実際に動作確認してみます

カート追加ボタンをクリック!


ちゃんと追加されています。成功です!


在庫を0にするときちんとSold out表示になっています

完成!!

お疲れ様でした。
これで完了です。
余談ですが、c-add-cart.jsのコメントはChatGPTに書いてもらっています。
なんとなく書いているコードに「詳しくコメントを書いて」と入れるとかなり詳しく書いてくれるのでコードに関する理解が深まります。超便利ですね。

最後に

本記事を最後まで見てくださりありがとうございました…!
普段からフリーランスのShopifyエンジニアとしてお仕事をしているので、Shopify関連のお仕事のご相談や、同じShopifyエンジニアの方・Web関連の勉強・お仕事をされている方は是非XのDM、もしくはChatworkまでお気軽にご連絡ください!

この記事が気に入ったらサポートをしてみませんか?