
next-sanity: The Complete Guide
next-sanity is Sanity's official toolkit for Next.js. It bundles the Sanity client, GROQ helpers, a Portable Text renderer, live content fetching, Visual Editing, Draft Mode routes, webhook parsing and an embeddable Studio into one package, split across entry points such as next-sanity, next-sanity/live, next-sanity/visual-editing, next-sanity/draft-mode, next-sanity/webhook, next-sanity/studio and next-sanity/image. The current major version, v13, requires Next.js 16 and React 19.2. Most projects need only createClient, defineQuery and defineLive, and add the other entry points as they need previews, webhooks or an embedded Studio.
It's easy to lose track of what lives where, or to install a separate library for something next-sanity already provides. This guide is a map of the package: installation and peer dependencies, what each entry point exports, which helper to reach for in which situation, how versions have changed, and where to find the step-by-step guides for each feature.
Install next-sanity
The quickest route is to let the Sanity CLI set everything up from inside your Next.js project:
npx sanity@latest init
It creates a client, environment variables and, if you choose, an embedded Studio. To install manually, add the package and the image URL builder:
npm install next-sanity @sanity/image-url
next-sanity depends on @sanity/client, groq, @portabletext/react, @sanity/visual-editing, @sanity/webhook and @sanity/preview-url-secret, so you don't install those yourself. The full setup, including environment variables and CORS, is covered in how to connect Sanity with Next.js.
Versions and Peer Dependencies
At the time of writing, the latest release is next-sanity 13.4. Its peer dependencies are:
| Peer dependency | Required range | Needed for |
|---|---|---|
next | ^16.0.0 | Everything |
react, react-dom | ^19.2.3 | Everything |
@sanity/client | ^7.28.0 or ^8.11.0 | Client and live fetching |
sanity | ^5.29.0 or ^6.0.0 | Embedded Studio only |
styled-components | ^6.1 | Embedded Studio only |
If you don't embed the Studio, sanity and styled-components are effectively optional, although npm and pnpm may still install them as peers.
Major versions follow Next.js closely. v9 required Next.js 14, v10 and v11 required Next.js 15, and v12 and v13 require Next.js 16, with v13 adding support for the Cache Components model. If you're stuck on an older Next.js, pin the matching next-sanity major and follow the official migration guides one version at a time.
The Entry Points at a Glance
Here's what each import path gives you:
| Import path | Main exports | Use it for |
|---|---|---|
next-sanity | createClient, defineQuery, groq, PortableText, stegaClean, createDataAttribute | Fetching and rendering |
next-sanity/live | defineLive, resolvePerspectiveFromCookies | Cached fetching with automatic revalidation |
next-sanity/live/cache-life | sanity cache life profile | Cache Components setups |
next-sanity/visual-editing | VisualEditing | Click-to-edit overlays in Draft Mode |
next-sanity/draft-mode | defineEnableDraftMode | Secure route to turn Draft Mode on |
next-sanity/hooks | useIsPresentationTool, useOptimistic, usePresentationQuery, useVisualEditingEnvironment | Client-side Presentation helpers |
next-sanity/webhook | parseBody | Verifying Sanity webhook requests |
next-sanity/studio | NextStudio, metadata, viewport | Embedding the Studio in a route |
next-sanity/image | Image, imageLoader | Rendering Sanity CDN images efficiently |
The Root Export: Client, Queries and Portable Text
createClient
next-sanity re-exports everything from @sanity/client, including createClient. Importing from next-sanity keeps one version of the client in your app and matches the version defineLive expects:
// src/sanity/lib/client.ts
import { createClient } from 'next-sanity'
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2025-02-19',
useCdn: true,
stega: { studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || '/studio' },
})
The stega option only takes effect during previews, so it's safe to set from the start. API versions from 2025-02-19 default to the published perspective, which keeps drafts out of production responses.
defineQuery and groq
defineQuery wraps a GROQ string so Sanity TypeGen can find it and generate a result type. groq is the older tagged template, which only adds editor syntax highlighting:
// src/sanity/lib/queries.ts
import { defineQuery } from 'next-sanity'
export const POST_QUERY = defineQuery(`
*[_type == "post" && slug.current == $slug][0]{
_id,
title,
"slug": slug.current,
mainImage,
body
}
`)
Prefer defineQuery for new code. With TypeGen's client overloads enabled, client.fetch(POST_QUERY) and sanityFetch({ query: POST_QUERY }) return typed results automatically. The Sanity TypeGen guide covers the generation step.
PortableText
The package re-exports @portabletext/react, so PortableText and its types come from next-sanity too. It also adds stega-aware type helpers, InferValue and InferStrictComponents, which accept query results with or without Visual Editing characters:
import { PortableText, type PortableTextComponents } from 'next-sanity'
const components: PortableTextComponents = {
marks: {
link: ({ value, children }) => <a href={value?.href}>{children}</a>,
},
}
export function Body({ value }: { value: any[] }) {
return <PortableText value={value} components={components} />
}
Custom blocks, annotations and styling are covered in rendering Portable Text in React.
stegaClean and createDataAttribute
Two Visual Editing helpers also live here. stegaClean strips invisible stega characters from a string before you compare it or use it in an attribute, such as a colour option. createDataAttribute builds a data-sanity attribute for non-text elements like images, so they're still clickable in the Presentation tool.
next-sanity/live: defineLive, sanityFetch and SanityLive
defineLive is the centre of a modern setup. You pass it your client and a Viewer token, and it returns two things:
// src/sanity/lib/live.ts
import { defineLive } from 'next-sanity/live'
import { client } from './client'
const token = process.env.SANITY_API_READ_TOKEN
export const { sanityFetch, SanityLive } = defineLive({
client,
serverToken: token,
browserToken: token,
})
sanityFetchreplacesclient.fetch()in Server Components,generateMetadataandgenerateStaticParams. It returns{ data, sourceMap, tags }, tags the cached result with Sanity's sync tags, switches to the drafts perspective in Draft Mode and adds stega during previews.<SanityLive />goes in your root layout once. It listens to the Live Content API and revalidates the cached data whose tags changed, so published updates reach the site without webhooks.
// src/app/posts/[slug]/page.tsx
import { sanityFetch } from '@/sanity/lib/live'
import { POST_QUERY } from '@/sanity/lib/queries'
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const { data: post } = await sanityFetch({ query: POST_QUERY, params: { slug } })
return <h1>{post?.title}</h1>
}
sanityFetch also decides whether to use the API CDN. Requests in the published perspective go through the CDN, while other perspectives, such as drafts, go straight to the live API. You don't need to manage useCdn per request. The trade-offs are explained in Sanity CDN vs the live API.
Keep the token server-side, without a NEXT_PUBLIC_ prefix. The browserToken is only sent to the browser while Draft Mode is on, which is why it should have Viewer permissions and nothing more.
Cache Components and Strict Mode
When cacheComponents: true is set in next.config.ts, request-time APIs like draftMode() and cookies() can't run inside 'use cache' functions. next-sanity v13 handles this with two additions:
next-sanity/live/cache-lifeexports asanitycache life profile. Set it as the default withcacheLife: { default: sanity }, so cached Sanity data relies on live revalidation rather than a short timer.strict: trueondefineLivemakes TypeScript requireperspectiveandstegaon everysanityFetchcall, andincludeDraftson<SanityLive />. You resolve those values outside the cache, usingresolvePerspectiveFromCookieswhen Draft Mode is on, and pass them in as props.
// next.config.ts
import type { NextConfig } from 'next'
import { sanity } from 'next-sanity/live/cache-life'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: { default: sanity },
}
export default nextConfig
The full three-layer pattern, with a page, a dynamic component and a cached component, is in Sanity with the Next.js App Router. If you don't enable Cache Components, you don't need strict mode.
By default, a live event marks affected tags as stale, so the next request triggers a background refresh. If every visitor must see the change immediately, <SanityLive waitFor="function" /> works with a Sanity Function that revalidates tags before the browser refreshes. The caching and performance guide compares these options with manual tags.
next-sanity/visual-editing and next-sanity/draft-mode
Previews need two pieces: a secure way to turn Draft Mode on, and overlays that make content clickable.
defineEnableDraftMode returns a ready-made GET route handler. It validates a short-lived secret created by an authenticated Studio session, so only editors can enable Draft Mode:
// src/app/api/draft-mode/enable/route.ts
import { defineEnableDraftMode } from 'next-sanity/draft-mode'
import { client } from '@/sanity/lib/client'
export const { GET } = defineEnableDraftMode({
client: client.withConfig({ token: process.env.SANITY_API_READ_TOKEN }),
})
<VisualEditing /> renders the overlays and connects the page to the Presentation tool. Render it only when Draft Mode is on:
// src/app/layout.tsx
import { draftMode } from 'next/headers'
import { VisualEditing } from 'next-sanity/visual-editing'
import { SanityLive } from '@/sanity/lib/live'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled } = await draftMode()
return (
<html lang="en">
<body>
{children}
<SanityLive />
{isEnabled && <VisualEditing />}
</body>
</html>
)
}
The disable route is plain Next.js, calling (await draftMode()).disable(). For the full flow, including the Presentation tool and document locations, follow Sanity Visual Editing in Next.js. For the editor experience of drafts updating as they type, see live preview with Sanity.
next-sanity/hooks
The hooks are client-side helpers for components that behave differently inside the Studio:
useIsPresentationTool()returnstrueinside the Presentation tool, for example to hide analytics.useVisualEditingEnvironment()tells you whether the page is in the Presentation iframe, a separate window or neither.useOptimistic()applies an editor's change locally before the server responds, such as reordering array items.usePresentationQuery()subscribes a client component to a query's live results in Presentation.
Most sites never need these.
next-sanity/webhook: parseBody
If you cache with your own tags or rebuild on publish, you'll receive webhooks from Sanity. parseBody reads the request, verifies the signature against your secret and, with the third argument set to true, waits briefly so the Content Lake is consistent before you refetch:
// src/app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import type { NextRequest } from 'next/server'
import { parseBody } from 'next-sanity/webhook'
export async function POST(req: NextRequest) {
const { isValidSignature, body } = await parseBody<{ _type: string }>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true,
)
if (!isValidSignature) return new Response('Invalid signature', { status: 401 })
if (!body?._type) return new Response('Bad request', { status: 400 })
revalidateTag(body._type, 'max')
return Response.json({ revalidated: body._type })
}
Without a secret, isValidSignature is null and nothing is verified, so always pass one. With <SanityLive /> you don't need this route for basic freshness; it's for manual tags, external caches and static rebuilds, which are covered in how to use Sanity webhooks.
next-sanity/studio: NextStudio
NextStudio renders a full Sanity Studio inside a Next.js route, usually a catch-all at app/studio/[[...tool]]/page.tsx. The entry point also exports metadata and viewport objects that set the right viewport and a noindex robots tag:
// src/app/studio/[[...tool]]/page.tsx
import { NextStudio } from 'next-sanity/studio'
import config from '../../../../sanity.config'
export const dynamic = 'force-static'
export { metadata, viewport } from 'next-sanity/studio'
export default function StudioPage() {
return <NextStudio config={config} />
}
This is the only part of the package that needs the sanity and styled-components peers. Route groups, workspaces and troubleshooting are in embedding Sanity Studio in Next.js.
next-sanity/image: Image and imageLoader
next/image works with Sanity, but it re-optimises images that Sanity's CDN can already resize. The Image component from next-sanity/image takes a Sanity CDN URL and generates the srcSet by changing the CDN's width and height parameters instead:
import { Image } from 'next-sanity/image'
import { urlFor } from '@/sanity/lib/image'
<Image
src={urlFor(post.mainImage).width(1200).height(630).url()}
alt={post.mainImage?.alt ?? ''}
width={1200}
height={630}
sizes="(min-width: 768px) 50vw, 100vw"
/>
The urlFor helper comes from @sanity/image-url, which isn't bundled, hence the separate install. imageLoader is also exported if you'd rather pass a loader to next/image directly. Both are marked alpha, so check the changelog when upgrading. Cropping, hotspots and formats are in Sanity's image URL builder.
Which Helper Should You Use?
| Situation | Reach for |
|---|---|
| Simple site, content rarely changes | createClient and client.fetch() with time-based revalidation |
| Content should update without redeploys | defineLive, sanityFetch, <SanityLive /> |
| Editors want previews | Add defineEnableDraftMode and <VisualEditing /> |
| Cache Components enabled | defineLive({ strict: true }) and the sanity cache life profile |
| External cache or static export | parseBody in a webhook route |
| Studio on the same domain | NextStudio |
| Many responsive images | next-sanity/image |
A plain client is fine to start with, and moving to defineLive later is mostly a find-and-replace from client.fetch(query, params) to sanityFetch({ query, params }).
FAQ: next-sanity
Usually not. next-sanity depends on @sanity/client and re-exports it, so import createClient from next-sanity. You only need to manage the client package yourself if another library in your app needs a specific version.
For Next.js 16, use v13, the current major. Older majors match older Next.js releases, so if you can't upgrade Next.js yet, pin the next-sanity major that supports your version and upgrade both together using the official migration guides.
client.fetch is a plain request that you cache yourself with revalidate options or tags. sanityFetch from defineLive tags results automatically, switches to drafts in Draft Mode, adds stega for Visual Editing and revalidates when SanityLive receives a content change.
The current features, including defineLive, SanityLive and defineEnableDraftMode, are built for the App Router. The client, queries, PortableText and parseBody also work in Pages Router API routes and getStaticProps, but live fetching and Visual Editing helpers assume the App Router.
Yes. Most production sites host the Studio separately. If you don't import next-sanity/studio, you can ignore the sanity and styled-components peer dependencies, and the Studio code never reaches your bundle.
Not for keeping pages fresh, because SanityLive revalidates cached data when content changes. Webhooks are still useful for anything outside Next.js's cache, such as purging a third-party CDN, triggering static rebuilds or notifying other services.
Conclusion
next-sanity covers nearly everything a Next.js site needs from Sanity: the client, defineQuery and PortableText from the root export, self-revalidating fetches from next-sanity/live, previews from visual-editing and draft-mode, webhook verification, an embeddable Studio and responsive images.
Start with createClient and defineQuery, move to defineLive when content should update without redeploys, and add the preview pieces when editors ask for them. Keep the package on the major that matches your Next.js version, and follow the linked deep dives to set up each feature.


