关于 jamstack-storefront
这项开发技能介绍如何构建 Jamstack 电商店铺,以静态页面呈现商品目录,并动态处理交易功能。适用于采用自定义无头架构的团队,涵盖 Next.js 或 Astro 渲染、目录更新和 CDN 缓存。
主要功能
- 01配置 Next.js 和 Astro 店铺
- 02静态生成商品目录
- 03按需重新验证页面
- 04客户端购物车状态
- 05配置 CDN 缓存
适用场景
- 01构建无头电商店铺
- 02通过 CDN 提供商品页
- 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 提供动态功能(购物车、结账、账户)。Next.js 的增量静态再生(ISR)和 Astro 的按需渲染是两种主流方案,各自在构建时间、内容新鲜度和交互性方面有所取舍。本技能介绍如何搭建 Jamstack 电商网站、管理商品目录再生成,以及集成无头电商 API。
适用场景
- 当 SEO 和 Core Web Vitals 评分是首要任务时——静态 HTML 的 Lighthouse 评分几近满分
- 当你拥有很少变动的大型商品目录,并希望通过 CDN 实现低于 100 ms 的页面加载时间时
- 当你希望将电商后端(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 graphql配置 Storefront 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 默认不会发送任何 JS——组件会被服务端渲染为静态 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'}, ], }, };
示例
使用 fetch 缓存标签的 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 fetch 缓存标签 — 为每次 fetch 添加语义化名称作为标签(
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/session 中的服务端购物车 |
| 图片在生产环境中无法加载 | 将所有电商 CDN 主机名添加到 next.config.ts 的 images.remotePatterns 中 |
| 按需重新验证端点遭到滥用 | 始终要求在 Authorization 标头中提供密钥令牌;如果端点可被公开发现,请轮换令牌 |
相关技能
- @shopify-hydrogen
- @saleor-development
- @pwa-storefront
- @image-optimization-cdn
- @edge-commerce