Caching strategies for Next.js and Apollo Client
This article gives you insight into caching strategies and our recommendation when connecting your Next.js web app with Prepr CMS using Apollo Client.
Introduction
Caching is the process of storing copies of data in a temporary storage layer so that future requests for that data can be served faster. Instead of fetching data from the CMS every time, cached data can be retrieved quickly from a nearby location like memory, a server, or a CDN.
Caching layers
When connecting your Next.js front end to Prepr CMS using Apollo Client, caching happens at several layers: : Next.js, Apollo Client, and the Prepr CDN.
Next.js
How Next.js caches your Prepr data depends on which caching model your app uses. Whichever model you use, the client-side Router Cache keeps visited layouts, loading states, and pages in the browser so that back and forward navigation is instant.
Cache Components (Next.js 16)
Starting with Next.js 16, the recommended way to handle caching is Cache Components, which you enable with the cacheComponents option in your next.config.ts file.
With Cache Components, nothing is cached unless you explicitly ask for it.
You mark a page, component, or async function as cacheable with the use cache directive, and control when it’s refreshed with cacheLife and cacheTag.
To set this up, check out the Cache Components section below.
Previous caching model
If you’re using Next.js 15, or Next.js 16 without Cache Components enabled, Next.js uses its previous caching model:
-
Request Memoization deduplicates identical requests within a single render. It only lasts for one request cycle, so it isn’t relevant for Prepr caching strategies.
-
Data Cache stores the results of
fetchrequests across requests. It’s turned off by default, and you can opt in per request with{ cache: 'force-cache' }. -
Full Route Cache stores the rendered output of a route. It’s enabled by default, but any uncached
fetchrequest in a route opts that route out. Since the Data Cache is off by default, routes that fetch from Prepr won’t use the Full Route Cache unless you configure caching for those requests.
In other words, if you don’t set any caching options, Next.js refetches your data from Prepr on every request.
If you’re using Next.js 14 or earlier, the default caching behavior is different. Check out the Next.js 14 caching documentation .
For more details on caching in current versions, see the Next.js caching guide and the Next.js caching without cache components .
Apollo Client
Apollo Client stores the results of your GraphQL queries in a local, normalized, in-memory cache. This lets Apollo Client respond almost instantly to queries for data that’s already cached, without sending a network request.
For more details, check out the Apollo Client caching docs .
Prepr CDN
All Prepr content is served by a globally distributed content delivery network (CDN). When you send your first API request to fetch data from Prepr, the response is cached at an edge location close to the requester.
For more details, checkout the GraphQL API caching doc.
Recommendation
To make the most out of the dynamic Prepr features, personalization and A/B testing, we recommend setting up your front end for SSR (server-side rendering). Check out the Next.js complete guide for an example Next.js project using Apollo Client.
If you use Next.js 16 with Cache Components enabled, personalized pages still render on each request, but you can cache the content that is the same for every visitor. Check out the Cache Components section below for more details.
Cache Components
Next.js 16 introduces Cache Components, an opt-in feature that lets you mix static, cached and dynamic content in a single route. Next.js sends a prerendered static shell of the page to the browser right away and streams in the dynamic parts when they’re ready.
Cache Components is turned off by default. If you enable Cache Components, you need to update your code as described in the steps below.
How Cache Components fit with Prepr
With Cache Components enabled, the content on a page falls into one of three types:
| Content type | Prepr example | How to set it up |
|---|---|---|
| Static | Layout and navigation code that doesn’t fetch content. | No setup needed. Next.js prerenders this content at build time. |
| Cached | Content that is the same for every visitor, like blog posts on a separate route or a navigation menu with its own query. | Add the 'use cache' directive and a cache tag. Use a Prepr webhook to refresh the cache when the content changes. |
| Dynamic | Pages that include personalized content or A/B tests, and data collection. | Wrap the component in a <Suspense> boundary and don’t cache it. |
When you query a page with the Prepr-Customer-Id header, Prepr returns the whole page with the adaptive content and A/B test variants for that specific visitor.
This means the page query is different for every visitor and can’t be cached.
Your front end also doesn’t know in advance if a page includes personalized content, so pages in a catch-all route like [[...slug]] stay dynamic.
Don’t cache queries that include the Prepr-Customer-Id header.
The customer ID becomes part of the cache key, so Next.js stores a separate copy of the content for every visitor.
If you remove the header from the query instead, Prepr returns the default content and your visitors won’t see personalized content or A/B test variants.
For pages with personalized content, the benefit of Cache Components is the static shell: visitors see the layout right away while the page content streams in. For content that is the same for every visitor, you get the speed of a static website and updated content as soon as it changes in Prepr.
Enable Cache Components
To enable Cache Components in your Next.js project, follow the steps below.
-
Add the
cacheComponentsoption to yournext.config.tsfile:./next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { cacheComponents: true, /* other config options here */ }; export default nextConfig; -
Remove the route segment config from your
page.tsxfiles. These options aren’t compatible with Cache Components and cause a build error../src/app/[[...slug]]/page.tsx// Remove these lines export const revalidate = 0 export const dynamic = 'force-dynamic'By default, all data fetching now happens on each request, so you don’t need these options for SSR anymore.
Render personalized pages dynamically
With Cache Components enabled, code that reads request data like cookies, headers or the params of a dynamic route needs to be wrapped in a <Suspense> boundary.
If it isn’t, Next.js shows a blocking route error.
This applies to the page from the Next.js complete guide, because the getData function reads the customer ID from the request headers.
To fix this, move the page content to a separate component and wrap it in a <Suspense> boundary like in the example below.
The getData function stays the same as in the personalization guide, without the 'use cache' directive.
import { Suspense } from "react";
type PageProps = { params: Promise<{ slug?: string[] }> }
export default function Page({ params }: PageProps) {
return (
<Suspense fallback={<p>Loading...</p>}>
<PageContent params={params}/>
</Suspense>
);
}
// Fetch the personalized page on each request
async function PageContent({ params }: PageProps) {
const { slug } = await params
const data = await getData(slug ? slug.join('/') : '/')
// Render the page sections like in the Next.js complete guide
// ...
}Next.js now prerenders the static shell, including the layout and the fallback, and streams in the personalized page content on each request.
Instead of a <Suspense> boundary in the page, you can also add a loading.tsx file to the route folder.
Cache content that isn’t personalized
If your website has content that is the same for every visitor, like the blog posts in the Next.js quick start guide, you can cache it.
To cache Prepr content, add the 'use cache' directive to the function that fetches the data.
Use cacheTag() to tag the cached result, so you can refresh it when the content is changed or published in Prepr.
The example below caches the blog post page from the quick start guide. The cache tags use the model name and the content item ID, which both match the webhook payload.
import { cacheLife, cacheTag } from "next/cache";
import client from "@/apollo-client";
import {GetPostBySlug} from "@/queries/get-post-by-slug";
import {GetPosts} from "@/queries/get-posts";
// Prerender the existing blog posts at build time
export async function generateStaticParams() {
const {data} = await client.query({
query: GetPosts,
fetchPolicy: 'no-cache',
})
return data.Posts.items.map((post) => ({ slug: post._slug }))
}
async function getData(slug) {
// Cache the result of this function
'use cache'
cacheTag('Post')
cacheLife('days')
const {data} = await client.query({
query: GetPostBySlug,
variables: {
slug
},
// Let Next.js handle the caching instead of Apollo Client
fetchPolicy: 'no-cache',
})
// Tag the cached result with the content item ID
if (data.Post) {
cacheTag(data.Post._id)
}
return data.Post
}
// The PostPage component stays the same as in the quick start guideNext.js prerenders the blog posts that exist at build time. When you publish a new blog post, Next.js renders it on the first visit and caches it for the next visitors.
The fetchPolicy: 'no-cache' option makes sure Apollo Client doesn’t serve old content from its own cache after Next.js refreshes the cache.
The cacheLife('days') function keeps the content cached for a long time, because the webhook in the next step refreshes the cache as soon as the content changes.
Check out the cacheLife() docs for other cache profiles.
Refresh the cache with a webhook
To show updated content on your website without waiting for the cache to expire, set up a Prepr webhook that triggers your front end to refresh the cache.
-
Create an API route in your Next.js project. The route reads the model name and the content item ID from the webhook payload and refreshes the matching cache tags.
./src/app/api/revalidate/route.jsimport { revalidateTag } from "next/cache"; export async function POST(request) { const body = await request.json(); const item = body.payload; if (!item?.id) { return new Response('Missing content item ID', { status: 400 }); } // Expire the cached content right away, so the next visitor sees the updated content revalidateTag(item.id, { expire: 0 }) revalidateTag(item.model.body_singular, { expire: 0 }) return new Response('OK'); } -
Deploy your app. You need the deployed URL to set up the webhook in Prepr.
-
Create a webhook in Prepr. Set the URL value to
{YOUR_DEPLOYMENT_URL}/api/revalidate, choose thecontent_item.published,content_item.unpublishedandcontent_item.changedEvents, and choosePostfor Models.
In a real world application, verify the webhook signature in your API route to prevent unwanted cache invalidation.
The { expire: 0 } option makes sure the next visitor gets the updated content.
If you use 'max' instead, Next.js serves the old content to the next visitor once while it refreshes the cache in the background.
Check out the revalidateTag() docs for more details.
Preview content with Draft Mode
When you use the GraphQL Preview access token in a staging environment, Next.js also caches the preview content. This means content editors don’t see their changes in the preview until the cache expires.
To fix this, enable Draft Mode for content editors. When Draft Mode is enabled, Next.js skips all cached functions and fetches the content on every request. You don’t need to change your caching code for this.
-
Add a
PREPR_DRAFT_SECRETvariable with a random value to the.envfile of your staging environment../.envPREPR_GRAPHQL_URL=<YOUR-PREPR-GRAPHQL-PREVIEW-URL> PREPR_ENV=preview PREPR_DRAFT_SECRET=<YOUR-RANDOM-SECRET>Don’t add this variable to your production environment. Without the secret, the API route below doesn’t enable Draft Mode, so visitors can’t bypass the cache of your live website.
-
Create an API route that checks the secret, enables Draft Mode and redirects to the page the content editor wants to preview.
./src/app/api/draft/route.tsimport { draftMode } from "next/headers"; import { redirect } from "next/navigation"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const secret = process.env.PREPR_DRAFT_SECRET; // Only enable Draft Mode in environments with a secret if (!secret) { return new Response('Not found', { status: 404 }); } if (searchParams.get('secret') !== secret) { return new Response('Invalid secret', { status: 401 }); } // Only redirect to pages on your own website const path = searchParams.get('redirect') ?? '/'; if (!path.startsWith('/') || path.startsWith('//') || path.startsWith('/\\')) { return new Response('Invalid redirect', { status: 400 }); } (await draftMode()).enable(); redirect(path); } -
In Prepr, update the preview URL of your models to point to this API route. For example:
https://<YOUR-STAGING-URL>/api/draft?secret=<YOUR-RANDOM-SECRET>&redirect=/{slug}.
Now when a content editor opens the preview in Prepr, Next.js enables Draft Mode and shows the latest preview content. Draft Mode stays enabled for the rest of the browser session.
Render the preview toolbar dynamically
The Prepr preview toolbar reads the request headers and fetches the segments from Prepr.
With Cache Components enabled, this needs to happen inside a <Suspense> boundary, just like personalized content.
If it doesn’t, Next.js shows a blocking route error and none of the pages in the layout get a static shell.
To fix this, move the toolbar to a separate component and wrap it in a <Suspense> boundary like in the example below.
import { Suspense } from 'react'
import { getToolbarProps, PreprToolbar } from '@preprio/toolkit/nextjs'
export default function Layout({ children }: { children: React.ReactNode }) {
const isPreview = process.env.PREPR_ENV === 'preview'
return (
<>
{isPreview && (
<Suspense fallback={null}>
<Toolbar/>
</Suspense>
)}
{children}
</>
)
}
// Fetch the toolbar props on each request
async function Toolbar() {
const toolbarProps = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
return <PreprToolbar {...toolbarProps}/>
}You don’t need a try...catch block here, because getToolbarProps doesn’t throw an error when it can’t fetch the segments.
For more details on cache lifetimes, cache profiles and limitations, check out the Next.js Cache Components docs .
Prepr Next.js SSG example
If you choose not to use Prepr personalization or A/B testing, you can implement a caching strategy in Next.js and Apollo Client to build an SSG (statically generated) website.
Check out the Prepr Next.js static GitHub repo for example page.tsx code to view the Next.js and Apollo Client cache setup.
This project also includes two webhooks, api/post-published and api/post-updated which reset the cache if triggered.
To create a simple SSG blog site, follow the steps below.
-
Clone the Prepr Next.js static GitHub repo .
-
Copy the
.env.examplefile to a new.envfile by running the following command:cp .env.example .env -
In the .env file, replace
{YOUR_GRAPHQL_URL}with the API URL of the Prepr GraphQL access token from your Acme Lease demo environment.
-
Deploy the app in your preferred deployment tool. You need the deployed URL to set up the webhooks in Prepr in the next step.
-
Configure two webhooks in Prepr to listen for changed and published content and trigger your site to reset the cache.
-
Set the URL value to
{YOUR_DEPLOYMENT_URL}/api/post-updated, choosecontent-item.changedfor Events and choosePostfor Models.
-
Set the URL value to
{YOUR_DEPLOYMENT_URL}/api/post-published, choosecontent-item.publishedfor Events, and choosePostfor Models.
-
In this simple project, there are no security headers defined, but in a real world application you should add authorization to prevent unwanted cache invalidation.
All done! Now you have a simple SSG blog post site that renders updated content whenever content is changed or published.
Next steps
To learn more on how to expand your Next.js project, check out the following resources: