caching-strategy

Reference: Next.js Cache Components — the official documentation for use cache , cacheLife , cacheTag , and Partial Prerendering.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "caching-strategy" with this command: npx skills add saleor/storefront/saleor-storefront-caching-strategy

Caching Strategy

Reference: Next.js Cache Components — the official documentation for use cache , cacheLife , cacheTag , and Partial Prerendering.

When to Use

Use this skill when:

  • Understanding which data is cached vs. real-time

  • Debugging why content isn't updating after Dashboard changes

  • Configuring Saleor webhooks for cache invalidation

  • Modifying the /api/revalidate endpoint

  • Working with "use cache" and Suspense patterns

Data Freshness Model

The Key Principle

Display pages are cached for performance. Transactional flows are always real-time.

Page/Component Data Source Freshness Why

PDP (Product Detail) getProductData()

⚠️ Cached (5 min TTL) Performance - instant loads

Category/Collection pages getCategoryData() / getCollectionData()

⚠️ Cached (5 min TTL) Performance

Homepage getFeaturedProducts()

⚠️ Cached (5 min TTL) Performance

Navigation NavLinks

⚠️ Cached (1 hour TTL) Rarely changes

Cart Drawer Checkout.find()

✅ Always fresh Uses cache: "no-cache"

Checkout Page useCheckoutQuery()

✅ Always fresh Direct API call via urql

Add to Cart action Saleor mutation ✅ Always fresh Saleor calculates price

Price Flow Diagram

┌─────────────────────────────────────────────────────────────────────┐ │ PRICE FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ PDP Display Cart/Checkout Payment │ │ ──────────── ───────────── ─────── │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Cached │───────▶│ FRESH │────────▶│ FRESH │ │ │ │ $29.99 │ Add │ $35.99 │ Pay │ $35.99 │ │ │ └───────────┘ to └───────────┘ └───────────┘ │ │ Cart │ │ "use cache" cache:"no-cache" Saleor validates │ │ 5 min TTL Always from API at checkout │ │ │ └─────────────────────────────────────────────────────────────────────┘

⚠️ User may see different price in cart than on PDP if price changed. ✅ User CANNOT checkout at stale price - Saleor always uses current price.

Why This Is Safe

  • Saleor is the source of truth: When you call checkoutLinesAdd , Saleor calculates the price server-side using current data

  • Cart always fetches fresh: Checkout.find() uses cache: "no-cache"

  • Checkout validates: checkoutComplete will fail if something is wrong

  • Webhooks enable instant updates: When configured, price changes trigger immediate cache invalidation

Cache Components Architecture

What It Is

Cache Components enable Partial Prerendering (PPR) - mixing static, cached, and dynamic content in a single route. The static shell is served instantly from CDN, while dynamic parts stream in via Suspense.

Current Status: ✅ ENABLED (Experimental)

⚠️ Note: Cache Components are still marked experimental in Next.js. The patterns are functional but evolving. See Disabling Cache Components if you need to rollback.

Cache Components are enabled in next.config.js :

const config = { cacheComponents: true, };

How It Works

┌─────────────────────────────────────────────────────────────────┐ │ STATIC SHELL (Instant from CDN) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Header skeleton, layout, cached product data │ │ │ │ Source: "use cache" functions (getProductData, etc.) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ <Suspense fallback={<Skeleton />}> │ │ │ │ Dynamic content (streams in after initial render) │ │ │ │ - Variant selection (reads searchParams) │ │ │ │ - Logo, NavLinks (use usePathname) │ │ │ │ - Cart count (reads cookies) │ │ │ │ </Suspense> │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘

Cached Functions with Tags

Each cached function has a tag for targeted invalidation:

// src/app/[channel]/(main)/products/[slug]/page.tsx async function getProductData(slug: string, channel: string) { "use cache"; cacheLife("minutes"); // 5 min default TTL cacheTag(product:${slug}); // Tag for webhook invalidation

return executePublicGraphQL(ProductDetailsDocument, {
	variables: { slug, channel },
});

}

Tag Registry

Tag Pattern Used By Invalidated When

product:{slug}

getProductData()

Product updated in Saleor

category:{slug}

getCategoryData()

Category updated

collection:{slug}

getCollectionData() , getFeaturedProducts()

Collection updated

navigation

NavLinks

Menu structure changed

Key Patterns

  1. Suspense Around Dynamic Content

Any component accessing runtime data must be wrapped in Suspense.

