【Dawn】顧客アカウントページに再注文ボタンを追加する方法

はじめに

Shopifyでは、2023年以降「新しいお客様アカウント」が登場し、Shopifyログイン画面+Shopアプリ向けにモダン化が進んでいます。
とはいえ、まだ多くのストアでは旧お客様アカウントが利用されており、
・再注文の導線を設置したい
・顧客体験を改善してリピート率を上げたい
といったニーズが依然として高いのが現状です。

そこでこの記事では、Dawnテーマの旧お客様アカウントを対象に、顧客アカウントページに再注文ボタンを追加するカスタマイズ方法をご紹介します。
再注文ボタンを設置することで過去の注文内容を簡単にカートに復元できるようになり、リピート購入の体験が向上します。
特に定番商品や日用品などのECサイトで効果的な機能です。

コーディングスキルが高くない方でもそのままコードを使用できるようご用意していますので、ぜひご覧ください!

実装方法

1. 顧客アカウントページの注文履歴テーブルに再注文ボタンを追加

アカウントページに再注文ボタンを表示するため、「main-account.liquid」の内容を下記に差し替えます。
※念のため既存ファイルのバックアップを取っておくことをお勧めします。

{{ 'customer.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;
    }
  }
  /*再注文ボタン*/
  td:has(.js-reorder-btn) {
    padding: 2rem 1rem !important;
    text-align: center !important;
  }
  .js-reorder-btn {
      margin: 0 !important;
  }
  @media screen and (max-width: 749px) {
  td:has(.js-reorder-btn) {
      padding: 2rem 0 3rem !important;
  }
  }
{%- endstyle -%}

