jamstack-storefront について
静的な商品カタログと動的なコマース機能を組み合わせたJamstackストアフロントの開発スキルです。カスタムのヘッドレス構成を使うチーム向けに、Next.jsやAstroでの描画、カタログ更新、CDNキャッシュを扱います。
主な機能
- 01Next.jsとAstroのストア設定
- 02商品カタログの静的生成
- 03オンデマンド再検証
- 04クライアント側カート状態
- 05CDNキャッシュ設定
活用シーン
- 01ヘッドレスECストアの構築
- 02CDN経由の商品ページ配信
- 03カタログ更新後のページ再生成
- 04カート機能の動的な維持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill jamstack-storefrontインストール後、Claude Code で/jamstack-storefrontを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/isr-webhook-revalidation-with-cache-tags/criteria.jsontask.mdevals/next-js-shopify-isr-product-pages/criteria.jsontask.mdevals/zustand-cart-with-localstorage-persisten/criteria.jsontask.md詳細
Jamstack ストアフロント
概要
Jamstack ストアフロントでは、パフォーマンスと CDN キャッシュの効率を最大限に高めるため、ビルド時にカタログページを事前レンダリングします。一方、カート、チェックアウト、アカウントなどの動的な機能には、クライアントサイドの JavaScript とコマース API を使用します。Incremental Static Regeneration(ISR)を備えた Next.js と、オンデマンドレンダリングに対応した Astro が主流の 2 つのアプローチであり、ビルド時間、情報の鮮度、インタラクティブ性の間でそれぞれ異なるトレードオフがあります。このスキルでは、Jamstack コマースサイトのセットアップ、カタログの再生成の管理、ヘッドレスコマース API の統合について説明します。
このスキルを使用する場面
- SEO と Core Web Vitals のスコアを最優先する場合 — 静的 HTML なら Lighthouse でほぼ満点の結果を得られます
- 頻繁には更新されない大規模なカタログがあり、CDN から 100 ミリ秒未満でページを読み込みたい場合
- コマースのバックエンド(Shopify、Saleor、commercetools)とストアフロントのデプロイサイクルを分離したい場合
- プラットフォーム独自のテーマシステムではなく、最新の React/Astro ツールを使いたい場合
- ビルド時にコマースデータと CMS(Contentful、Sanity)のデータを組み合わせる必要がある場合
前提条件とプラットフォームに関する注意事項
このスキルはカスタム/ヘッドレスストアフロント向けです(Node.js、Python、または同様のバックエンド)。コード例では TypeScript/Node.js を使用していますが、どのスタックにも適応できます。
Shopify:Shopify Hydrogen は Shopify のヘッドレスフレームワークです。Shopify をコマースバックエンドとしてカスタムフロントエンドと組み合わせる場合や、Shopify と他の最適なサービスを組み合わせる場合は、MACH/コンポーザブルのパターンを適用できます。 WooCommerce:WooCommerce は REST API と WPGraphQL を介してヘッドレスバックエンドとして利用できます。これらのパターンは、フロントエンドを WordPress から分離する場合に適用できます。 Magento:Magento の GraphQL API と PWA Studio は、ヘッドレスアーキテクチャをサポートしています。これらのコンポーザブルパターンは、MACH スタックのバックエンドサービスとして Magento を使用する場合に適用できます。
必要なもの:
- Node.js 18+(またはバックエンドの言語に合わせて調整してください)
- キャッシュ/キュー用の Redis
- メール送信サービス(SendGrid、AWS SES、または Postmark)
- CDN(Cloudflare、CloudFront、または Fastly)
主な手順
-
Next.js のコマースストアフロントをセットアップする
npx create-next-app@latest my-store --typescript --tailwind --app cd my-store npm install @shopify/storefront-api-client graphqlStorefront API クライアントを設定します:
// lib/shopify.ts import {createStorefrontApiClient} from '@shopify/storefront-api-client'; export const shopify = createStorefrontApiClient({ storeDomain: process.env.SHOPIFY_STORE_DOMAIN!, publicAccessToken: process.env.SHOPIFY_STOREFRONT_TOKEN!, apiVersion: '2025-01', }); -
ISR で商品ページを静的に生成する
// app/products/[handle]/page.tsx (Next.js App Router) import {shopify} from '@/lib/shopify'; import {notFound} from 'next/navigation'; // ISR: revalidate every 60 seconds export const revalidate = 60; // Pre-build top products at build time export async function generateStaticParams() { const {data} = await shopify.request(TOP_PRODUCTS_QUERY, { variables: {first: 200}, }); return data.products.edges.map(({node}: any) => ({handle: node.handle})); } export default async function ProductPage({params}: {params: {handle: string}}) { const {data} = await shopify.request(PRODUCT_QUERY, { variables: {handle: params.handle}, }); if (!data.product) notFound(); return <ProductDetail product={data.product} />; } const PRODUCT_QUERY = ` query ProductByHandle($handle: String!) { product(handle: $handle) { id title descriptionHtml images(first: 5) { edges { node { url altText } } } variants(first: 20) { edges { node { id title price { amount currencyCode } availableForSale } } } } } `; -
JavaScript のオーバーヘッドを最小限に抑えた Astro ストアフロントを構築する
Astro はデフォルトで JavaScript を一切出力しません。明示的にハイドレーションを指定しない限り、コンポーネントはサーバー側で静的 HTML にレンダリングされます:
npm create astro@latest -- --template minimal cd my-astro-store npx astro add tailwind npm install @astrojs/node graphql-request--- // src/pages/products/[handle].astro import {GraphQLClient, gql} from 'graphql-request'; import Layout from '../../layouts/Layout.astro'; import AddToCartButton from '../../components/AddToCartButton.tsx'; // Island export async function getStaticPaths() { const client = new GraphQLClient(import.meta.env.SALEOR_API_URL); const {products} = await client.request(gql`query { products(first: 200, channel: "default-channel") { edges { node { slug } } } }`); return products.edges.map(({node}: any) => ({params: {handle: node.slug}})); } const {handle} = Astro.params; const client = new GraphQLClient(import.meta.env.SALEOR_API_URL); const {product} = await client.request(PRODUCT_QUERY, {slug: handle, channel: 'default-channel'}); --- <Layout title={product.name}> <h1>{product.name}</h1> <img src={product.thumbnail.url} alt={product.thumbnail.alt} /> <AddToCartButton client:load variantId={product.variants[0].id} /> </Layout> -
カタログの鮮度を保つため、オンデマンド ISR の Webhook を実装する
CMS またはコマースプラットフォームで商品が更新されたときに、Next.js をトリガーし、そのページのみを再検証します:
// app/api/revalidate/route.ts import {NextRequest, NextResponse} from 'next/server'; import {revalidatePath, revalidateTag} from 'next/cache'; export async function POST(req: NextRequest) { const authHeader = req.headers.get('authorization'); if (authHeader !== `Bearer ${process.env.REVALIDATION_TOKEN}`) { return NextResponse.json({error: 'Unauthorized'}, {status: 401}); } const body = await req.json(); const {type, handle, collectionHandle} = body; switch (type) { case 'product': revalidatePath(`/products/${handle}`); revalidateTag('products'); break; case 'collection': revalidatePath(`/collections/${collectionHandle}`); revalidateTag('collections'); break; case 'all': revalidateTag('products'); revalidateTag('collections'); break; } return NextResponse.json({revalidated: true, timestamp: Date.now()}); }
商品が更新された際に、このエンドポイントへ POST するようコマースプラットフォームを設定します。
-
Zustand でクライアントサイドのカートを実装する
静的な商品ページでは、クライアントサイドのカート状態が必要です。localStorage による永続化に対応した軽量な状態管理を使用します:
// lib/cart-store.ts import {create} from 'zustand'; import {persist} from 'zustand/middleware'; interface CartItem { variantId: string; title: string; price: number; quantity: number; image: string; } interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (variantId: string) => void; updateQuantity: (variantId: string, quantity: number) => void; clearCart: () => void; total: () => number; } export const useCartStore = create<CartStore>()( persist( (set, get) => ({ items: [], addItem: (item) => set((state) => { const existing = state.items.find(i => i.variantId === item.variantId); if (existing) { return {items: state.items.map(i => i.variantId === item.variantId ? {...i, quantity: i.quantity + item.quantity} : i)}; } return {items: [...state.items, item]}; }), removeItem: (variantId) => set((state) => ({items: state.items.filter(i => i.variantId !== variantId)})), updateQuantity: (variantId, quantity) => set((state) => ({items: state.items.map(i => i.variantId === variantId ? {...i, quantity} : i)})), clearCart: () => set({items: []}), total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0), }), {name: 'cart-storage'}, ), ); -
CDN のキャッシュとキャッシュのパージを設定する
// next.config.ts export default { async headers() { return [ { source: '/products/:path*', headers: [ {key: 'Cache-Control', value: 'public, s-maxage=60, stale-while-revalidate=600'}, ], }, { source: '/api/:path*', headers: [ {key: 'Cache-Control', value: 'no-store'}, ], }, ]; }, images: { remotePatterns: [ {protocol: 'https', hostname: '**.shopify.com'}, {protocol: 'https', hostname: '**.saleor.io'}, ], }, };
例
キャッシュタグを使用した Next.js 15 App Router
// lib/get-product.ts
export async function getProduct(handle: string) {
const res = await fetch(`${process.env.SALEOR_API_URL}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query: PRODUCT_QUERY, variables: {slug: handle, channel: 'default-channel'}}),
next: {
revalidate: 300,
tags: [`product-${handle}`, 'products'],
},
});
if (!res.ok) throw new Error(`Failed to fetch product: ${res.status}`);
const {data} = await res.json();
return data.product;
}
カートページにオンデマンドレンダリングを使用した Astro
---
// src/pages/cart.astro
// Opt out of static generation for cart — render on every request
export const prerender = false;
import Layout from '../layouts/Layout.astro';
import CartPage from '../components/CartPage.tsx';
---
<Layout title="Your Cart">
<CartPage client:only="react" />
</Layout>
ベストプラクティス
- 上位 N 件の商品のみ静的パラメーターを生成します — ビルド時に 100,000 件の商品ページを生成すると時間がかかります。上位 500 件を生成し、ロングテールは初回リクエスト時に ISR で処理します
- Next.js のフェッチキャッシュタグを使用します — 各フェッチに意味のある名前(
product-${handle}、collections)のタグを付けることで、すべてを消去するのではなく、必要な箇所だけを再検証できます - 動的な要素と静的な要素を分けます — 商品の詳細と画像は静的 HTML にし、カート、アカウント、パーソナライズ機能はクライアントでレンダリングするアイランドにします
- デプロイ後に ISR ページを事前にウォームアップします — デプロイ直後に上位商品の URL にアクセスするスクリプトを実行し、顧客が訪れる前に CDN キャッシュを生成します
- ビルド時のデータレイヤーを使用します — N 件の商品ページごとに N 回個別に API を呼び出すのではなく、ビルド時にカタログデータを一括取得します
- 更新頻度に応じて
revalidateを設定します — フラッシュセールの価格には短い再検証間隔(10s)が必要ですが、定番商品の説明なら 1 時間以上でも問題ありません - CI で Lighthouse を使ってテストします — Lighthouse CI のステップを追加してパフォーマンス予算を適用し、リグレッションを本番環境に反映される前に検出します
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| カタログが大きくなるとビルド時間が急増する | 上位商品のみに generateStaticParams を使用し、それ以外はオンデマンドでレンダリングされるよう dynamicParams = true を設定します |
| ISR によってフラッシュセール中に古い価格が表示される | 価格データに revalidate = 10 を設定するか、価格をクライアント側で取得して静的シェルにマージします |
| ページ遷移後にカートの状態が保持されない | persist ミドルウェアを備えた zustand を使用するか、Cookie/セッションに保存するサーバーサイドのカートを使用します |
| 本番環境で画像を読み込めない | すべてのコマース CDN ホスト名を next.config.ts の images.remotePatterns に追加します |
| オンデマンド再検証エンドポイントが悪用される | Authorization ヘッダーにシークレットトークンを必ず要求し、エンドポイントが公開されている場合はトークンをローテーションします |
関連スキル
- @shopify-hydrogen
- @saleor-development
- @pwa-storefront
- @image-optimization-cdn
- @edge-commerce