Use Sanity with Next.js

Sanity Live with Next.js Cache Components

Configure next-sanity v13 for Next.js Cache Components (cacheComponents: true). Covers the three-layer component pattern, defineLive setup, draft mode handling, and manual migration from earlier versions.

This guide shows how to configure next-sanity for cacheComponents: true. The important difference from traditional Sanity Live usage is that sanityFetch must run inside cached boundaries, while request-time values such as draftMode() and cookies must be resolved outside those boundaries and passed in as props.

Automate the migration with an agent

Setup

Install next-sanity@13:

1. Configure next.config.ts

In your next.config.ts, enable cacheComponents and add the Sanity cacheLife preset. Sanity Live handles on-demand revalidation, so cached Sanity data should not rely on the default 15-minute time-based revalidation.

2. Configure the Sanity client

Projects typically have a src/sanity/lib/client.ts file. It should use a modern apiVersion, default to the published perspective, and configure stega.studioUrl for Visual Editing:

If this file already exists, extend it rather than overwriting it. Changing apiVersion or removing existing stega.* options can break an app.

3. Configure defineLive

Create a live.ts file next to client.ts. Use strict: true so TypeScript requires every sanityFetch call to pass perspective and stega, and every <SanityLive /> render to pass includeDrafts. You also need helpers for the places where Sanity data is fetched outside normal React Server Component rendering.

4. Render <SanityLive /> in a root layout

Render <SanityLive /> once in a root layout and pass includeDrafts={isDraftMode}. Render <VisualEditing /> only in draft mode.

If the app has an embedded Sanity Studio route (for example app/studio/[[...index]]/page.tsx), put <SanityLive /> in a route-group layout that the Studio route does not use, such as src/app/(website)/layout.tsx.

5. Fetching data with sanityFetch

Cache Components introduce a layered caching system, so you need to define cache boundaries yourself depending on your application needs and how dynamic or cacheable the data you are fetching is.

Key difference from cacheComponents: false

When cacheComponents: false, sanityFetch can read draftMode() to set perspective and stega for you. When cacheComponents: true, Next.js does not allow request-time APIs like draftMode() and cookies() inside use cache boundaries.

To handle this, use a three-layer structure:

  • Page/layout component: branches on draftMode() when the route can be prerendered.
  • Dynamic component: resolves params, cookies(), and getDynamicFetchOptions() outside the cache boundary.
  • Cached component: has use cache and receives serializable props, including perspective and stega.

Under the hood, sanityFetch automatically calls the cacheTag() API and the cacheLife() API, so you can focus on defining your query and params.

Keep these rules in mind:

  • Any async function that calls sanityFetch should have a use cache or use cache: remote directive.
  • Do not hardcode perspective: published or stega: false inside cached components that render page content. Resolve those values outside the cache boundary and pass them in as props.
  • Do not take perspective or stega as server action input. Server action inputs are untrusted; resolve them inside the server action and pass them to a cached helper.
  • In route.ts handlers, use stega: false unless the response is rendered into the same DOM as <VisualEditing />.

Static routes

Dynamic routes with params

In Next.js 16+, params is a Promise. For routes where params is used as input to sanityFetch, implement generateStaticParams() and use sanityFetchStaticParams(). The dynamic layer unwraps both params and the fetch options before passing plain, serializable values to the cached component:

PageProps<"/product/[slug]"> is provided by Next.js next typegen output, so the params are typed from the route segment without having to define a Props type by hand.

Caching generateMetadata

Metadata should not use stega encoding, but it should still resolve perspective so Presentation Tool can preview draft content and content releases in a new preview window. Use sanityFetchMetadata() and pass the resolved perspective.

Routes with loading.tsx

If a dynamic route has a sibling loading.tsx, the route can rely on that fallback instead of adding its own Suspense boundary. In that case it can await params and getDynamicFetchOptions() directly in the page component before rendering a cached component:

Without a sibling loading.tsx, keep request-time work in a dynamic component wrapped by Suspense.

Migrating an existing Sanity Live setup

If the app is already using defineLive, this is a refactor, not a rewrite. The five-step sequence above still applies, but watch for these specific differences:

  • Do not overwrite client.ts or live.ts if they exist. Append missing options. Preserve any existing token and stega.* settings.
  • Search for hardcoded perspective: published and stega: false in sanityFetch call sites and refactor them to source perspective/stega via getDynamicFetchOptions and the three-layer pattern.
  • Search for sanityFetch calls inside generateStaticParams and swap for sanityFetchStaticParams.
  • Search for sanityFetch calls inside generateMetadata, sitemap.ts, and opengraph-image.tsx and swap for sanityFetchMetadata.
  • Search for sanityFetch calls directly inside a use server function and split into a separate use cache helper.
  • Verify there is exactly one <SanityLive> and one <VisualEditing> in the tree. Multiple renders are undefined behavior.

Verify both modes

Run the app with next dev and test both draft mode enabled and draft mode disabled. next build --debug-prerender can catch prerendering issues, but it does not prove that draft mode, Presentation Tool perspective switching, or Visual Editing overlays work correctly.

Was this page helpful?