Caching with Next.js App Router SDK

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

Developer Preview

This page documents the developer preview of the SDK, version 20.81.1-alpha.25.sha-f076f9b857. In this version, the middleware does not resolve routes, so the middleware runtime cache of the stable SDK is removed.

The SDK caches data in three places:

CacheWhat it keepsHow it expires
Next.js data cache (fetch)Route API and Composition API responses, and the Context manifestUniform webhooks call revalidateTag and revalidatePath
Next.js full route cache (ISR)The rendered HTML of each codeThe same webhooks. Refer to Static generation (ISR).
Vercel runtime cache (edge mode only)The Context manifest and the edge route recordsonRevalidateTags: expireVercelRuntimeCacheTags

The resolvers fetch published content with cache: "force-cache". Next.js keeps the response until a webhook revalidates its tag.

The resolvers of @uniformdev/next-app-router do not use the cache in these cases:

  • The page state is draft or editor (preview in Canvas).
  • NODE_ENV is development or test.

In these cases, the resolvers fetch with cache: "no-cache" and send the x-bypass-cache: true header to Uniform.

note

UniformContext gets the published Context manifest with force-cache in all environments, also under next dev. To see a new manifest in development, do a hard reload of the page in the browser. You can also delete the .next folder, or send the manifest.published webhook to your local server.

TagAdded toExample
routeAll Route API responsesroute
path:<path>Route API responses, one tag for each prefix of the path/authors/alex gives path:/, path:/authors and path:/authors/alex
composition:<id>Composition API responses, and route responses in a 'use cache' scopecomposition:6c4f…
composition-slug:<slug>Composition API responses that you get by slugcomposition-slug:global-footer
manifestThe Context manifestmanifest

The SDK writes all tags in lowercase. It percent-encodes path segments that are not ASCII.

Project map responses have no tags.

The POST handler of app/api/preview/route.ts receives Uniform webhooks, and revalidates the tags and paths of the changed content:

app/api/preview/route.ts

import { createPreviewPOSTRouteHandler } from "@uniformdev/next-app-router/handler"; export const POST = createPreviewPOSTRouteHandler();
  1. In Uniform, go to Settings > Webhooks.
  2. Add a webhook with the URL https://your-site.com/api/preview?secret=your-preview-secret.
  3. Select the events in the table below.
  4. Save the webhook.
EventRevalidated tagsRevalidated paths
composition.published, composition.deleted, composition.changedcomposition:<id>, composition-slug:<slug>, and the path: tag of each project map node of the compositionThe path of each node
The same events for a patternAlso route, and the composition: tag of each composition that uses the pattern. For a deleted pattern, the handler cannot find these compositions.The path of each node
entry.published, entry.deleted, entry.changedThe composition: tag of each composition that uses the entry, and route if there is oneNone
projectmap.node.insert, projectmap.node.update, projectmap.node.deleteThe path: tag of the node (for an update, also of the old path), and composition:<id> of its compositionThe path of the node
redirect.insert, redirect.update, redirect.deleteThe path: tag of the source pathThe source path
manifest.publishedmanifestNone

For a path with a dynamic segment, for example /products/:slug, the handler uses the part before the first dynamic segment: /products.

The handler finds the compositions that use a pattern or an entry through the relationships API. It follows a maximum of 5 levels, and makes a maximum of 50 requests. When it stops at a limit, or when the lookup fails, it also revalidates the route tag. Some lookup errors go away on a retry. For these errors, the handler answers 503, so Uniform sends the webhook again. Other errors in the handler give a 500 response.

The handler answers with JSON:

{ "handled": true, "tags": ["composition:6c4f…", "path:/about"], "paths": ["/about"] }

handled is false for an event that the handler does not use, for example release.launched.

warning

The *.changed events occur each time an author saves a draft. Each event revalidates the cache, but the published content does not change. Select composition.changed and entry.changed only if you need them.

The handler has two checks:

VariableCheck
UNIFORM_PREVIEW_SECRETWhen it is set, the secret query string of the webhook URL must have the same value.
UNIFORM_WEBHOOK_SECRETWhen it is set, the handler verifies the Svix signature of the request. The signing secret is in the webhook settings in Uniform.

The request must always have the svix-id, svix-timestamp and svix-signature headers. A failed check gives a 401 response.

warning

