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