# Caching with Next.js App Router SDK

> How the Next.js App Router SDK caches routes, compositions and the Context manifest, which cache tags it uses, and how Uniform webhooks revalidate them.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/caching

> **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)](https://docs.uniform.app/docs/sdk/nextjs-app-router/incremental-static-regeneration-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_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.

## 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`

```ts
import { createPreviewPOSTRouteHandler } from "@uniformdev/next-app-router/handler";

export const POST = createPreviewPOSTRouteHandler();
```

### Configure the webhook

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.

| 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:

```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.

### 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](https://www.svix.com/) 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`

```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.

## 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](https://docs.uniform.app/docs/sdk/nextjs-app-router/edge-mode).

> **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`

```ts
import { withUniformConfig } from "@uniformdev/next-app-router/config";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default withUniformConfig(nextConfig);
```

### Step 2: Use the cached resolver

`app/uniform/[code]/page.tsx`

```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](https://nextjs.org/docs/messages/empty-generate-static-params).

### 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:

```tsx
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.

## Server clients and retries

The [server clients](https://docs.uniform.app/docs/sdk/nextjs-app-router/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`.

## 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:

```tsx
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](https://nextjs.org/docs/app/api-reference/functions/revalidatePath#using-revalidatepath-with-rewrites). To revalidate all Uniform pages, use `revalidatePath("/uniform/[code]", "page")`.