If you do not set the two variables, the handler accepts all requests that have the Svix headers. It only writes a message to the log. Set at least one of the two variables in production. For the best security, set both.

onRevalidateTags gets the list of tags after revalidateTag. Use it to expire the same tags in caches that Next.js does not manage:

app/api/preview/route.ts

import { createPreviewPOSTRouteHandler } from "@uniformdev/next-app-router/handler"; import { expireVercelRuntimeCacheTags } from "@uniformdev/next-app-router/vercel"; export const POST = createPreviewPOSTRouteHandler({ onRevalidateTags: expireVercelRuntimeCacheTags });

If onRevalidateTags fails, the webhook request fails, and Uniform sends it again.

In edge mode on Vercel, vercelUniformEdgeMiddleware keeps two types of data in the Vercel runtime cache:

DataKeyTag
The published Context manifestuniform-manifest-<projectId>manifest
The edge route record of each pageuniform-edge-route-<projectId>-<locale>-<routePath>path:<routePath>

The two stay until their tag expires. Give expireVercelRuntimeCacheTags to onRevalidateTags (above), so the webhooks expire them. Refer to Edge mode execution.

note

A webhook for a dynamic project map node, for example /products/:slug, expires only path:/products. The record of a page has the tag of its full path, for example path:/products/shoe. Thus the records of the pages under a dynamic node stay until the cache removes them.

@uniformdev/next-app-router/cache exports four resolvers with the 'use cache' directive: resolveRouteFromCode, resolveRouteFromPath, resolveCompositionById and resolveCompositionBySlug. They have the same signatures as the resolvers of @uniformdev/next-app-router.

next.config.ts

import { withUniformConfig } from "@uniformdev/next-app-router/config"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { cacheComponents: true, }; export default withUniformConfig(nextConfig);

app/uniform/[code]/page.tsx

import { createUniformStaticParams, UniformComposition, type UniformPageParameters, } from "@uniformdev/next-app-router"; import { resolveRouteFromCode } from "@uniformdev/next-app-router/cache"; import { resolveComponent } from "@/components/resolveComponent"; // With Cache Components, generateStaticParams must return at least one code. // Use a path that has a published composition. export const generateStaticParams = () => createUniformStaticParams({ paths: ["/"] }); export default async function UniformPage(props: UniformPageParameters) { const { code } = await props.params; return ( <UniformComposition code={code} resolveRoute={resolveRouteFromCode} resolveComponent={resolveComponent} /> ); }

The cached resolvers work as follows:

  • Only the published state uses the 'use cache' scope. Draft and editor requests call the resolver without the cache.
  • Under next dev, the 'use cache' scope still keeps published results. Only the fetches in the scope skip the cache.
  • The scope has the cache tags of the route, and the composition: tag of the composition that it found. Thus the webhooks revalidate it.
  • In the scope, the fetches do not use the Next.js data cache, so Next.js does not keep the data two times.
  • The SDK does not call cacheLife. The default cache profile of Next.js applies.

warning

With Cache Components, Next.js does not accept an empty generateStaticParams, and the build fails. Return at least one code. Refer to the Next.js message about empty generateStaticParams.

resolveComponent can put a component in a React Suspense boundary. The page shell then renders first, and the slow component streams in later:

export const resolveComponent: ResolveComponentFunction = ({ component }) => { if (component.type === "productList") { return { component: ProductList, suspense: { fallback: () => <div className="h-64 animate-pulse bg-gray-200" /> }, }; } return { component: componentMap[component.type] ?? NotFound }; };

The SDK does not put the full composition in a Suspense boundary. In Canvas, visual editing refreshes the page. A new boundary then shows its empty fallback each time.

The server clients have these limits for each client instance:

  • A maximum of 6 requests at the same time
  • A maximum of 10 new requests each second
  • 1 retry after a failure, after a delay of 1 second. The clients do not retry a 4xx error, but they retry 408 and 429.

To revalidate a page manually, revalidate its cache tag. The cached page uses the fetch tags of its route, so the tag expires the page too:

import { revalidateTag } from "next/cache"; revalidateTag("path:/about", "max");

Do not use revalidatePath with the path of the visitor. The middleware rewrites the request to /uniform/[code], and with a rewrite, revalidatePath must get the destination route. Refer to revalidatePath with rewrites. To revalidate all Uniform pages, use revalidatePath("/uniform/[code]", "page").