spree-storefront について
Spreeバックエンドと @spree/sdk の Store API を連携する、Spree Next.js ストアフロントの開発向けリファレンスです。サーバー側のリクエスト、カートとチェックアウト、地域別ルーティング、任意の卸売画面を扱います。
主な機能
- 01@spree/sdk による Store API 呼び出し
- 02サーバー側での API リクエストと認証
- 03カート、決済、注文フローの実装
- 04国と言語に応じたストアフロントのルーティング
- 05卸売チャネルと Webhook の設定
活用シーン
- 01Spreeバックエンド向けの Next.js ストア構築
- 02カートから注文までのチェックアウト実装
- 03複数地域向けストアフロントページの提供
- 04卸売専用のショッピング体験の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-storefrontインストール後、Claude Code で/spree-storefrontを入力して読み込まれたか確認します。
詳細
Spree Storefront (Next.js)
Spree Next.js ストアフロントは、@spree/sdk 2.x を使用して v3 Store API(/api/v3/store/*)経由で Spree バックエンドと通信する独立したアプリです。create-spree-app は apps/storefront/(server/ と apps/dashboard/ の隣)に配置します。または https://github.com/spree/storefront をフォークしてください。Next.js 16、React 19、Tailwind 4 を使用します。
ストアフロントは任意です。任意のフロントエンド(Astro、Remix、React Native など)で同じ Store API を利用できます。このスキルでは公式ストアフロントを前提としていますが、API コントラクトはどのクライアントでも同一です。
アーキテクチャ — サーバー優先
Browser → Server Action (src/lib/data/*) → @spree/sdk → Spree Store API
httpOnly cookies (JWT, refresh token, cart token, country/locale) via src/lib/spree helpers
- すべての API 呼び出しはサーバー側で実行されます(
src/lib/data/内の Server Actions / Server Components)。ブラウザーから Spree を直接呼び出すことはありません。公開可能キーもサーバー側のみで使用します(SPREE_PUBLISHABLE_KEY。NEXT_PUBLIC_は付けません)。 src/lib/spree/には統合機能が含まれます。getClient()、Cookie ヘルパー、withAuthRefresh()、getLocaleOptions()、getCartOptions()、ミドルウェア、サーフェス(DTC 対卸売)、Webhook です。- クライアントコンポーネント(
CartContext、CheckoutContextなど)は UI の状態を保持して Server Actions を呼び出します。ブラウザーから Store API に対する SWR/React Query のフェッチを追加しないでください。
// src/lib/data/customer.ts (pattern)
import { getClient, withAuthRefresh, setAccessToken, setRefreshToken } from '@/lib/spree'
export async function login(email: string, password: string) {
const { token, refresh_token, user } = await getClient().auth.login({ email, password })
await setAccessToken(token)
await setRefreshToken(refresh_token)
return { success: true, user }
}
export async function getCustomer() {
return withAuthRefresh((options) => getClient().customer.get(options))
}
必須の環境変数(.env.local):SPREE_API_URL、SPREE_PUBLISHABLE_KEY。任意:NEXT_PUBLIC_DEFAULT_COUNTRY / NEXT_PUBLIC_DEFAULT_LOCALE、NEXT_PUBLIC_SITE_URL、SPREE_WHOLESALE_CHANNEL、SPREE_WHOLESALE_PUBLISHABLE_KEY、SPREE_WEBHOOK_SECRET、RESEND_API_KEY、EMAIL_FROM。NEXT_PUBLIC_* は、ブラウザーで実際に必要な値(デフォルトの地域、Stripe/PayPal の公開可能キー)にのみ使用します。
@spree/sdk の基本
import { createClient } from '@spree/sdk'
const spree = createClient({
baseUrl: process.env.SPREE_API_URL!,
publishableKey: process.env.SPREE_PUBLISHABLE_KEY!,
locale: 'en', // sent as x-spree-locale (also per request: { locale })
channel: 'online', // sent as X-Spree-Channel; omit for the store default
})
const { data, meta } = await spree.products.list({ expand: ['media', 'default_variant'] })
const product = await spree.products.get('cool-shirt', { expand: ['variants', 'media'] }) // slug or prod_ id
const cart = await spree.carts.create()
await spree.carts.items.create(cart.id, { variant_id: 'variant_k5nR8xLq', quantity: 1 }, { spreeToken: cart.token })
const orders = await spree.customer.orders.list({}, { token }) // logged-in customer (JWT)
- フラットなレスポンス(
product.name)を使用します。リレーションは*_idフィールド、またはexpandを指定した場合はネストされたオブジェクト(product.default_variant、product.primary_media、product.media[])です。価格はPriceオブジェクト(product.price.display_amount)として返されます。クライアント側で金額を計算しないでください。 - 認証モード:公開可能キーのみ(閲覧)・
spreeTokenを追加(ゲストカート、x-spree-token)・tokenJWT を追加(顧客)。型は@spree/sdk(および@spree/sdk/zod)、Webhook ヘルパーは@spree/sdk/webhooksにあります。 - ID はプレフィックス付きの文字列です(
prod_、variant_、cart_、or_、cust_、ful_)。カートではcart.idを使用します。cart.numberは 5.x クライアント向けに維持されている、idの非推奨のミラーです(6.1 で削除)。
カート → チェックアウト → 注文
Spree 6 では、変更可能なカート(cart_…)と、カートの完了時に作成される変更不可の注文(or_…)が分かれています。進行させるべきチェックアウト状態マシンはありません。カート(メールアドレス、住所、配送レート、支払い方法)は任意の順序で更新でき、カートには不足している項目が表示されます。
const cart = await spree.carts.get(cartId, { spreeToken })
cart.requirements
// [{ step: 'address', field: 'ship_address', code: 'ship_address_required', message: '…' },
// { step: 'delivery', field: 'delivery_method', code: 'delivery_method_required', message: '…' }, …]
requirements[]の各項目は{ step, field, code, message }です。コードにはline_items_required、email_required、ship_address_required、delivery_method_required、payment_required、po_number_required、guest_checkout_not_allowed、quantity_rule_violated、order_minimum_not_met、discontinued、out_of_stockがあります。ステップ(cart、address、delivery、payment)は UI のグループ分けを補助するものです。チェックアウトのセクションに対応付けて、messageを表示してください。確定を阻止するのは完了処理だけです。カスタム要件はバックエンドから取得します(Spree::Checkout::Registry.add_requirement。spree-checkoutを参照)。- 配送:
spree.carts.fulfillments.update(cartId, fulfillmentId, { selected_delivery_rate_id }, opts)は、再計算されたカートを返します。 - 支払い(Stripe、Adyen、PayPal などのセッションベースのゲートウェイ):
spree.carts.paymentSessions.create(cartId, { payment_method_id }, opts)→ プロバイダーのデータ(クライアントシークレット、ドロップイントークン、リダイレクト URL)。- プロバイダーの UI を表示する/リダイレクトします。
spree.carts.paymentSessions.complete(cartId, sessionId, { session_result }, opts)。spree.carts.complete(cartId, opts)→ 注文を返します(型はOrder | OrderGroup。複数の販売者がいるマーケットプレイスのカートではogrp_…グループが返されます。@spree/sdkのisOrderGroupで型を絞り込んでください)。この処理はべき等です。ゲートウェイの Webhook がすでにカートを完了している場合は、既存の結果を返します。 非セッション方式(小切手、代金引換、銀行振込):spree.carts.payments.create(...)。
- 完了後、カートは
carts.getから取得できなくなります。結果は**spree.orders.get(id, params, { spreeToken | token })**で読み取ってください。or_…ID と元のcart_…ID のどちらも受け付けるため、確認ページ(order-placed/[id])では引き続きカート ID を識別子として使用できます。注文のstatusはdraft/placed/canceledで、payment_statusとfulfillment_statusはそれぞれ別に管理されます。 - 参考フロー:
src/lib/data/checkout.ts、payment.ts(completeCheckoutOrderは 403/422 の競合も許容します)、components/checkout/*、ゲートウェイ gem(例:spree_stripe)。
マルチリージョン URL
すべてのルートは src/app/[country]/[locale]/… 配下(/us/en/products、/de/de/products)にあり、Spree のマーケット(国+通貨+ロケール)に対応しています。src/proxy.ts(Next 16 のミドルウェアファイル)は、@/lib/spree/middleware の createSpreeMiddleware({ defaultCountry, defaultLocale, staticRoutes? }) を設定し、以下を行います。
- ベースパスを
/{country}/{locale}/…にリダイレクトします。 spree_countryCookie →x-vercel-ip-country/cf-ipcountry→ デフォルトの順に国を判定し、spree_locale→Accept-Language→ デフォルトの順にロケールを判定します。- URL に合わせて
spree_country/spree_localeCookie を同期します。
データ関数はリージョン引数を受け取りません。代わりに getLocaleOptions() を呼び出し、Cookie を読み取ってロケール/国を SDK に渡します。新しいページは src/app/[country]/[locale]/(storefront)/your-page/page.tsx 配下に作成すると、レイアウトを継承します。CountrySwitcher は新しいプレフィックスへ移動してリージョンを切り替えます。
卸売/チャネルのサーフェス
/wholesale は別のチャネル上にある、オプトイン方式の B2B ポータルです。ゲート付きチャネルのコードを SPREE_WHOLESALE_CHANNEL に設定した場合のみ有効になります(未設定の場合、卸売 UI はすべて非表示になり、ルートは 404 を返します)。サーフェスとは、チャネルに紐付いた SDK クライアント(channel: 'wholesale' → X-Spree-Channel)を指し、独自のカート Cookie とキャッシュタグを持ちます。顧客 JWT は共有されます。チャネルの storefront_access(login_required | prices_hidden | public)は API によって適用されます(401、または金額フィールドが null)。購入者の承認は Wholesale 顧客グループへの所属で判定され、取引価格はバックエンドの価格リストから取得されます。参考実装として、会員限定の DTC や B2B 専用ストアなどに応用できます。spree-b2b を参照してください。
Webhook とトランザクションメール
ストアフロントでは顧客向けメールを管理できます(react-email テンプレートは src/lib/emails/ にあり、Resend 経由で送信されます。開発時は .next/emails/ に書き込まれ、/dev/emails でプレビューできます)。ルート src/app/api/webhooks/spree/route.ts は @/lib/spree/webhooks の createWebhookHandler を使用します。これは X-Spree-Webhook-Signature("{timestamp}.{body}" に対する HMAC-SHA256、タイミング攻撃に安全な比較、5 分間のリプレイ防止期間)を検証し、イベント名に応じて処理を振り分けます。
import { createWebhookHandler } from '@/lib/spree/webhooks'
const handler = createWebhookHandler({
secret: process.env.SPREE_WEBHOOK_SECRET!,
handlers: {
'order.placed': handleOrderPlaced,
'order.canceled': handleOrderCanceled,
'order.fulfilled': handleOrderFulfilled, // or 'fulfillment.fulfilled' for per-parcel emails
'customer.password_reset_requested': handlePasswordReset, // must be subscribed by name — `*` never receives it
},
// waitUntil, toleranceSeconds optional
})
export const POST = handler
- イベント名は Spree 6 のものです。
order.placed(order.completedではありません)、order.fulfilled/fulfillment.fulfilled(order.shippedではありません)。現在リリースされているストアフロントのコードでは、order.completedとorder.shippedが引き続き登録されています。これらは非推奨のエイリアスで、Spree 6.0 では新しい名前と並行して送信されますが、6.1 では送信されなくなります。ハンドラーとエンドポイントのサブスクリプションを新しい名前に切り替えてください。 - マーケットプレイス(分割)チェックアウトの場合、子注文は
notify_customer: falseを指定してorder.placedを発行します。購入の確認はorder_group.completedに属します。注文メールを送信する場合は、このイベントのハンドラーを追加してください(spree-marketplaceを参照)。 - ストアフロントで顧客向けメールを管理する場合は、ストアの設定
send_consumer_transactional_emailsをオフ(false)にして、Spree 独自のメールを無効にしてください。これは、注文のnotify_customer: falseと同様に、顧客向けメールのみを停止します。new_order_notifications_emailにアドレスが設定されている場合、ストアの新規注文通知は引き続き送信されます。 - ダッシュボードの Settings → Developer → Webhooks でエンドポイントを作成し、イベントをサブスクライブして、そのシークレットを
SPREE_WEBHOOK_SECRETにコピーしてください。ローカルでテストするには、ストアフロントを外部公開します(cloudflared tunnel --url http://localhost:3001)。 - 配信に失敗すると、バックオフ付きで再試行されます(約 6 分間に 5 回、同じイベント
id)。配信が 15 回連続で失敗するとエンドポイントが無効になります。速やかに 2xx を返し、event.idで重複を排除して、時間のかかる処理はwaitUntil/ キューで実行してください。未処理のイベントには 200{ handled: false }を返します。 - 公式ストアフロント以外では、
@spree/sdk/webhooksのverifyWebhookSignature(body, signature, timestamp, secret)とtype WebhookEvent<T>を使用してください。イベントの全一覧はspree-events-webhooksを参照してください。
テスト
- ユニット/インテグレーション: Vitest —
pnpm test/pnpm test:watch(テストはコードの隣に配置されています。例:src/lib/data/__tests__)。 - E2E: Docker 上の実際のバックエンドに対して Playwright を実行します。対応する Stripe テスト用キーのペア(
STRIPE_PUBLISHABLE_KEY、STRIPE_SECRET_KEY)をエクスポートし、pnpm e2e:up(Postgres/Redis とghcr.io/spree/spree:latestを起動し、@spree/cliでシードを実行して、.env.e2eを書き込みます)、pnpm test:e2e(対話形式で実行する場合は:ui)、pnpm e2e:downの順に実行します。 - 独自のバックエンドに対してテストするには、
SPREE_IMAGE=<image> pnpm e2e:upを実行してください。create-spree-appプロジェクトでは、まずルートからdocker build . -f server/Dockerfile -t project-spree:e2eを実行してビルドしてください。生成された CI ではこの処理が自動で行われます。
ストアフロントとバックエンド — 変更箇所の見極め
| 変更したい内容 | 該当箇所 |
|---|---|
| レイアウト、文言、スタイル、新しいコンテンツページ | ストアフロント(src/app/[country]/[locale]/(storefront)/…、src/app/globals.css の @theme トークン) |
| 商品/カートのレスポンスにフィールドを追加する | バックエンド — モデルとシリアライザーの差し替え(spree-resource、spree-api-v3) |
| 価格を変更する | バックエンド — 価格リスト/ルール(spree-pricing) |
| カート合計の計算方法を変更する | バックエンド — Spree::Carts::RecalculateTotals(DI キー cart_recalculate_totals_workflow)またはワークフローフック(spree-order-totals、spree-workflows) |
| チェックアウト要件を追加する(例:「利用規約に同意する」) | バックエンドの Spree::Checkout::Registry.add_requirement。ストアフロントでは新しい requirements[] コードを表示します |
| 国/通貨/言語を追加する | バックエンドの Markets。ストアフロントは URL と Cookie に従うだけです |
| チェックアウト UI をカスタマイズする | ストアフロント(components/checkout/*) |
| 商品詳細ページの A/B テスト、クライアント分析 | ストアフロント(src/lib/analytics) |
| 注文を CRM/ERP に同期する | バックエンドの order.placed サブスクライバー(spree-events-webhooks) |
| 顧客向けメールの外観 | ストアフロントのメール(webhook)またはバックエンドのメーラー — 管理元をどちらか一方に決めてください |
ルール:顧客に表示されるものはすべてストアフロント、データ、お金、ビジネスルールに関わるものはすべてバックエンドです。これにより、すべてのフロントエンドで同じ動作になります。
注意点
- シークレットキー(
sk_…)をストアフロントに置かないでください — サーバー側であっても同様です。Store API では公開可能キーを使用します。管理タスクはダッシュボードまたはバックエンドのジョブで行ってください。 - カートトークンはカートへのアクセス権を付与します — httpOnly のセキュア Cookie に保持してください(ヘルパーが対応します)。
- カタログはキャッシュし、カート/チェックアウトはキャッシュしないでください。 ストアフロントでは、対象ごとに Next のキャッシュタグ(
updateTag(cartTag(surface)))を使用します — カートを変更した後は無効化してください。 - チェックアウトの手順順序を決めつけないでください —
requirements[]を基に UI を制御し、carts.completeがエラー(コードcart_cannot_complete)を返した場合は、カートのrequirementsを再取得してください。 - 完了後に
carts.getを呼び出さないでください — カート ID または注文 ID を使ってorders.getを使用してください。 - ローカライズはヘッダーで指定します(
x-spree-locale、X-Spree-Channel)。Accept-Languageではありません。ストアフロントでは Cookie を介して URL セグメントから取得します。 - 上流の変更を取り込み続けてください:ストアフロントリポジトリを
upstreamリモートとして追加し、定期的にマージしてください。カスタマイズは、自分で管理するコンポーネント/データファイルに保持してください。
詳しく読むには
- ドキュメント(インストール済み):
node_modules/@spree/docs/dist/developer/storefront/nextjs/{architecture,customization,multi-region,wholesale,emails,environment-variables,testing,deployment,wallet-payments}.md。SDK:developer/sdk/quickstart.md。オンライン:https://spreecommerce.org/docs/developer/storefront/nextjs/architecture - チュートリアル:
developer/tutorial/storefront.md - ソース:https://github.com/spree/storefront(`src/lib/spree/`、`src/lib/data/`、`src/app/api/webhooks/spree/route.ts`)
- 関連スキル:
spree-typescript-sdk、spree-api-v3、spree-checkout、spree-events-webhooks、spree-b2b、spree-i18n、spree-deployment。