mcpField
S
spree-storefront

spree-storefront

github.com/spree開発10230 回インストール4 週間前に更新
リポジトリ

spree-storefront について

Spreeバックエンドと @spree/sdk の Store API を連携する、Spree Next.js ストアフロントの開発向けリファレンスです。サーバー側のリクエスト、カートとチェックアウト、地域別ルーティング、任意の卸売画面を扱います。

主な機能

  1. 01@spree/sdk による Store API 呼び出し
  2. 02サーバー側での API リクエストと認証
  3. 03カート、決済、注文フローの実装
  4. 04国と言語に応じたストアフロントのルーティング
  5. 05卸売チャネルと Webhook の設定

活用シーン

  1. 01Spreeバックエンド向けの Next.js ストア構築
  2. 02カートから注文までのチェックアウト実装
  3. 03複数地域向けストアフロントページの提供
  4. 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)・token JWT を追加(顧客)。型は @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 などのセッションベースのゲートウェイ):
    1. spree.carts.paymentSessions.create(cartId, { payment_method_id }, opts) → プロバイダーのデータ(クライアントシークレット、ドロップイントークン、リダイレクト URL)。
    2. プロバイダーの UI を表示する/リダイレクトします。
    3. spree.carts.paymentSessions.complete(cartId, sessionId, { session_result }, opts)。
    4. 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_country Cookie → x-vercel-ip-country/cf-ipcountry → デフォルトの順に国を判定し、spree_locale → Accept-Language → デフォルトの順にロケールを判定します。
  • URL に合わせて spree_country/spree_locale Cookie を同期します。

データ関数はリージョン引数を受け取りません。代わりに 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 リモートとして追加し、定期的にマージしてください。カスタマイズは、自分で管理するコンポーネント/データファイルに保持してください。

詳しく読むには

コメント