On-demand static revalidation for App Router apps
On-demand static revalidation (OSR) allows published Sitecore content to refresh in your Next.js App Router application without requiring a full redeploy, using tag-based cache invalidation wired in by default.
OSR is optional. You can adopt it when you use Next.js Cache Components and want publish-driven or tag-driven invalidation. You can also use Cache Components with time-based expiration only, or skip OSR entirely and fetch content dynamically.
The Content SDK now ships two App Router starter templates. OSR is optional in both:
-
nextjs-app-router - the standard template where you can choose your own caching strategy.
-
nextjs-app-router-cache-components - the standard template but with Cache Components and a revalidation route handler out-of-the-box.
Both templates share the same routing ([site]/[locale]/[[...path]]), middleware, SitecoreClient, preview/editing, and i18n support.
Preview and editing always remain dynamic as they call SitecoreClient directly and are never cached. OSR is available only in Content SDK 2.2 or later.
Choosing a caching strategy
You can choose a caching strategy based on your requirements. Listed below are some of the common scenarios:
|
Requirement |
Approach |
|---|---|
|
Content updates within minutes or hours |
Use Cache Compnents with a |
|
Content must appear immediately after publish |
Wire a Sitecore webhook to POST |
|
Invalidation based on an operation or selection |
Call the endpoint manually with a |
|
No caching for published content |
Skip Cache Component helpers entirely and fetch content dynamically via |
Template contents
The cache-components template includes the following out of the box:
|
Area |
Behavior |
|---|---|
|
Cache Components |
Enabled in |
|
Cached reads |
|
|
Revalidation route |
Exposed as POST |
|
Dictionary caching |
SDK in-process dictionary cache disabled in |
Flow of OSR
On-demand static revalidation happens in three phases:
-
Read path - When a visitor first requests a page, the
getSitecorePageorgetSitecoreDictionaryfetches data from Sitecore and stores the result in Next.js's built-in data cache attachingsc:*tags to the results. -
Invalidation path - After a publish, Sitecore fires a webhook that POSTs to
/api/revalidate. The handler,createSitecoreRevalidateRouteHandler, reads the publish payload, resolves whichsc:*tags correspond to the updated items, and calls Next.js's nativerevalidateTagfor each one. -
Next request - Affected cache entries are marked as stale. This is not served to the next visitor and the page fetches fresh data from Sitecore using
getSitecorePageorgetSitecoreDictionaryagain.
Cache tag helpers
The SDK computes tags via public helpers. Cached reads and revalidation share the same tags:
|
Tag |
Purpose |
Example |
|---|---|---|
|
|
URL-level |
|
|
|
Item-level |
|
|
|
Dictionary |
|
-
Pages -
getSitecorePageattaches route and item tags viacollectSitecorePageCacheTags. -
Dictionary -
getSitecoreDictionaryattaches a dictionary tag viabuildSitecoreDictionaryCacheTag. -
Personalization - Variant pages are isolated by URL path. When
PersonalizeProxyidentifies a variant, it rewrites the request to an internal path encoding the variant ID (for example,/default/en/about/_variantId_hero-b). Each unique path gets its own cache entry, so variants never overwrite each other. Thesc:route:…tag attached always uses the clean URL (variant segments are stripped), while thesc:item:…tag is shared across all variants of the same page item. This means a publish webhook invalidatingsc:item:{pageItemId}:en:latestclears all variant cache entries for that page at once. For component A/B testing with multiple simultaneous experiments, each variant combination produces a distinct cached response, all of which are invalidated together via the shared item tag. To invalidate a single specific variant URL explicitly, send its fullsc:route:…tag in thetags[]payload
There is currently no cache tag per variant. Invalidating either the shared sc:item:… tag (typical publish webhook) or the sc:route:… tag (normalized path, variant segments stripped) clears all variant cache entries for that page. The variants are isolated when stored. But when it comes to clearing the cache, you can't pick and choose which ones to invalidate.
Publish webhooks typically invalidate item tags. Route tags are only cleared when the full sc:route:… string is explicitly sent in a revalidate request.
Revalidation endpoint
The Content SDK exposes createSitecoreRevalidateRouteHandler from @sitecorecontent-sdk/nextjs/route-handler. It is a factory that returns a Next.js App Router POST handler. You can mount it to any route, the cache-components template wires it at POST /api/revalidate via src/app/api/revalidate/route.ts. The handler accepts an object of SitecoreRevalidateRouteHandlerOptions.
The following are the parameters required by the revalidation endpoint:
|
Parameter |
Type |
Description |
|---|---|---|
|
|
String Default: |
Shared secret for POST |
|
|
String Default: |
Locale used for item tags when culture is missing, and for dictionary tags when a site has no language configured. |
|
|
Default: |
Next.js |
|
|
Default: |
List of sites (for example, from |
Flow of revalidation
There are two ways in which revalidation can occur:
-
The primary flow is using a Sitecore webhook. Point your Experience Edge/Content Operations webhook at your app URL. The handler processes the standard payload:
-
updates[]— Each publish row's identifier (with-media/-layoutstripped) is mapped tosc:item:{id}:{locale}:latest. Locale is derived fromentity_culture, or falls back to the app's default language. -
Dictionary tags - One
sc:dict:{site}:{locale}tag per site from.sitecore/sites.jsonis revalidated on every call.
-
-
You can also revalidate without webhooks using a
tagspayload that the endpoint accepts.tags[] value
Behavior
Starts with
sc:Revalidated as-is
Bare item ID
Mapped to
sc:item:{id}:{defaultLocale}:latest