Caching with Next.js App Router SDK
Developer preview
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:
| Cache | What it keeps | How it expires |
|---|---|---|
Next.js data cache (fetch) | Route API and Composition API responses, and the Context manifest | Uniform webhooks call revalidateTag and revalidatePath |
| Next.js full route cache (ISR) | The rendered HTML of each code | The same webhooks. Refer to Static generation (ISR). |
| Vercel runtime cache (edge mode only) | The Context manifest and the edge route records | onRevalidateTags: expireVercelRuntimeCacheTags |
The data cache#
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_ENVisdevelopmentortest.
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.
Cache tags#
| Tag | Added to | Example |
|---|---|---|
route | All Route API responses | route |
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' scope | composition:6c4f… |
composition-slug:<slug> | Composition API responses that you get by slug | composition-slug:global-footer |
manifest | The Context manifest | manifest |
The SDK writes all tags in lowercase. It percent-encodes path segments that are not ASCII.
Project map responses have no tags.
Webhooks#
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
Configure the webhook#
- In Uniform, go to Settings > Webhooks.
- Add a webhook with the URL
https://your-site.com/api/preview?secret=your-preview-secret. - Select the events in the table below.
- Save the webhook.
| Event | Revalidated tags | Revalidated paths |
|---|---|---|
composition.published, composition.deleted, composition.changed | composition:<id>, composition-slug:<slug>, and the path: tag of each project map node of the composition | The path of each node |
| The same events for a pattern | Also 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.changed | The composition: tag of each composition that uses the entry, and route if there is one | None |
projectmap.node.insert, projectmap.node.update, projectmap.node.delete | The path: tag of the node (for an update, also of the old path), and composition:<id> of its composition | The path of the node |
redirect.insert, redirect.update, redirect.delete | The path: tag of the source path | The source path |
manifest.published | manifest | None |
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 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.
Secure the webhook#
The handler has two checks:
| Variable | Check |
|---|---|
UNIFORM_PREVIEW_SECRET | When it is set, the secret query string of the webhook URL must have the same value. |
UNIFORM_WEBHOOK_SECRET | When 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.
Expire other caches#
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
If onRevalidateTags fails, the webhook request fails, and Uniform sends it again.
The Vercel runtime cache#
In edge mode on Vercel, vercelUniformEdgeMiddleware keeps two types of data in the Vercel runtime cache:
| Data | Key | Tag |
|---|---|---|
| The published Context manifest | uniform-manifest-<projectId> | manifest |
| The edge route record of each page | uniform-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.
Cache Components#
@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.
Step 1: Enable Cache Components#
next.config.ts
Step 2: Use the cached resolver#
app/uniform/[code]/page.tsx
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.
Streaming with Suspense#
resolveComponent can put a component in a React Suspense boundary. The page shell then renders first, and the slow component streams in later:
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.
Server clients and retries#
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
4xxerror, but they retry408and429.
Manual revalidation#
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:
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").