RMC Digital Logo
Sitecore,  SitecoreAI

Getting Started with Next.js 16 Cache Components in Sitecore AI

Author

Roberto Armas

Date Published

Modern Sitecore AI builds are moving toward a cleaner model: fetch fresh data when needed, cache intentionally, and revalidate only what changed. That is exactly why Next.js 16 Cache Components and Sitecore Content SDK 2.0 fit so well together.

Next.js 16 makes caching explicit with the new cacheComponents flag and the "use cache" directive, instead of relying on older implicit App Router behavior. It also introduces newer cache APIs like cacheLife, cacheTag, and updateTag() for fine-grained control.

On the Sitecore side, Content SDK 2.0 upgraded its Next.js stack to Next.js 16, enabled Cache Components, and added Turbopack File System Caching in its Next.js tooling and templates.

In this post, I’ll walk through how to enable Cache Components in a Sitecore-powered Next.js app, where to apply caching, and how to think about invalidation when your content changes.

Migration Steps

Step 1: Enable Cache Components Feature

Next.js 16 introduces Cache Components as the preferred caching model for the App Router. You can enable it in next.config.ts:

1import type { NextConfig } from 'next'
2
3const nextConfig: NextConfig = {
4 cacheComponents: true,
5}
6
7export default nextConfig

Step 2: Remove dynamic directives from routes

Remove any export const dynamic = 'force-dynamic'; declarations from your routes and pages. In Next.js 16, everything is dynamic by default unless you explicitly opt into caching, so this setting is no longer needed in most cases. Leaving it in place usually adds noise without changing behavior.

The new recommendation is to let routes remain dynamic by default and then selectively cache the parts of your application that should be reused by using Cache Components and the "use cache" directive.

For APIs that use headers(), such as robots.txt or well-known/ai-plugin/route.ts, calling await headers() makes the route dynamic at request time. This is useful when the route depends on request-specific values such as the host name or forwarded headers.

