Configuring Next.js App Router Caching for Uniform Entries
Last updated: December 24, 2025
Overview
When using Uniform Content entries with Next.js App Router, you may want to leverage Next.js's built-in data caching to reduce API calls and improve performance. While @uniformdev/canvas-next-rsc provides caching for compositions via canvasCache, there is no built-in entriesCache configuration for the Content API.
This article shows how to create a custom entry client wrapper that integrates with Next.js App Router's caching system.
Prerequisites
Next.js 14+ with App Router
@uniformdev/canvas package installed
Environment variables configured:
UNIFORM_PROJECT_ID
UNIFORM_API_KEYSolution
Create a custom ContentClient wrapper that uses Next.js's fetch caching options.
Step 1: Create the Entry Client
Create a new file at lib/uniform/entryClient.ts:
import { ContentClient } from '@uniformdev/canvas';
import { draftMode } from 'next/headers';
/**
* Cache configuration options
*/
export type EntryCacheConfig =
| { type: 'no-cache' }
| { type: 'force-cache' }
| { type: 'revalidate'; interval: number };
export type GetEntryClientOptions = {
cache?: EntryCacheConfig;
/** Bypass the edge SWR cache */
disableSWR?: boolean;
/** Bypass the edge cache entirely */
bypassCache?: boolean;
};
/**
* Creates a ContentClient with Next.js App Router data caching.
*/
export function getEntryClient(options: GetEntryClientOptions = {}) {
const {
cache = { type: 'force-cache' },
disableSWR = true,
bypassCache = false
} = options;
return new ContentClient({
projectId: process.env.UNIFORM_PROJECT_ID!,
apiKey: process.env.UNIFORM_API_KEY!,
apiHost: process.env.UNIFORM_CLI_BASE_URL || 'https://uniform.app',
edgeApiHost: process.env.UNIFORM_CLI_BASE_EDGE_URL || 'https://uniform.global',
disableSWR,
fetch: (req, init) => {
const tags = extractCacheTags(req);
const fetchOptions = determineFetchOptions(cache);
return fetch(req, {
...init,
cache: fetchOptions.cache,
headers: {
...init?.headers,
'x-bypass-cache': bypassCache.toString(),
},
next: {
revalidate: fetchOptions.revalidate,
tags: tags.length > 0 ? tags : undefined,
},
});
},
});
}
/**
* Creates an entry client with automatic cache strategy based on environment.
* - Draft mode: no-cache with bypass
* - Development: no-cache
* - Production: force-cache (or custom config)
*/
export async function getDefaultEntryClient(productionCache?: EntryCacheConfig) {
const { isEnabled: isDraft } = await draftMode();
const isDev = process.env.NODE_ENV === 'development';
if (isDraft) {
return getEntryClient({
cache: { type: 'no-cache' },
bypassCache: true,
});
}
if (isDev) {
return getEntryClient({
cache: { type: 'no-cache' },
});
}
return getEntryClient({
cache: productionCache ?? { type: 'force-cache' },
});
}
function extractCacheTags(req: RequestInfo | URL): string[] {
const tags: string[] = ['uniform-entries'];
let url: URL;
if (typeof req === 'string') {
url = new URL(req);
} else if (req instanceof URL) {
url = req;
} else {
url = new URL(req.url);
}
const entryId = url.searchParams.get('entryId');
if (entryId) {
tags.push(`entry:${entryId}`);
}
const entryIds = url.searchParams.get('entryIds');
if (entryIds) {
entryIds.split(',').forEach((id) => tags.push(`entry:${id}`));
}
const contentType = url.searchParams.get('type');
if (contentType) {
tags.push(`content-type:${contentType}`);
}
return tags;
}
function determineFetchOptions(cache: EntryCacheConfig): {
cache: RequestCache | undefined;
revalidate: number | undefined;
} {
if (cache.type === 'revalidate') {
return { cache: undefined, revalidate: cache.interval };
}
return { cache: cache.type, revalidate: undefined };
}Step 2: Basic Usage
Use the client in your Server Components:
// app/blog/page.tsx
import { getDefaultEntryClient } from '@/lib/uniform/entryClient';
export default async function BlogPage() {
const client = await getDefaultEntryClient();
const { entries } = await client.getEntries({
type: 'blogPost',
state: 64, // published
limit: 10,
});
return (
<ul>
{entries.map((entry) => (
<li key={entry.entry._id}>{entry.entry._name}</li>
))}
</ul>
);
}Cache Configuration Options
Force Cache (Default)
Caches indefinitely until manually revalidated:
const client = getEntryClient({
cache: { type: 'force-cache' },
});Time-Based Revalidation
Automatically revalidates after a specified interval:
const client = getEntryClient({
cache: { type: 'revalidate', interval: 60 }, // seconds
});No Cache
Always fetches fresh data:
const client = getEntryClient({
cache: { type: 'no-cache' },
});On-Demand Revalidation
The client automatically tags requests for targeted cache invalidation. Create a route handler to trigger revalidation:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { secret, entryId, contentType } = await request.json();
// Validate webhook secret
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
// Revalidate specific entry
if (entryId) {
revalidateTag(`entry:${entryId}`);
}
// Revalidate all entries of a content type
if (contentType) {
revalidateTag(`content-type:${contentType}`);
}
// Revalidate all entries
// revalidateTag('uniform-entries');
return NextResponse.json({ revalidated: true });
}Available Cache Tags
Tag Pattern | Description |
uniform-entries | All entry requests |
entry:{id} | Specific entry by ID |
content-type:{type} | All entries of a content type |
How It Works
Custom fetch function: The ContentClient accepts a custom fetch option that wraps the native fetch with Next.js caching directives.
Automatic tagging: Each request is tagged based on the query parameters (entry ID, content type) for targeted revalidation.
Draft mode awareness: getDefaultEntryClient automatically disables caching when Next.js draft mode is enabled.
Edge cache control: The bypassCache and disableSWR options control Uniform's edge caching layer independently from Next.js caching.
Comparison with canvasCache
Feature | canvasCache | entryCache |
Built into SDK | ✅ | ❌ (custom code) |
Next.js data cache | ✅ | ✅ |
Cache tags | Composition IDs | Entry IDs, Content Types |
Draft mode support | ✅ | ✅ |
Time-based revalidation | ✅ | ✅ |