What counts as "dynamic data" (triggers Suspense requirement):

Data Access Why It's Dynamic

cookies()

Per-request

headers()

Per-request

searchParams

URL-dependent

usePathname()

Client-side routing

useParams()

Client-side routing

Date.now()

Time-dependent

Server Actions Form submissions

cache: "no-cache" fetches Always fresh

// Layout wraps children in Suspense <main className="flex-1"> <Suspense>{props.children}</Suspense> </main>

// Header wraps NavLinks in Suspense (uses usePathname for active state) <Suspense fallback={<NavLinksSkeleton />}> <NavLinks channel={channel} /> </Suspense>

  1. Public vs Authenticated Queries

Two explicit GraphQL helpers:

  • executePublicGraphQL

  • Safe inside "use cache" (no cookies needed)

  • executeAuthenticatedGraphQL

  • NOT safe inside "use cache" (requires cookies)

import { executePublicGraphQL, executeAuthenticatedGraphQL } from "@/lib/graphql";

// ✅ Public data - safe inside "use cache" async function getProductData(slug: string, channel: string) { "use cache"; return executePublicGraphQL(ProductDetailsDocument, { variables: { slug, channel }, }); }

// ✅ User data - NOT inside "use cache" (requires cookies) const { me } = await executeAuthenticatedGraphQL(CurrentUserDocument, { cache: "no-cache", });

  1. Don't Use searchParams Inside "use cache"

// ❌ BAD - searchParams is runtime data export async function generateMetadata(props) { "use cache"; const searchParams = await props.searchParams; // Error! }

// ✅ GOOD - Only access params (becomes cache key) export async function generateMetadata(props) { "use cache"; const params = await props.params; // OK }

// ✅ GOOD - Access searchParams outside cache scope export async function generateMetadata(props) { const searchParams = await props.searchParams; // No "use cache" }

  1. CSS Order Pattern for Mixed Static/Dynamic Layouts

When you need dynamic content to appear above static content visually, use CSS order :

// PDP: Category (dynamic) appears above Product Name (static) <div className="flex flex-col gap-3"> {/* Static shell - renders first but order:2 */} <h1 className="order-2">{product.name}</h1>

{/* Dynamic - streams in, order:1 appears above h1 */}
&#x3C;Suspense fallback={&#x3C;Skeleton className="order-1" />}>
	&#x3C;VariantSection /> {/* Contains order-1 and order-3 elements */}
&#x3C;/Suspense>

{/* Static - order:4 appears last */}
&#x3C;div className="order-4">
	&#x3C;ProductAttributes />
&#x3C;/div>

</div>

Visual result:

  1. Category + Sale badge (dynamic, order-1)
  2. Product Name (static, order-2)
  3. Variant selectors (dynamic, order-3)
  4. Product details (static, order-4)

This keeps <h1> in the static shell for SEO while allowing dynamic content to appear above it.

  1. GraphQL Auth Defaults

Two explicit GraphQL helpers ensure you always know what data access level you're using:

  • executePublicGraphQL

  • Public queries only (products, menus, categories)

  • executeAuthenticatedGraphQL

  • Requires user session cookies (checkout, user data)

This ensures:

  • Only publicly visible products are fetched

  • No user cookies in cache scope (safe for "use cache" )

  • No "Signature has expired" errors on public pages

import { executePublicGraphQL, executeAuthenticatedGraphQL } from "@/lib/graphql";

// ✅ Public data (menus, products) - no auth, only public data const menu = await executePublicGraphQL(MenuDocument, { variables: { slug: "footer" }, });

// ✅ User data - requires session cookies let user = null; try { const result = await executeAuthenticatedGraphQL(CurrentUserDocument, { cache: "no-cache", }); user = result.me; } catch { // Expired token = treat as not logged in }

// ✅ Checkout/cart - requires session cookies await executeAuthenticatedGraphQL(CheckoutAddLineDocument, { variables: { id: checkoutId, productVariantId: variantId }, cache: "no-cache", });

// ✅ App token (server-side only) - explicit header const channels = await executePublicGraphQL(ChannelsListDocument, { headers: { Authorization: Bearer ${process.env.SALEOR_APP_TOKEN}, }, });

Cache Invalidation

Automatic via Webhooks (Recommended)

When configured, Saleor sends webhooks on data changes, triggering instant invalidation.