1export async function GET(request: NextRequest) {
2 await headers();


Step 3: Wrap Sitecore Client to function-level cache

Move Sitecore client calls into dedicated helper functions and apply caching at the function level. This makes caching more intentional and gives you better control over reuse, expiration, and invalidation.

Example: lib/cached-sitecore-client.ts

1import { cacheLife, cacheTag } from "next/cache";
2import {
3 PageOptions,
4 FetchOptions,
5 Page,
6 ErrorPage,
7} from "@sitecore-content-sdk/content/client";
8import client from "./sitecore-client";
9import {
10 LayoutServiceData,
11 RouteOptions,
12} from "node_modules/@sitecore-content-sdk/content/types/layout/models";
13import {
14 ComponentMap,
15 ComponentPropsCollection,
16 DictionaryPhrases,
17 NextjsContentSdkComponent,
18} from "@sitecore-content-sdk/nextjs";
19import { GetServerSidePropsContext, GetStaticPropsContext } from "next/types";
20
21export async function getPage(
22 path: string | string[],
23 pageOptions: PageOptions,
24 options?: FetchOptions
25): Promise<Page | null> {
26 "use cache";
27 cacheLife("hours");
28 cacheTag("page", "sitecore-content");
29 return await client.getPage(path, pageOptions, options);
30}
31
32export async function getErrorPage(
33 code: ErrorPage,
34 pageOptions?: Partial<RouteOptions>,
35 options?: FetchOptions
36): Promise<Page | null> {
37 "use cache";
38 cacheLife("hours");
39 cacheTag("page", "sitecore-content");
40 return await client.getErrorPage(code, pageOptions, options);
41}
42
43export async function getComponentData(
44 layoutData: LayoutServiceData,
45 context: GetServerSidePropsContext | GetStaticPropsContext,
46 components: ComponentMap<NextjsContentSdkComponent>
47): Promise<ComponentPropsCollection> {
48 "use cache";
49 cacheLife("hours");
50 cacheTag("component", "sitecore-content");
51 return await client.getComponentData(layoutData, context, components);
52}
53
54export async function getDictionary(
55 routeOptions?: Partial<RouteOptions>,
56 options?: FetchOptions
57): Promise<DictionaryPhrases> {
58 "use cache";
59 cacheLife("hours");
60 cacheTag("page", "sitecore-content");
61 return await client.getDictionary(routeOptions, options);
62}
63

Step 4: Replace direct Sitecore client usage with cached functions

Example: src\app\[site]\[locale]\[[...path]]\page.tsx

1import { isDesignLibraryPreviewData } from "@sitecore-content-sdk/nextjs/editing";
2import { notFound } from "next/navigation";
3import { draftMode } from "next/headers";
4import { SiteInfo } from "@sitecore-content-sdk/nextjs";
5import sites from ".sitecore/sites.json";
6import { routing } from "src/i18n/routing";
7import scConfig from "sitecore.config";
8import client from "src/lib/sitecore-client";
9import Layout, { RouteFields } from "src/Layout";
10import components from ".sitecore/component-map";
11import Providers from "src/Providers";
12import { NextIntlClientProvider } from "next-intl";
13import { setRequestLocale } from "next-intl/server";
14import { getComponentData, getPage } from "lib/cached-sitecore-client";
15import { Suspense } from "react";
16
17type PageProps = {
18 params: Promise<{
19 site: string;
20 locale: string;
21 path?: string[];
22 [key: string]: string | string[] | undefined;
23 }>;
24 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
25};
26
27async function EditingModePageContent({
28 searchParams,
29}: {
30 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
31}) {
32 const editingParams = await searchParams;
33 let page;
34 if (isDesignLibraryPreviewData(editingParams)) {
35 page = await client.getDesignLibraryData(editingParams);
36 } else {
37 page = await client.getPreview(editingParams);
38 }
39 if (!page) {
40 notFound();
41 }
42 const componentProps = await client.getComponentData(
43 page.layout,
44 {},
45 components
46 );
47 return (
48 <NextIntlClientProvider>
49 <Providers page={page} componentProps={componentProps}>
50 <Layout page={page} />
51 </Providers>
52 </NextIntlClientProvider>
53 );
54}
55
56export default async function Page({ params, searchParams }: PageProps) {
57 const { site, locale, path } = await params;
58 const draft = await draftMode();
59
60 // Set site and locale to be available in src/i18n/request.ts for fetching the dictionary
61 setRequestLocale(`${site}_${locale}`);
62
63 // Fetch the page data from Sitecore
64 let page;
65 if (draft.isEnabled) {
66 return (
67 <Suspense fallback={<div>Loading editing mode page...</div>}>
68 <EditingModePageContent searchParams={searchParams} />
69 </Suspense>
70 );
71 } else {
72 page = await getPage(path ?? [], { site, locale });
73 }
74
75 // If the page is not found, return a 404
76 if (!page) {
77 notFound();
78 }
79
80 // Fetch the component data from Sitecore (Likely will be deprecated)
81 const componentProps = await getComponentData(page.layout, {}, components);
82
83 return (
84 <NextIntlClientProvider>
85 <Providers page={page} componentProps={componentProps}>
86 <Layout page={page} />
87 </Providers>
88 </NextIntlClientProvider>
89 );
90}
91
92// This function gets called at build and export time to determine
93// pages for SSG ("paths", as tokenized array).
94export const generateStaticParams = async () => {
95 if (process.env.NODE_ENV !== "development" && scConfig.generateStaticPaths) {
96 // Filter sites to only include the sites this starter is designed to serve.
97 // This prevents cross-site build errors when multiple starters share the same Sitecore AI instance.
98 const defaultSite = scConfig.defaultSite;
99 const allowedSites = defaultSite
100 ? sites
101 .filter((site: SiteInfo) => site.name === defaultSite)
102 .map((site: SiteInfo) => site.name)
103 : sites.map((site: SiteInfo) => site.name);
104 return await client.getAppRouterStaticParams(
105 allowedSites,
106 routing.locales.slice()
107 );
108 }
109 return [
110 {
111 site: scConfig.defaultSite,
112 locale: scConfig.defaultLanguage,
113 path: ["/"],
114 },
115 ];
116};
117
118// Metadata fields for the page.
119export const generateMetadata = async ({ params }: PageProps) => {
120 const host = process.env.PUBLIC_URL || "";
121 const protocol = process.env.NODE_ENV === "development" ? "http" : "https";
122 const baseUrl =
123 process.env.NEXT_PUBLIC_SITE_URL || (host ? `${protocol}://${host}` : "");
124
125 const { path, site, locale } = await params;
126
127 // Canonical URL: base URL + content path only (no site/locale segments)
128 const pathSegment = path?.length ? `/${path.join("/")}` : "";
129 const canonicalUrl = baseUrl ? `${baseUrl}${pathSegment}` : undefined;
130
131 // The same call as for rendering the page. Should be cached by default react behavior
132 const page = await getPage(path ?? [], { site, locale });
133 const fields = page?.layout.sitecore.route?.fields as RouteFields;
134
135 // Parse keywords from comma-separated string to array
136 const keywordsString = fields?.metadataKeywords?.value?.toString() || "";
137 const keywords = keywordsString
138 ? keywordsString.split(",").map((k: string) => k.trim())
139 : [];
140
141 return {
142 title: fields?.Title?.value?.toString() || "Page",
143 description:
144 fields?.ogDescription?.value?.toString() ||
145 fields?.metadataDescription?.value?.toString() ||
146 "Sitecore Next.js Basic Example",
147 keywords,
148 ...(canonicalUrl && {
149 alternates: {
150 canonical: canonicalUrl,
151 },
152 }),
153 openGraph: {
154 title: fields?.ogTitle?.value?.toString() || "Page",
155 description:
156 fields?.ogDescription?.value?.toString() ||
157 fields?.metadataDescription?.value?.toString() ||
158 "Sitecore Next.js Basic Example",
159 url: canonicalUrl,
160 images: fields?.ogImage?.value?.src || fields?.thumbnailImage?.value?.src,
161 },
162 };
163};
164

Key implementation changes

After enabling Cache Components, the most important change is moving Sitecore data fetching away from page-level direct client calls and into dedicated cached helper functions. In Next.js 16, data is dynamic by default unless you explicitly cache it, so wrapping calls like getPage() and getComponentData() in function-level cache gives you better control over reuse, cache duration, and future invalidation.

In this example, editing mode is intentionally handled differently from the published site. When Draft Mode is enabled, the page renders through a separate EditingModePageContent path wrapped in Suspense, allowing preview and design library content to stay request-driven and up to date. For normal delivery, the page uses cached helper functions instead. This separation is important because authoring and preview flows need fresh data, while published traffic benefits from explicit caching.

Another useful shift is that Cache Components can now be applied beyond the page level. Once the route data is cached through helper functions, the same model can be used inside Sitecore components where data is stable enough to benefit from reuse. That makes it possible to gradually introduce caching into shared content, reusable datasource lookups, and component-level content composition without forcing the entire page into a single caching strategy.

One more detail worth noting is that older route segment options such as dynamic and revalidate no longer apply when Cache Components are enabled. Request-time APIs like headers() and cookies() should also stay outside cached scopes and be passed into cached functions only as plain arguments when needed. This becomes especially important for multisite scenarios, metadata routes, and request-aware endpoints.

Conclusion

Next.js 16 Cache Components bring a much clearer caching story to Sitecore AI projects. Instead of relying on broad rendering directives, we can now decide exactly what should stay dynamic, what should be cached, and how those cached results should be reused across the application.

That becomes especially powerful when page rendering and generateMetadata() share the same cached page helper, keeping SEO output aligned with the actual content being rendered. At the same time, preview and editing flows should still prioritize freshness over caching, and request-based values such as headers() and cookies() should remain outside cached helpers.

Most importantly, this is an incremental migration. You can start by caching page-level Sitecore calls, then expand into reusable component-level patterns as your application evolves. That makes Cache Components a practical improvement for real projects, not just a new feature to enable