<div class="customer account section-{{ section.id }}-padding">
  <div>
    <h1 class="customer__title">{{ 'customer.account.title' | t }}</h1>
    <a href="{{ routes.account_logout_url }}">
      <span class="svg-wrapper">
        {{- 'icon-account.svg' | inline_asset_content -}}
      </span>
      {{ 'customer.log_out' | t }}
    </a>
  </div>

  <div>
    <div>
      <h2>{{ 'customer.orders.title' | t }}</h2>

      {% paginate customer.orders by 20 %}
        {%- if customer.orders.size > 0 -%}
          <table role="table" class="order-history">
            <caption class="visually-hidden">
              {{ 'customer.orders.title' | t }}
            </caption>
            <thead role="rowgroup">
              <tr role="row">
                <th id="ColumnOrder" scope="col" role="columnheader">{{ 'customer.orders.order_number' | t }}</th>
                <th id="ColumnDate" scope="col" role="columnheader">{{ 'customer.orders.date' | t }}</th>
                <th id="ColumnPayment" scope="col" role="columnheader">{{ 'customer.orders.payment_status' | t }}</th>
                <th id="ColumnFulfillment" scope="col" role="columnheader">
                  {{ 'customer.orders.fulfillment_status' | t }}
                </th>
                <th id="ColumnTotal" scope="col" role="columnheader">{{ 'customer.orders.total' | t }}</th>
                <!-- 以下追加 -->
                <th></th>
                <!-- ここまで -->
              </tr>
            </thead>
            <tbody role="rowgroup">
              {%- for order in customer.orders -%}
                <tr role="row">
                  <td
                    id="RowOrder"
                    role="cell"
                    headers="ColumnOrder"
                    data-label="{{ 'customer.orders.order_number' | t }}"
                  >
                    <a
                      href="{{ order.customer_url }}"
                      aria-label="{{ 'customer.orders.order_number_link' | t: number: order.name }}"
                    >
                      {{ order.name }}
                    </a>
                  </td>
                  <td headers="RowOrder ColumnDate" role="cell" data-label="{{ 'customer.orders.date' | t }}">
                    {{ order.created_at | time_tag: format: 'date' }}
                  </td>
                  <td
                    headers="RowOrder ColumnPayment"
                    role="cell"
                    data-label="{{ 'customer.orders.payment_status' | t }}"
                  >
                    {{ order.financial_status_label }}
                  </td>
                  <td
                    headers="RowOrder ColumnFulfillment"
                    role="cell"
                    data-label="{{ 'customer.orders.fulfillment_status' | t }}"
                  >
                    {{ order.fulfillment_status_label }}
                  </td>
                  <td headers="RowOrder ColumnTotal" role="cell" data-label="{{ 'customer.orders.total' | t }}">
                    {{ order.total_net_amount | money_with_currency }}
                  </td>
                  <!-- 以下追加 -->
                  <td>
                    <button
                      class="button button--primary js-reorder-btn"
                      data-order-id="{{ order.id }}"
                      data-line-items='[
                        {% for line in order.line_items %}
                          {
                            "variant_id": {{ line.variant_id }},
                            "quantity": {{ line.quantity }},
                            "title": {{ line.title | json }},
                            "handle": {{ line.product.handle | json }}
                          }{% unless forloop.last %},{% endunless %}
                        {% endfor %}
                      ]'>
                      同じ商品を再注文する
                    </button>
                  </td>
                  <!-- ここまで -->
                </tr>
              {%- endfor -%}
            </tbody>
          </table>
        {%- else -%}
          <p>{{ 'customer.orders.none' | t }}</p>
        {%- endif -%}

        {%- if paginate.pages > 1 -%}
          {%- if paginate.parts.size > 0 -%}
            <nav class="pagination" role="navigation" aria-label="{{ 'general.pagination.label' | t }}">
              <ul role="list">
                {%- if paginate.previous -%}
                  <li>
                    <a href="{{ paginate.previous.url }}" aria-label="{{ 'general.pagination.previous' | t }}">
                      <span class="svg-wrapper">
                        {{- 'icon-caret.svg' | inline_asset_content -}}
                      </span>
                    </a>
                  </li>
                {%- endif -%}

                {%- for part in paginate.parts -%}
                  <li>
                    {%- if part.is_link -%}
                      <a href="{{ part.url }}" aria-label="{{ 'general.pagination.page' | t: number: part.title }}">
                        {{- part.title -}}
                      </a>
                    {%- else -%}
                      {%- if part.title == paginate.current_page -%}
                        <span aria-current="page" aria-label="{{ 'general.pagination.page' | t: number: part.title }}">
                          {{- part.title -}}
                        </span>
                      {%- else -%}
                        <span>{{ part.title }}</span>
                      {%- endif -%}
                    {%- endif -%}
                  </li>
                {%- endfor -%}

                {%- if paginate.next -%}
                  <li>
                    <a href="{{ paginate.next.url }}" aria-label="{{ 'general.pagination.next' | t }}">
                      <span class="svg-wrapper">
                        {{- 'icon-caret.svg' | inline_asset_content -}}
                      </span>
                    </a>
                  </li>
                {%- endif -%}
              </ul>
            </nav>
          {%- endif -%}
        {%- endif -%}
      {% endpaginate %}
    </div>

    <div>
      <h2>{{ 'customer.account.details' | t }}</h2>

      {{ customer.default_address | format_address }}

      <a href="{{ routes.account_addresses_url }}">
        {{ 'customer.account.view_addresses' | t }} ({{ customer.addresses_count }})
      </a>
    </div>
  </div>
</div>

<!-- 再注文用スクリプト読み込み -->
<script src="{{ 'reorder.js' | asset_url }}" defer="defer"></script>


{% schema %}
{
  "name": "t:sections.main-account.name",
  "settings": [
    {
      "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
    }
  ]
}
{% endschema %}

2. 再注文ボタン用スクリプトを作成

新規アセット「reorder.js」を作成し、再注文ボタン用の下記スクリプトを記述します。

