Cache Components is an architecture to build fast Next.js websites, mixing static and dynamic content as needed. APIs like "use cache", cacheTag, and cacheLife opt-in to and control caching.
Here are some improvements Prismic websites gain by using Cache Components over traditional App Router:
- Simpler client mental model: one client shared everywhere.
- Page-specific revalidation, speeding up content publication.
- Potentially lower hosting costs by caching more aggressively.
Meanwhile, the website can still be statically built, support content previews, and quickly update when new content is published. The implementation is straightforward, and most of it is plain Next.js. In some cases, it’s simpler to use Cache Components than not.
We first wrote about Cache Components while we were getting to know it. We have kept exploring it since, and we have learned how to use it well for marketing websites. This post shows what we landed on.
Cache Components is new and still developing
Treat this guide as an alternative approach rather than our official recommendation.
Set up the project
For this guide, we’ll assume you are starting a new project. If you have an existing website, adapt the pieces to your project.
Explore a full reference project
Prefer reading finished code? See the cache-components branch of our Next.js Landing Page starter.
To start, create an empty Next.js project and initialize Prismic in it:
npx create-next-app@latest my-website --empty
cd my-website
npx prismic init --no-setupThe npx prismic init command creates a Prismic repository and a prismic.config.json file with your repository name. Since we are using Cache Components, use the --no-setup flag with the command so it does not add packages or create files.
You’ll need to manually install the Prismic packages:
npm install @prismicio/client @prismicio/react @prismicio/nextNext, model your content. You can use the Type Builder or create everything from your terminal with the CLI. Here’s what a Homepage and Page type with some slices looks like from the CLI:
# Page types (each comes with a slice zone, SEO metadata, and a route)
npx prismic type create Homepage --format page --single
npx prismic type create Page --format page
# Slices, each with a field
npx prismic slice create Hero
npx prismic field add rich-text heading --to-slice hero --allow heading1 --single
npx prismic slice create Text
npx prismic field add rich-text text --to-slice text
# Add the slices to both page types' slice zones
npx prismic slice connect hero --to homepage
npx prismic slice connect hero --to page
npx prismic slice connect text --to homepage
npx prismic slice connect text --to page
# Commit the models, then push them to your repository
git add .
git commit -m "Add content models"
npx prismic pushThe CLI writes your models, slice components, and TypeScript types locally and keeps your routes in sync in prismic.config.json. The push command requires the model files to be committed first to maintain history, so commit before pushing.
Turn on Cache Components in next.config.ts:
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
};To complete the setup, create a prismicio.ts file with a single shared client, which will be imported in pages to fetch content:
// prismicio.ts
import { createClient } from "@prismicio/client";
import config from "./prismic.config.json";
export const repositoryName = config.repositoryName;
export const client = createClient(repositoryName, {
routes: config.routes,
});Build page files
In Prismic, each page type maps to a route. A reusable page type named Page, for example, lives at app/[uid]/page.tsx. Here is what that file looks like:
// app/[uid]/page.tsx
import { cacheTagPrismicPages, getPreviewRef } from "@prismicio/next";
import { SliceZone } from "@prismicio/react";
import type { Metadata } from "next";
import { cacheLife } from "next/cache";
import { notFound } from "next/navigation";
import { client } from "@/prismicio";
import { components } from "@/slices";
// 1. Fetch a page from Prismic, cached for reuse.
async function fetchPage(uid: string, ref?: string) {
"use cache";
const page = await client.getByUID("page", uid, { ref }).catch(() => notFound());
cacheTagPrismicPages([page]);
cacheLife("max");
return page;
}
// 2. List the pages to build ahead of time.
export async function generateStaticParams() {
const pages = await client.getAllByType("page");
return pages.map((page) => ({ uid: page.uid! }));
}
// 3. Set the page's SEO metadata.
export async function generateMetadata({ params }: PageProps<"/[uid]">): Promise<Metadata> {
const { uid } = await params;
const page = await fetchPage(uid, await getPreviewRef());
return {
title: page.data.meta_title,
description: page.data.meta_description,
openGraph: {
images: [{ url: page.data.meta_image.url ?? "" }],
},
};
}
// 4. Render the page's slices.
export default async function Page({ params }: PageProps<"/[uid]">) {
const { uid } = await params;
const page = await fetchPage(uid, await getPreviewRef());
return <SliceZone slices={page.data.slices} components={components} />;
}Most of it is ordinary Next.js. That page is static, previewable, and refreshed as soon as an editor publishes.
The unique part that matters for Cache Components is the fetchPage function:
async function fetchPage(uid: string, ref?: string) {
"use cache";
const page = await client.getByUID("page", uid, { ref }).catch(() => notFound());
cacheTagPrismicPages([page]);
cacheLife("max");
return page;
}What is `fetchPage`?
fetchPage is not a Cache Components API. It is a function you write to fetch data (a Prismic page in this case) and use in your page.tsx component. You can name and structure it any way you want, but we recommend the pattern shown here.
Here is what each part does:
getByUIDfetches thepagedocument for the given UID using the Prismic client."use cache"andcacheLife("max")are Next.js Cache Components APIs."use cache"caches the result, andcacheLife("max")keeps it for as long as Next.js allows, which suits content that changes rarely.cacheTagPrismicPagestags the cache entry with the document’s ID, so a later publish can refresh this page. It comes from@prismicio/next, a package of helpers that make Prismic easier to use with Next.js.refis the only request-time input. It points to a version of your Prismic content, used to load draft content during a preview. Passing it as an argument makes it part of the cache key, so draft and published content stay separate.
Everything about how this page is cached lives in this one function.
The rest of the file is standard Prismic and Next.js:
export async function generateStaticParams() {
const pages = await client.getAllByType("page");
return pages.map((page) => ({ uid: page.uid! }));
}
export async function generateMetadata({ params }: PageProps<"/[uid]">): Promise<Metadata> {
const { uid } = await params;
const page = await fetchPage(uid, await getPreviewRef());
return {
title: page.data.meta_title,
description: page.data.meta_description,
openGraph: {
images: [{ url: page.data.meta_image.url ?? "" }],
},
};
}
export default async function Page({ params }: PageProps<"/[uid]">) {
const { uid } = await params;
const page = await fetchPage(uid, await getPreviewRef());
return <SliceZone slices={page.data.slices} components={components} />;
}Here are those parts:
generateStaticParamslists the pages to build ahead of time. Any page it leaves out renders on the first request and is cached after.<SliceZone>, from@prismicio/react, renders each slice with the matching component from yourslicesfolder.getPreviewRef, also from@prismicio/next, returns the preview ref while an editor is previewing and nothing otherwise. We cover previews next.generateMetadatamaps your SEO fields, the same as any other Next.js page.
The website’s homepage file follows the same pattern, but without generateStaticParams. Its file is app/page.tsx, and it fetches one document with getSingle("homepage") instead of getByUID:
// app/page.tsx
import { cacheTagPrismicPages, getPreviewRef } from "@prismicio/next";
import { SliceZone } from "@prismicio/react";
import type { Metadata } from "next";
import { cacheLife } from "next/cache";
import { notFound } from "next/navigation";
import { client } from "@/prismicio";
import { components } from "@/slices";
// 1. Fetch a page from Prismic, cached for reuse.
async function fetchHomepage(ref?: string) {
"use cache";
const page = await client.getSingle("homepage", { ref }).catch(() => notFound());
cacheTagPrismicPages([page]);
cacheLife("max");
return page;
}
// 2. Set the page's SEO metadata.
export async function generateMetadata(): Promise<Metadata> {
const page = await fetchHomepage(await getPreviewRef());
return {
title: page.data.meta_title,
description: page.data.meta_description,
openGraph: {
images: [{ url: page.data.meta_image.url ?? "" }],
},
};
}
// 3. Render the page's slices.
export default async function Page() {
const page = await fetchHomepage(await getPreviewRef());
return <SliceZone slices={page.data.slices} components={components} />;
}Create and publish a homepage and a page in Prismic so the build has content to fetch, then run node --run build and check the route summary. Next.js marks each route:
○Static: fully prerendered, like your homepage.◐Partial Prerender: a static shell with request-time parts filled in as needed, like your[uid]pages.ƒDynamic: rendered on each request, like your API routes.
You want your pages to be ○ or ◐. The setup stays flexible: you can keep most of a page static and stream in the parts that need to be dynamic. We cover that in Add dynamic content when you need it below.
Build slices
Slice components are ordinary React components that receive their content as props:
// slices/Hero/index.tsx
import { Content } from "@prismicio/client";
import { PrismicRichText, SliceComponentProps } from "@prismicio/react";
type HeroProps = SliceComponentProps<Content.HeroSlice>;
export default function Hero({ slice }: HeroProps) {
return (
<section>
<PrismicRichText field={slice.primary.heading} />
</section>
);
}When a slice component needs additional data, like fetching another Prismic document, use the same "use cache" data fetching pattern from the page files:
async function fetchSettings(ref?: string) {
"use cache";
const settings = await client.getSingle("settings", { ref });
cacheTagPrismicPages([settings]);
cacheLife("max");
return settings;
}
export default async function Hero({ slice }: HeroProps) {
const settings = await fetchSettings(await getPreviewRef());
// ...
}To see your slices while you develop, use the Slice Simulator. Add its page at app/slice-simulator/page.tsx:
// app/slice-simulator/page.tsx
import { SliceSimulator, SliceSimulatorParams, getSlices } from "@prismicio/next";
import { SliceZone } from "@prismicio/react";
import { components } from "@/slices";
export default async function SliceSimulatorPage({
searchParams,
}: SliceSimulatorParams) {
const { state } = await searchParams;
const slices = getSlices(state);
return (
<SliceSimulator>
<SliceZone slices={slices} components={components} />
</SliceSimulator>
);
}Reading searchParams accesses request-time data, which makes Cache Components throw an error since the route isn’t wrapped in <Suspense>. The simplest solution is to add a loading.tsx file that renders nothing:
// app/slice-simulator/loading.tsx
export default function Loading() {
return null;
}The empty loading.tsx gives the route the Suspense shell Cache Components need, and it builds as ◐ Partial Prerender.
Finally, register the route as your simulator:
npx prismic preview set-simulator http://localhost:3000Preview draft content
Editors need to see drafts before they go live. Two pieces make that work, and both come from @prismicio/next.
First, read the preview reference in your fetches. You already saw this in the above files:
const page = await fetchPage(uid, await getPreviewRef());getPreviewRef returns a reference to the draft while a preview session is active, and nothing otherwise. Passing it into fetchPage loads the draft during a preview and the published content the rest of the time.
Second, two Route Handlers start and end a preview session:
// app/api/preview/route.ts
import { redirectToPreviewURL } from "@prismicio/next";
import { NextRequest } from "next/server";
import { client } from "@/prismicio";
export async function GET(request: NextRequest) {
return await redirectToPreviewURL({ client, request });
}// app/api/exit-preview/route.ts
import { exitPreview } from "@prismicio/next";
export async function GET() {
return await exitPreview();
}Add <PrismicPreview> to your root layout so the Prismic toolbar loads and refreshes the page as editors work:
// app/layout.tsx
import { ReactNode } from "react";
import { PrismicPreview } from "@prismicio/next";
import { repositoryName } from "@/prismicio";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<PrismicPreview repositoryName={repositoryName} />
</html>
);
}Previews need no special caching code. When a preview is active, Next.js Draft Mode turns on, and Draft Mode bypasses use cache completely. Every cached function reruns on each request, so editors always see the latest draft.
The prismic init command already connected your local development server as a preview endpoint. Once you deploy your website, add another preview endpoint for production:
npx prismic preview add http://example.com/api/preview --name ProductionRevalidate on publish
We want the website to always show the latest published content. To do that, we revalidate the pages that changed so Next.js rebuilds them with the latest content.
Prismic can fire a webhook whenever documents are published or unpublished. We point it at a Route Handler that revalidates the affected pages:
// app/api/revalidate/route.ts
import type { WebhookBody } from "@prismicio/client";
import { revalidatePrismicPages } from "@prismicio/next";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body: WebhookBody = await request.json();
if (body.type === "api-update") {
revalidatePrismicPages(body.documents);
}
return NextResponse.json({ revalidated: true });
}revalidatePrismicPages reads the IDs of the documents that changed and refreshes only those pages.
This step needs a deployed website since Prismic calls the webhook at a public URL. Once deployed, register the route:
npx prismic webhook create https://example.com/api/revalidate --trigger documentsPublished --trigger documentsUnpublishedAdd dynamic content when you need it
Most of a marketing website is the same for everyone, so it caches well. Some parts are not, like a banner personalized to the visitor or an A/B test. Those should not be cached.
For a component with per-request content, skip use cache and wrap the component in <Suspense>:
import { Suspense } from "react";
// fetchPage, generateStaticParams, and generateMetadata are still here.
export default async function Page({ params }: PageProps<"/[uid]">) {
const { uid } = await params;
const page = await fetchPage(uid, await getPreviewRef());
return (
<>
<Suspense>
<PromoBanner />
</Suspense>
<SliceZone slices={page.data.slices} components={components} />
</>
);
}
async function PromoBanner() {
// Fetch your uncached personalized content and render the component...
}The slices render into the static shell, and the personalized banner streams in at request time. The page stays static where it can be and dynamic only where it must be.
Wrap up
With this setup, your Prismic website follows Next.js’ latest features and benefits from aggressive, but precise, caching. It follows idiomatic Next.js practices, so you can build upon it however you need.
If you try this setup or have experience using Cache Components in your website, we’d love to hear your opinion on it by leaving a comment below.



