
Jamstack Storefront
- 59 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build a fast storefront with Next.js or Astro that pre-renders product pages as static HTML and fetches live data from commerce APIs.
About
Sets up a Jamstack commerce site pre-rendering catalog pages at build time with ISR/on-demand rendering and headless APIs. A developer uses it when SEO and Core Web Vitals are top priority or to decouple the backend from the storefront deploy cycle.
- Next.js ISR vs Astro on-demand rendering tradeoffs
- Catalog regeneration and headless commerce API integration
Jamstack Storefront by the numbers
- 59 all-time installs (skills.sh)
- Ranked #1,218 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill jamstack-storefrontAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build a fast storefront with Next.js or Astro that pre-renders product pages as static HTML and fetches live data from commerce APIs.
Files
Jamstack Storefront
Overview
A Jamstack storefront pre-renders catalog pages at build time for maximum performance and CDN cacheability, while using client-side JavaScript and commerce APIs for dynamic functionality (cart, checkout, account). Next.js with Incremental Static Regeneration (ISR) and Astro with on-demand rendering are the two dominant approaches, each offering different tradeoffs between build times, freshness, and interactivity. This skill covers setting up a Jamstack commerce site, managing catalog regeneration, and integrating headless commerce APIs.
When to Use This Skill
- When SEO and Core Web Vitals scores are top priorities — static HTML scores near-perfect Lighthouse results
- When you have a large catalog that rarely changes and want sub-100ms page loads from CDN
- When you want to decouple the commerce backend (Shopify, Saleor, commercetools) from the storefront deployment cycle
- When your team wants to use modern React/Astro tooling rather than a platform's proprietary theme system
- When you need to combine commerce data with a CMS (Contentful, Sanity) at build time
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services. WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress. Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- Redis for caching/queues
- An email sending service (SendGrid, AWS SES, or Postmark)
- CDN (Cloudflare, CloudFront, or Fastly)
Core Instructions
1. Bootstrap a Next.js commerce storefront
npx create-next-app@latest my-store --typescript --tailwind --app
cd my-store
npm install @shopify/storefront-api-client graphqlConfigure the Storefront API client:
// 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',
});2. Statically generate product pages with 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 } }
}
}
}
`;3. Build an Astro storefront for minimal JavaScript overhead
Astro ships zero JS by default — components are server-rendered to static HTML unless explicitly hydrated:
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} />
<!-- Only this interactive island ships JavaScript -->
<AddToCartButton client:load variantId={product.variants[0].id} />
</Layout>4. Implement on-demand ISR webhooks for catalog freshness
When a product is updated in your CMS or commerce platform, trigger Next.js to revalidate only that page:
// 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()});
}Configure your commerce platform to POST to this endpoint on product updates.
5. Implement client-side cart with Zustand
Static product pages need client-side cart state. Use lightweight state management with localStorage persistence:
// 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'},
),
);6. Configure CDN caching and cache purging
// 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'},
],
},
};Examples
Next.js 15 App Router with fetch caching tags
// 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 with on-demand rendering for cart pages
---
// 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">
<!-- Full client hydration for interactive cart UI -->
<CartPage client:only="react" />
</Layout>Best Practices
- Generate static params for only your top N products — generating 100,000 product pages at build time is slow; generate the top 500 and let ISR handle the long tail on first request
- Use Next.js fetch cache tags — tag each fetch with semantic names (
product-${handle},collections) so revalidation is surgical rather than purging everything - Separate dynamic from static concerns — product details and images should be static HTML; cart, account, and personalization should be client-rendered islands
- Pre-warm ISR pages after deployment — run a script that hits your top product URLs immediately after deploy to populate the CDN cache before customers arrive
- Use a build-time data layer — fetch all catalog data in a single large batch at build time rather than making N individual API calls for N product pages
- Set `revalidate` based on update frequency — flash sale prices need low revalidation (10s); evergreen product descriptions can be 1 hour or more
- Test with Lighthouse in CI — add a Lighthouse CI step to enforce performance budgets so regressions are caught before they reach production
Common Pitfalls
| Problem | Solution |
|---|---|
| Build times balloon with large catalogs | Use generateStaticParams only for top products; set dynamicParams = true so the rest are rendered on demand |
| ISR serves stale prices during flash sales | Use revalidate = 10 for pricing data or fetch price client-side and merge into the static shell |
| Cart state not persisting across page navigations | Use zustand with persist middleware or a server-side cart stored in a cookie/session |
| Images fail to load in production | Add all commerce CDN hostnames to next.config.ts images.remotePatterns |
| On-demand revalidation endpoint abused | Always require a secret token in the Authorization header; rotate the token if the endpoint is publicly discoverable |
Related Skills
- @shopify-hydrogen
- @saleor-development
- @pwa-storefront
- @image-optimization-cdn
- @edge-commerce
{
"context": "Tests whether the agent correctly implements on-demand ISR revalidation with Authorization header security, targeted use of revalidatePath and revalidateTag, correct event type handling, and semantic fetch cache tags in the data layer.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Authorization header check",
"max_score": 12,
"description": "app/api/revalidate/route.ts reads the 'authorization' header and returns a 401 response if it does not match 'Bearer <token>' (using an env variable for the token)"
},
{
"name": "revalidatePath used",
"max_score": 10,
"description": "app/api/revalidate/route.ts calls revalidatePath() for specific page paths (e.g. /products/${handle})"
},
{
"name": "revalidateTag used",
"max_score": 10,
"description": "app/api/revalidate/route.ts calls revalidateTag() to invalidate named cache tags"
},
{
"name": "product event type handled",
"max_score": 10,
"description": "app/api/revalidate/route.ts handles a 'product' event type by revalidating the specific product path and/or 'products' tag"
},
{
"name": "collection event type handled",
"max_score": 8,
"description": "app/api/revalidate/route.ts handles a 'collection' event type by revalidating the specific collection path and/or 'collections' tag"
},
{
"name": "all event type handled",
"max_score": 8,
"description": "app/api/revalidate/route.ts handles an 'all' event type that revalidates both 'products' and 'collections' tags"
},
{
"name": "Semantic tag names in data layer",
"max_score": 12,
"description": "lib/get-product.ts assigns fetch cache tags using semantic names including a per-product tag like 'product-${handle}' AND a collection tag like 'products'"
},
{
"name": "fetch next.tags used",
"max_score": 10,
"description": "lib/get-product.ts uses the next: { tags: [...] } option in the fetch call (not a manual cache wrapper)"
},
{
"name": "Targeted invalidation only",
"max_score": 10,
"description": "The revalidation endpoint does NOT call revalidatePath('/') or any other blanket full-site purge — it only revalidates paths/tags related to the event type"
},
{
"name": "Success response",
"max_score": 10,
"description": "app/api/revalidate/route.ts returns a JSON response with at least { revalidated: true } on success"
}
]
}
On-Demand Cache Invalidation for a Next.js Storefront
Problem/Feature Description
SportZone runs a large statically generated Next.js storefront connected to a Saleor backend. The merchandising team runs frequent flash sales where prices drop for a few hours — they need those price changes to appear on the site within seconds of being published in the backend, not after a 5-minute ISR interval. The engineering team wants a webhook-driven approach: whenever a product or collection is updated in Saleor, the backend posts to a Next.js API endpoint, which then purges only the affected cached pages and their associated cache tags.
Security is a concern: the endpoint should not be callable by anyone on the internet without authorization. The team also uses Next.js fetch cache tags throughout the data layer, so revalidation needs to be targeted using those same tag names to avoid purging the entire cache unnecessarily.
You also need to implement the data-fetching function that assigns fetch cache tags when loading product data, so that tag-based revalidation works end-to-end.
Output Specification
Produce the following files:
app/api/revalidate/route.ts— the Next.js API route handler for on-demand revalidationlib/get-product.ts— a data-fetching function that fetches a product with appropriate fetch cache tags
Do not install packages or run a build — just produce the source files.
{
"context": "Tests whether the agent correctly sets up a Next.js Shopify storefront with the right Storefront API client, ISR configuration, static params strategy, cache headers, and image configuration.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Correct Shopify package",
"max_score": 10,
"description": "lib/shopify.ts imports from '@shopify/storefront-api-client' (not a different Shopify client library)"
},
{
"name": "createStorefrontApiClient used",
"max_score": 8,
"description": "lib/shopify.ts calls createStorefrontApiClient() to construct the client instance"
},
{
"name": "API version 2025-01",
"max_score": 10,
"description": "The Storefront API client is configured with apiVersion: '2025-01'"
},
{
"name": "ISR revalidate export",
"max_score": 10,
"description": "app/products/[handle]/page.tsx exports 'export const revalidate' with a numeric value (e.g. 60)"
},
{
"name": "generateStaticParams limit",
"max_score": 10,
"description": "generateStaticParams fetches a limited set of products (e.g. first: 200 or first: 500) rather than fetching all products without a limit"
},
{
"name": "notFound() used",
"max_score": 8,
"description": "app/products/[handle]/page.tsx calls notFound() when the product query returns no product"
},
{
"name": "dynamicParams true",
"max_score": 10,
"description": "app/products/[handle]/page.tsx exports 'export const dynamicParams = true' so long-tail products are rendered on demand"
},
{
"name": "Product cache header",
"max_score": 12,
"description": "next.config.ts sets Cache-Control to 'public, s-maxage=60, stale-while-revalidate=600' for '/products/:path*'"
},
{
"name": "API no-store header",
"max_score": 10,
"description": "next.config.ts sets Cache-Control to 'no-store' for '/api/:path*'"
},
{
"name": "Image remote patterns",
"max_score": 12,
"description": "next.config.ts configures images.remotePatterns to include at least one Shopify CDN hostname pattern (e.g. '**.shopify.com')"
}
]
}
Headless Shopify Storefront with Next.js
Problem/Feature Description
A fashion retailer, LookBook Co., is migrating away from a monolithic Shopify theme to a custom headless storefront. The marketing team wants near-perfect Core Web Vitals scores and sub-second page loads so product pages rank well in search. The engineering team has decided on Next.js with the App Router as the framework. They have a Shopify store and want product pages to be statically generated at build time, with automatic background regeneration so pricing and availability stay fresh without requiring a full redeploy.
The catalog has around 12,000 products, but only a few hundred are top sellers. Generating every single page at build time would make deploys prohibitively slow. They also need Next.js to serve product images from Shopify's CDN without errors in production.
Output Specification
Produce the following files as if scaffolding the storefront from scratch. The files should contain working TypeScript code — they don't need to be a complete running app, but they should be structurally correct and importable:
lib/shopify.ts— the Shopify Storefront API client configurationapp/products/[handle]/page.tsx— the product page component with static generation and ISRnext.config.ts— Next.js configuration including CDN cache headers and image domains
All files should be placed directly in the output directory. Do not install packages or run a build — just produce the source files.
{
"context": "Tests whether the agent implements the Zustand cart store with the persist middleware, correct CartItem interface fields, the expected localStorage key, and all required store operations including quantity merging.",
"type": "weighted_checklist",
"checklist": [
{
"name": "zustand imported",
"max_score": 8,
"description": "lib/cart-store.ts imports 'create' from 'zustand'"
},
{
"name": "persist middleware imported",
"max_score": 10,
"description": "lib/cart-store.ts imports 'persist' from 'zustand/middleware' (not from another package)"
},
{
"name": "persist middleware used",
"max_score": 10,
"description": "The store is wrapped with persist() middleware rather than managing localStorage manually"
},
{
"name": "Storage key name",
"max_score": 10,
"description": "The persist configuration uses name: 'cart-storage' as the localStorage key"
},
{
"name": "variantId field",
"max_score": 8,
"description": "The CartItem type/interface includes a 'variantId' field (string)"
},
{
"name": "price and quantity fields",
"max_score": 8,
"description": "The CartItem type/interface includes both 'price' (number) and 'quantity' (number) fields"
},
{
"name": "title and image fields",
"max_score": 8,
"description": "The CartItem type/interface includes both 'title' (string) and 'image' (string) fields"
},
{
"name": "addItem merges quantity",
"max_score": 12,
"description": "addItem checks for an existing item with the same variantId and increments quantity rather than adding a duplicate entry"
},
{
"name": "total() computed",
"max_score": 10,
"description": "The store includes a total() function that computes the sum of price * quantity across all items"
},
{
"name": "removeItem and clearCart",
"max_score": 8,
"description": "The store includes both a removeItem(variantId) operation and a clearCart() operation"
},
{
"name": "updateQuantity operation",
"max_score": 8,
"description": "The store includes an updateQuantity(variantId, quantity) operation"
}
]
}
Shopping Cart State for a Static Storefront
Problem/Feature Description
HomeGoods Direct runs a statically generated Next.js product catalog with hundreds of pre-rendered product pages. Because the pages are static HTML served from a CDN, there is no server session available — the cart needs to live entirely on the client side. Users frequently navigate between pages and even close and reopen their browser while shopping; they expect items they've added to the cart to still be there when they return.
The team has decided to implement a global cart store in TypeScript. The store needs to track items added from different product pages, handle cases where a user adds the same product variant twice (it should increment the quantity rather than duplicate the entry), and persist the cart across browser sessions. The store will be consumed by a cart drawer component rendered as a client island in the layout.
Output Specification
Produce the file lib/cart-store.ts containing the complete cart store implementation in TypeScript.
The file should be self-contained and importable. Include all necessary type definitions inline. Do not install packages or run a build — just produce the source file.
{
"name": "finsi/jamstack-storefront",
"version": "0.1.0",
"summary": "Static-generated storefronts with Next.js/Astro + commerce API backends",
"skills": {
"jamstack-storefront": {
"path": "SKILL.md"
}
}
}