Type something to search...
next-sanity: The Complete Guide

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 dependencyRequired rangeNeeded for
next^16.0.0Everything
react, react-dom^19.2.3Everything
@sanity/client^7.28.0 or ^8.11.0Client and live fetching
sanity^5.29.0 or ^6.0.0Embedded Studio only
styled-components^6.1Embedded 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 pathMain exportsUse it for
next-sanitycreateClient, defineQuery, groq, PortableText, stegaClean, createDataAttributeFetching and rendering
next-sanity/livedefineLive, resolvePerspectiveFromCookiesCached fetching with automatic revalidation
next-sanity/live/cache-lifesanity cache life profileCache Components setups
next-sanity/visual-editingVisualEditingClick-to-edit overlays in Draft Mode
next-sanity/draft-modedefineEnableDraftModeSecure route to turn Draft Mode on
next-sanity/hooksuseIsPresentationTool, useOptimistic, usePresentationQuery, useVisualEditingEnvironmentClient-side Presentation helpers
next-sanity/webhookparseBodyVerifying Sanity webhook requests
next-sanity/studioNextStudio, metadata, viewportEmbedding the Studio in a route
next-sanity/imageImage, imageLoaderRendering 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,
})
  • sanityFetch replaces client.fetch() in Server Components, generateMetadata and generateStaticParams. 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-life exports a sanity cache life profile. Set it as the default with cacheLife: { default: sanity }, so cached Sanity data relies on live revalidation rather than a short timer.
  • strict: true on defineLive makes TypeScript require perspective and stega on every sanityFetch call, and includeDrafts on <SanityLive />. You resolve those values outside the cache, using resolvePerspectiveFromCookies when 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() returns true inside 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?

SituationReach for
Simple site, content rarely changescreateClient and client.fetch() with time-based revalidation
Content should update without redeploysdefineLive, sanityFetch, <SanityLive />
Editors want previewsAdd defineEnableDraftMode and <VisualEditing />
Cache Components enableddefineLive({ strict: true }) and the sanity cache life profile
External cache or static exportparseBody in a webhook route
Studio on the same domainNextStudio
Many responsive imagesnext-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.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper