クーポン取得 サンプルページ
========== [カスタマイズ追記] ==========
■ システム概要
スペシャルページやブログ記事にショートコードを貼るだけで「クーポンを取得する」
ボタンを設置できる仕組み。ログイン中の顧客がボタンを押すと、顧客タグに
`have:{メタオブジェクトのハンドル}` が記録され、以降クーポン一覧ページに
「保有中のクーポン」として表示される。
顧客タグに記録するため、ブラウザのキャッシュを消しても、別の端末から
ログインしても保有状態が維持される。localStorage / Cookie では実現できない。
■ システム構成(3点セット)
1. 本ファイル: snippets/coupon-claim-button.liquid
- スペシャルページのセクション、または main-article.liquid 等の末尾で
{% render 'coupon-claim-button' %} で読み込む
- 本文中のショートコードを検出してボタンを描画し、クリック時にWorkerへPOSTする
2. Cloudflare Worker「coupon-claim-api-album」(ソース: coupon-claim-worker.js)
- 顧客タグ書き込み用API。署名検証後にAdmin APIの tagsAdd を実行
- 環境変数: SHOP_DOMAIN / CLIENT_ID / CLIENT_SECRET / CLAIM_SECRET / ALLOWED_ORIGIN
3. Shopify Dev Dashboardアプリ「entry_button_api」
- スコープ: read_customers, write_customers(エントリーボタンと共用。追加不要)
■ ショートコード仕様(ページ本文・記事本文のHTML編集モードに貼る)
data-coupon-code を持つ要素なら何でも取得ボタンになる。中身は一切書き換えないので、
テキストでも画像でも自由に組める。
<div class="js-coupon-claim" data-coupon-code="NEWYEAR20">
<img src="バナー画像.jpg" alt="クーポンを取得">
</div>
<div class="js-coupon-claim" data-coupon-code="NEWYEAR20">
<span class="btn btn--primary">クーポンを取得する</span>
</div>
単体で置く場合は引数指定でも呼べる(この場合は中身を持たない空要素になるので、
CSS で背景画像を当てるなどして使う):
{% render 'coupon-claim-button', code: 'NEWYEAR20' %}
■ 本文以外の場所に置く場合
署名の発行対象は「ページ本文・記事本文に data-coupon-code が含まれるコード」に限っている。
セクション設定のカスタムHTMLやテンプレート内に直接書いた場合、本文の走査に引っかからず
署名が出ないためクリックしても反応しない。その場合は対象コードを明示的に渡す。
{% render 'coupon-claim-button', codes: 'NEWYEAR20,WLCM20' %}
→ このとき、ページ内の
<div class="js-coupon-claim" data-coupon-code="NEWYEAR20">…</div>
が動作するようになる。
反応しないときはブラウザのコンソールを確認すること。原因が出力される。
■ スタイル調整・デバッグ
URL に ?coupon_debug=1 を付けるか、コンソールで
window.CouponClaimDebug = true
としてからクリックすると、
・トーストが自動で消えなくなる(スタイルを調整しやすい)
・クリック検知のログがコンソールに出る(反応しているかの切り分けに使う)
■ 状態クラス(.js-coupon-claim 自身に付く)
is-ready 取得できる
is-guest 未ログイン(クリックでログインページへ)
is-loading 通信中
is-claimed 取得済み
is-upcoming 受付開始前
is-closed 受付終了
is-error 失敗(クリックで再試行できる)
data-claim-state 属性にも同じ値(プレフィックス無し)が入る。
見た目はこれらを使って貼る側の CSS で作る。例:
.js-coupon-claim.is-claimed { opacity: .5; pointer-events: none; }
.js-coupon-claim.is-loading { opacity: .6; }
取得の成否は、クーポンコードのコピーと同じ吹き出し(ツールチップ)で通知する。
吹き出しは <body> 直下に <span class="coupon-claim__tip"> として生成され、
表示中は is-visible が付く。
要素の子にしていないのは、親に opacity や overflow: hidden が掛かっていても
影響を受けないようにするため。
■ 旧仕様からの変更(2026-09-02)
以前は .js-coupon-claim の中に <button> を生成していたため、
バナー画像をクリックさせるといった使い方ができなかった。
現在は要素の中身に一切触れず、クリックを拾って状態クラスを付けるだけにしている。
data-claim-label / data-claim-done / data-claim-color は廃止。
■ 後から差し込まれた要素への対応(2026-10-16 追記)
以前は DOMContentLoaded 時点に存在する要素だけを対象にしていたため、
・商品ページのバリアント切替(#price-{section.id} の innerHTML 差し替え)
・クイックビュー(DOMContentLoaded 後に読み込まれる)
で入ってきた .js-coupon-claim が反応しなかった。
現在は MutationObserver で追加要素を検知して都度バインドする(処理済みは data-coupon-claim-bound で除外)。
手動で再走査したい場合は window.CouponClaim.scan() を呼ぶ。
呼び出し元: snippets/coupon-card.liquid(商品ページのクーポンバー「取得する」)
■ 署名を「本文に実際に貼られているコード」だけに限定している理由
エントリーボタン(entry-button-custom.liquid)は customer.id:ts のみで署名しているため、
署名を1つ入手すれば任意のキーを送れる。クーポンでこれをやると
「どこかの取得ボタンを踏んだ人が、他のシークレットクーポンのコードを推測して取得する」
ことが成立してしまう。
そこで本ファイルは
・署名対象に code を含める(1署名1クーポンに固定)
・ページ本文 / 記事本文をスキャンし、実際に貼られているコードにだけ署名を発行する
の2点で、そのページを訪れた人だけがそのクーポンを取得できる状態にしている。
■ Worker が「取得可能なクーポンか」を検証しない設計について
claimable のチェックと受付期間の判定は、このファイル(Liquid = サーバー側)で行い、
条件を満たすものにだけ署名を発行する。署名は CLAIM_SECRET を知らないと偽造できないため、
Worker 側は「有効な署名がある = 取得可能」とみなしてよい。
この設計により Worker にメタオブジェクト読み取りスコープを追加せずに済む。
ただし発行済み署名は24時間有効なので、その間にキャンペーンを終了しても取得できる。
厳密に締め切りたい場合は Worker 側の TOKEN_TTL_SEC を短くすること。
■ 動作仕様
- 未ログイン: 「ログインして取得」→ クリックで /account/login へ(return_urlで元ページに復帰)
- 取得済み: グレーアウトの「取得済み」(二重取得は不可・冪等)
- 受付開始前: 「取得開始前」/ 受付終了後: 「受付は終了しました」でグレーアウト
- 通信エラー時: 「エラーが発生しました(タップで再試行)」
- 署名期限切れ: 「再読み込みしてお試しください」
- タグ上限(250)到達: 「取得できませんでした」
- 同一ハンドルのボタンを複数設置した場合、1つの取得で全ボタンが同期する
■ データ仕様
- 記録先: 顧客タグ `have:{割引コード}`(例: have:NEWYEAR20)
- 使用済みタグ `used:{割引コード}` と同じキー(コード)で揃えている。
管理画面の顧客一覧でもタグを見れば何のクーポンか分かる。
- 注意: Shopifyのタグはカンマが区切り文字。コードにカンマを含めないこと
(Worker側でもバリデーションで弾いている)
- 取得済み判定は本ファイルが customer.tags を走査してJSへ渡す
■ メタオブジェクト dis に必要なフィールド
claimable 真偽値 true のものだけ取得ボタンで取得できる
start/end 既存の掲載期間フィールドを取得受付期間として流用する
days_after_signup 整数(任意) 会員登録から◯日を過ぎた顧客は受付終了(2026-09-29 追加)
登録日は顧客メタフィールド custom.signed_up_at を参照する
■ 一覧ページ側との対応
sections/coupon-list-personalized.liquid が同じ `have:{コード}` タグを読み、
secret が true のクーポンをタグ保持者にだけ表示する。
プレフィックスを変更する場合は両方を揃えること。
■ セキュリティ
- claim_secret は entry-button-custom.liquid の entry_secret とは別の値にすること
(entry の漏洩はメタフィールドが書かれるだけだが、こちらはクーポン取得制御が破られる)
- 署名の有効期限24時間 = Shopifyページキャッシュ対策で長め
作成日: 2026-08-28
======================================
1. ボタン
クーポンを受け取る
2. テキスト
クーポンを受け取る
3. 画像