Help us improve
Share bugs, ideas, or general feedback.
From contentful
Make Contentful-driven sites fast and stay fast — pick between Delivery and Preview correctly, use the Sync API for incremental hydration, exploit the Images API for responsive imagery, design CDN cache headers and Next.js revalidation tags together, and avoid the cold-cache cliffs that show up on launch day. Use this skill any time Core Web Vitals are at issue, when Contentful queries are showing up in performance traces, when image weight is killing LCP, when a search index needs incremental sync, when Vercel cache is fighting Contentful's CDN, or when the team is debating "static export vs. ISR vs. on-demand revalidation." Trigger on any "this is slow" question that touches Contentful.
npx claudepluginhub bpainter/composable-dxp-claude-marketplace --plugin contentfulHow this skill is triggered — by the user, by Claude, or both
Slash command
/contentful:contentful-delivery-optimizationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Performance is a contract between Contentful's CDN, your Next.js cache, the browser, and the editor's mental model of "publish should be fast." This skill owns that contract.
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
Performance is a contract between Contentful's CDN, your Next.js cache, the browser, and the editor's mental model of "publish should be fast." This skill owns that contract.
Pair with contentful-graphql (query shape is the upstream variable), contentful-react-wrapper (where revalidation tags live), contentful-webhooks (the event source), contentful-rich-text (image-heavy bodies), and contentful-localization (locale-keyed cache fragmentation).
next/image together.Two read APIs, two purposes:
| Delivery | Preview | |
|---|---|---|
| Reads | Published content only | Drafts + published |
| Token | CONTENTFUL_DELIVERY_TOKEN | CONTENTFUL_PREVIEW_TOKEN |
| Host | cdn.contentful.com (REST) / GraphQL via standard endpoint | preview.contentful.com / GraphQL with preview: true |
| CDN cache | Aggressive | Minimal |
| Rate limit | ~78 rps | Lower |
| Use for | Production | Editor preview routes only |
Production is Delivery only. Preview never leaks. The wrapper layer chooses per-request based on draftMode() (Next.js App Router); see contentful-react-wrapper.
The stack from cold to hot:
Browser
↓
Edge cache (Vercel / Cloudflare)
↓
App-level cache (Next.js fetch cache, "force-cache" / tags / revalidate)
↓
Contentful CDN cache (cdn.contentful.com)
↓
Contentful origin
Each layer has its own invalidation. The trick is to structure them so an event invalidates only the layers that need it.
Three primary controls:
fetch(url, {
next: {
revalidate: 3600, // safety-net TTL
tags: ["page:home", "hero:abc123"], // on-demand invalidation
},
cache: "force-cache" | "no-store",
});
Practical pattern:
revalidate: 3600 as the floor. If a webhook is dropped, content goes stale for at most an hour.tags are the actual mechanism. Each fetch tags itself with both an aggregate (hero) and a specific (hero:${id}) tag. The webhook handler revalidates the matching tags (see contentful-webhooks).cache: "no-store" in preview mode. Editors expect every page to reflect latest draft state.The CDA / GraphQL response is CDN-cached based on URL (and authorization). Same query → same cache key. Implications:
Standard Cache-Control semantics apply to your front-end responses. s-maxage for the edge, max-age for the browser, stale-while-revalidate for graceful serving.
Editor publishes in Contentful
│
▼
Webhook fires (signed, environment-filtered) — see contentful-webhooks
│
▼
Handler in Next.js: revalidateTag("article:abc"); revalidateTag("article")
│
▼
Next request to the affected page misses the Next.js cache, refetches from
Contentful, repopulates the cache, returns fresh content.
Tag design:
| Tag pattern | What it covers |
|---|---|
${contentType}:${id} | a single specific entry — revalidates pages that read that entry |
${contentType} | all entries of a type — revalidates listing pages |
page:${slug} | a specific assembled page — revalidates that page even if the trigger isn't an entry on it |
nav | global nav fetched on every page — revalidates all of <layout> |
Tag every fetch with the most specific tag that's accurate. Aggregate tags are cheap on the read; expensive on the invalidate (everything matching is recomputed).
revalidate: 3600 catches it. The page is stale for up to an hour. For business-critical content (homepage, top product), reduce to revalidate: 600 or 300. Don't go below that without a reason; you're paying real origin cost.
The Sync API gives you a nextSyncToken after an initial pull and returns deltas on subsequent calls. Reach for it when:
Pattern:
import { createClient } from "contentful";
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
environment: process.env.CONTENTFUL_ENVIRONMENT ?? "master",
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN!,
});
// Initial sync (one time per consumer, expensive)
let { entries, deletedEntries, nextSyncToken } = await client.sync({ initial: true });
saveTokenSomewhere(nextSyncToken);
indexAll(entries);
// Subsequent (cheap, run on a cadence or on webhook)
({ entries, deletedEntries, nextSyncToken } = await client.sync({ nextSyncToken: loadTokenSomewhere() }));
saveTokenSomewhere(nextSyncToken);
indexUpdates(entries);
removeDeletions(deletedEntries);
Implications:
Contentful asset URLs accept transformation parameters. The CDN caches per parameter set, so a sane responsive-image strategy is essentially free at scale.
Key parameters:
?w=1200, ?h=600 (proportions preserved by default)?fit=fill | pad | scale | crop | thumb?f=face | center | top | left | ... (focal point on crop)?fm=webp | avif | jpg | png — usually let auto choose via ?fm=avif fallback or rely on next/image to set the right one?q=80 (1–100; default 75)?bg=rgb:f0f0f0 (for fit=pad)?r=12Pattern with next/image:
<Image
src={asset.url} // Contentful asset URL
alt={asset.description ?? ""}
width={asset.width}
height={asset.height}
sizes="(min-width: 1024px) 50vw, 100vw"
// Next will append its own width param via the loader; you can also pre-bake quality / format
/>
For art-directed crops on hero images, set the focal point via the Images API parameters or via the Contentful focal-point UI on the asset. Pass through to next/image.
For above-the-fold imagery, set priority on the Image and let next/image's preload handle LCP.
Some patterns hurt CDN warmth:
?include=10). Single response is bigger; CDN payload is heavier; first-byte slower.where: { publishedAt_gte: <now-7d> } keeps shifting cache keys. Pin to discrete buckets if possible (gte: <last week's monday>).Patterns that help:
The first 24 hours after launch are pathological because:
Mitigations:
dynamicParams = true and generateStaticParams for known slugs so the first request is a build-cache hit.revalidate for the first day, then raise it.| Pattern | When |
|---|---|
Pure static (generateStaticParams + no revalidate) | Marketing pages with rare updates, when build-time hydration is fine |
| ISR (time-based) | Listings that need to feel fresh but don't need second-by-second |
| On-demand revalidation (tag-based) | The default for Composable DXP work — webhook-driven cache invalidation |
Server-rendered (no-store or revalidate: 0) | Personalized pages, dashboards, editor preview |
Slalom default is on-demand revalidation with a 1h floor. ISR is the fallback when the webhook surface is messy; server-rendered is for personalization and preview. Pure static is rare in modern composable work.
What to instrument:
Surface in a single dashboard so the team can spot regressions (cache fragmentation from a new locale, image weight from a new art-directed asset, query complexity from a new fragment).
cache: "force-cache" left in for the preview path. Editors lose minds. Always opt out in preview.revalidateTag("article") on every article publish triggers the rebuild of every page that reads any article. Use specific tags for hot pages.article:abc invalidates fetches across all locales. Sometimes correct; sometimes you want article:abc:fr-FR. Be deliberate.revalidate: 3600 only fires on next request after expiry. Low-traffic pages stay stale for days. Pair with explicit invalidation.contentful-graphql.contentful-react-wrapper.contentful-webhooks.contentful-rich-text.contentful-localization.contentful-personalization.software-engineering-nextjs-scaffold.../../references/api-surface.md