document.addEventListener('click', async (e) => {
  const btn = e.target.closest('.js-reorder-btn');
  if (!btn) return;

  let items = [];
  try { items = JSON.parse(btn.dataset.lineItems || '[]'); }
  catch (_) {
    alert('再注文情報の取得に失敗しました。ページを更新してお試しください。');
    return;
  }

  let soldOutItems = [];
  let adjustedItems = [];
  let addedCount = 0;

  for (const item of items) {
    if (!item?.variant_id || !item?.handle) continue;

    const productRes = await fetch(`/products/${item.handle}.js`);
    if (!productRes.ok) { soldOutItems.push(item.title); continue; }

    const product = await productRes.json();
    const variant = product.variants.find(v => v.id == item.variant_id);
    if (!variant) { soldOutItems.push(item.title); continue; }

    const requestedQty = Number(item.quantity) || 1;
    const inventoryPolicy = variant.inventory_policy; // "continue" or "deny"
    const inventoryQty = typeof variant.inventory_quantity === 'number' ? variant.inventory_quantity : null;
    const available = !!variant.available;
    const inventoryManaged = !!variant.inventory_management; // shopify, null = 未管理

    // 在庫0 & 販売続けるOFF = 除外
    if (!available && inventoryPolicy !== "continue") {
      soldOutItems.push(item.title);
      continue;
    }

    // 在庫数を強制的に丸める(販売続けるOFF & 在庫管理Shopify)
    let finalQty = requestedQty;
    if (inventoryPolicy !== "continue" && inventoryManaged && typeof inventoryQty === "number") {
      if (requestedQty > inventoryQty) {
        finalQty = inventoryQty;
        adjustedItems.push(`${item.title}(${requestedQty} → ${finalQty})`);
      }
    }

    // 0 or NaNならスキップ
    if (!finalQty || finalQty <= 0) {
      soldOutItems.push(item.title);
      continue;
    }

    // 念押し:カートに送る数量も必ず min() にする
    finalQty = Math.min(finalQty, inventoryQty ?? finalQty);

    const addRes = await fetch('/cart/add.js', {
      method: 'POST',
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: Number(item.variant_id),
        quantity: finalQty
      })
    });

    if (!addRes.ok) {
      soldOutItems.push(item.title);
      continue;
    }

    addedCount++;
    await new Promise(r => setTimeout(r, 60));
  }

  // 完了後のフィードバック
  if (addedCount > 0) {
    let msg = "";
    if (adjustedItems.length) {
      msg += `在庫数に合わせ数量を調整しました:\n- ${adjustedItems.join('\n- ')}\n\n`;
    }
    if (soldOutItems.length) {
      msg += `追加できない商品がありました:\n- ${soldOutItems.join('\n- ')}`;
    }
    if (msg) alert(msg.trim());
    window.location.href = "/cart";
  } else {
    alert("現在追加できる商品がありませんでした。");
  }
});


これにより、注文履歴から再注文へ進んだ際に、自動で商品がカートへ追加されるようになります。
この再注文ボタンは以下の仕様で動作します。

商品の設定 ボタンの動作
在庫あり 過去注文数をカートに追加
※過去注文数>現在個数の場合、購入手続きの際に現在個数を上限として数量調整
在庫0 + 販売を続けるON 過去注文数をカートに追加
在庫0 + 販売を続けるOFF カート追加不可&アラート表示

まとめ

いかがでしたか?
顧客アカウントページは、リピート購入を増やす重要な接点です。
今回ご紹介した再注文ボタン追加&スクリプトによる自動カート追加で、ユーザーは過去の購入履歴からワンクリックで商品を再購入でき、購買体験の利便性が大きく向上します。

特に日用品・食品・美容・D2Cブランドのように、定期的なリピート購入が想定される商材では、UI上の摩擦削減=LTV向上に直結します。
また、顧客が自身でスムーズに再注文できる環境は、カスタマーサポートや運用負荷の削減にも繋がります。

「買いやすいEC体験」は売上の持続性の基盤となります。
今回のカスタマイズをきっかけに顧客アカウント導線の最適化を進め、より強いファンを育てるストアづくりを目指しましょう!

売上最大化に向けて戦略立案から構築・運用までワンストップでサポート

EC運用でお悩みの方はお任せください!

お問い合わせはこちら
一覧に戻る