Setup in Saleor Dashboard:

  • Go to Configuration → Webhooks

  • Create webhook pointing to: https://your-site.com/api/revalidate

  • Subscribe to events:

  • PRODUCT_CREATED , PRODUCT_UPDATED , PRODUCT_DELETED

  • CATEGORY_CREATED , CATEGORY_UPDATED , CATEGORY_DELETED

  • COLLECTION_CREATED , COLLECTION_UPDATED , COLLECTION_DELETED

  • Copy the secret key to SALEOR_WEBHOOK_SECRET env var

What happens on webhook:

// Product update webhook triggers: revalidateTag(product:${slug}, "minutes"); // Invalidates "use cache" data revalidatePath(/channel/products/${slug}); // Invalidates ISR page

Manual Invalidation

Invalidate a specific product (both tag and path)

curl "https://store.com/api/revalidate?secret=xxx&#x26;tag=product:blue-hoodie&#x26;path=/default-channel/products/blue-hoodie"

Invalidate just the cached function data

curl "https://store.com/api/revalidate?secret=xxx&#x26;tag=product:blue-hoodie"

Invalidate navigation (uses "hours" profile)

curl "https://store.com/api/revalidate?secret=xxx&#x26;tag=navigation&#x26;profile=hours"

No Webhooks? TTL Takes Over

Data Default TTL

Products 5 minutes

Categories 5 minutes

Collections 5 minutes

Navigation 1 hour

Environment Variables

Cache invalidation

REVALIDATE_SECRET=your-secret # Manual revalidation (GET requests) SALEOR_WEBHOOK_SECRET=webhook-hmac # Saleor webhook HMAC verification

Debugging Stale Content

Checklist

Is the webhook configured?

  • Check Saleor Dashboard → Webhooks → Deliveries

Did the webhook fire?

  • Check server logs for [Revalidate] entries

Is the tag correct?

  • Product slugs must match exactly: product:blue-hoodie

Force manual revalidation:

curl "https://store.com/api/revalidate?secret=xxx&#x26;tag=product:my-product"

Check browser cache:

  • Hard refresh: Cmd+Shift+R / Ctrl+Shift+R

Anti-patterns

❌ Don't use cache: "no-cache" for display pages - Destroys performance

❌ Don't skip webhook setup in production - Users see stale prices

❌ Don't access cookies/searchParams inside "use cache"

  • Will error

❌ Don't use executeAuthenticatedGraphQL inside "use cache"

  • Requires cookies

❌ Don't expose REVALIDATE_SECRET

  • Keep it server-side only

Disabling Cache Components

If you need to rollback to standard ISR caching:

Step 1: Disable in Config

// next.config.js const config = { cacheComponents: false, // or comment out entirely };

Step 2: Remove Cache Directives

Remove "use cache" , cacheLife() , and cacheTag() from these files:

File What to Remove

src/app/[channel]/(main)/products/[slug]/page.tsx

getProductData() cache directives

src/app/[channel]/(main)/categories/[slug]/page.tsx

getCategoryData() cache directives

src/app/[channel]/(main)/collections/[slug]/page.tsx

getCollectionData() cache directives

src/app/[channel]/(main)/page.tsx

getFeaturedProducts() cache directives

src/ui/components/nav/components/nav-links.tsx

Navigation cache directives

Step 3: Update Revalidation

// src/app/api/revalidate/route.ts // Change from: revalidateTag(product:${slug}, "minutes"); // To: revalidateTag(product:${slug}); // Remove second argument

What You Can Keep

  • Suspense boundaries - Still useful for loading states

  • CSS order layout - Pure CSS, no impact

  • executeAuthenticatedGraphQL

  • Good separation regardless

  • ISR via revalidate option - Works as fallback

Files Reference

File Purpose

src/app/api/revalidate/route.ts

Webhook endpoint and manual revalidation

src/app/[channel]/(main)/products/[slug]/page.tsx

PDP with "use cache"

src/app/[channel]/(main)/categories/[slug]/page.tsx

Category with "use cache"

src/app/[channel]/(main)/collections/[slug]/page.tsx

Collection with "use cache"

src/app/[channel]/(main)/page.tsx

Homepage with "use cache"

src/ui/components/pdp/variant-section-dynamic.tsx

Dynamic variant section

src/ui/components/header.tsx

Header with Suspense boundaries

src/lib/checkout.ts

Cart operations (always fresh)

next.config.js

cacheComponents: true

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

filtering-system

No summary provided by upstream source.

Repository SourceNeeds Review
General

checkout-management

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-turbopack

No summary provided by upstream source.

Repository SourceNeeds Review