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_KEY

Solution

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

  1. Custom fetch function: The ContentClient accepts a custom fetch option that wraps the native fetch with Next.js caching directives.

  2. Automatic tagging: Each request is tagged based on the query parameters (entry ID, content type) for targeted revalidation.

  3. Draft mode awareness: getDefaultEntryClient automatically disables caching when Next.js draft mode is enabled.

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

Related Resources