The official Sanity.io toolkit for Next.js apps.
Features:
- The Sanity Client fully compatible with Next.js’ caching features
- Live Preview mode
- Visual Editing
- GROQ syntax highlighting
- Embedded Sanity Studio
- Table of contents
- Installation
- Usage
- Cache revalidation
- Preview
- Visual Editing with Content Source Maps
- Embedded Sanity Studio
- Migration guides
- License
For basic functionality, run the following command in the package manager of your choice:
npm install next-sanity
yarn add next-sanity
pnpm install next-sanity
bun install next-sanity
Building with Sanity and Next.js, you‘re likely to want libraries to handle On-Demand Image Transformations and block content with Portable Text:
npm install @portabletext/react @sanity/image-url
yarn add @portabletext/react @sanity/image-url
pnpm install @portabletext/react @sanity/image-url
bun install @portabletext/react @sanity/image-url
When using npm
newer than v7
, or pnpm
newer than v8
, you should end up with needed dependencies like sanity
and styled-components
when you npm install next-sanity
. It also works in yarn
v1
using install-peerdeps
:
npx install-peerdeps --yarn next-sanity
There are different ways to integrate Sanity with Next.js depending on your usage and needs for features like Live Preview, tag-based revalidation, and so on. It's possible to start simple and add more functionality as your project progresses.
To start running GROQ queries with next-sanity
, we recommend creating a client.ts
file:
// ./src/utils/sanity/client.ts
import {createClient} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: true, // if you're using ISR or only static generation at build time then you can set this to `false` to guarantee no stale content
})
To fetch data in a React Server Component using the App Router:
// ./src/app/page.tsx
import {client} from '@/src/utils/sanity/client'
type Post = {
_id: string
title?: string
slug?: {
current: string
}
}
export async function PostIndex() {
const posts = await client.fetch<Post[]>(`*[_type == "post"]`)
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={post?.slug.current}>{post?.title}</a>
</li>
))}
</ul>
)
}
If you're using the Pages Router, then you can do the following from a page component:
// ./src/pages/index.tsx
import {client} from '@/src/utils/sanity/client'
type Post = {
_id: string
title?: string
slug?: {
current: string
}
}
export async function getStaticProps() {
return await client.fetch<Post[]>(`*[_type == "post"]`)
}
export async function HomePage(props) {
const {posts} = props
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={post?.slug.current}>{post?.title}</a>
</li>
))}
</ul>
)
}
You might notice that you have to set the useCdn
to true
or false
in the client configuration. Sanity offers caching on a CDN for content queries. Since Next.js often comes with its own caching, it might not be necessary, but there are some exceptions.
The general rule is that useCdn
should be true
when:
- Data fetching happens client-side, for example, in a
useEffect
hook or in response to a user interaction where theclient.fetch
call is made in the browser. - Server-Side Rendered (SSR) data fetching is dynamic and has a high number of unique requests per visitor, for example, a "For You" feed.
And it makes sense to set useCdn
to false
when:
- Used in a static site generation context, for example,
getStaticProps
orgetStaticPaths
. - Used in an ISR on-demand webhook responder.
- Good
stale-while-revalidate
caching is in place that keeps API requests on a consistent low, even if traffic to Next.js spikes. - For Preview or Draft modes as part of an editorial workflow, you need to ensure that the latest content is always fetched.
Sanity uses date-based API versioning. The tl;dr is that you can send the implementation date in a YYYY-MM-DD format, and it will automatically fall back on the latest API version of that time. Then, if a breaking change is introduced later, it won't break your application and give you time to test before upgrading (by setting the value to a date past the breaking change).
This toolkit includes the @sanity/client
that fully supports Next.js’ fetch
based features, including the revalidateTag
API. It‘s not necessary to use the React.cache
method like with many other third-party SDKs. This gives you tools to ensure great performance while preventing stale content in a way that's native to Next.js.
Note
Some hosts (like Vercel) will keep the content cache in a dedicated data layer and not part of the static app bundle, which means that it might not be revalidated from re-deploying the app like it has done earlier. We recommend reading up on caching behavior in the Next.js docs.
Time-based revalidation is best for less complex cases and where content updates don't need to be immediately available.
// ./src/app/home/layout.tsx
import { client } from '@/src/utils/sanity/client'
import { PageProps } from '@/src/app/(page)/Page.tsx'
type HomePageProps = {
_id: string
title?: string
navItems: PageProps[]
}
export async function HomeLayout({children}) {
const home = await client.fetch<HomePageProps>(`*[_id == "home"][0]{...,navItems[]->}`,
next: {
revalidate: 3600 // look for updates to revalidate cache every hour
}
})
return (
<main>
<nav>
<span>{home?.title}</span>
<ul>
{home?.navItems.map(navItem => ({
<li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
}))}
</ul>
</nav>
{children}
</main>
)
}
Tag-based or on-demand revalidation gives you more fine-grained and precise control for when to revalidate content. This is great for pulling content from the same source across components and when content freshness is important.
Below is an example configuration that ensures the client is only bundled server-side and comes with some defaults. It‘s also easier to adapt for Live Preview functionality (see below).
If you're planning to use revalidateTag
, then remember to set up the webhook (see code below) as well.
// ./src/utils/sanity/client.ts
import 'server-only'
import type {QueryParams} from '@sanity/client'
import {createClient} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: false,
})
const DEFAULT_PARAMS = {} as QueryParams
const DEFAULT_TAGS = [] as string[]
export async function sanityFetch<QueryResponse>({
query,
params = DEFAULT_PARAMS,
tags = DEFAULT_TAGS,
}: {
query: string
params?: QueryParams
tags: string[]
}): Promise<QueryResponse> {
return client.fetch<QueryResponse>(query, params, {
cache: 'force-cache',
next: {
//revalidate: 30, // for simple, time-based revalidation
tags, // for tag-based revalidation
},
})
}
Now you can import the sanityFetch()
function in any component within the app
folder, and specify for which document types you want it to revalidate:
// ./src/app/home/layout.tsx
import { sanityFetch } from '@/src/utils/sanity/client'
import { PageProps } from '@/src/app/(page)/Page.tsx'
type HomePageProps = {
_id: string
title?: string
navItems: PageProps[]
}
export async function HomeLayout({children}) {
// revalidate if there are changes to either the home document or to a page document (since they're referenced to in navItems)
const home = await sanityFetch<HomePageProps>({
query: `*[_id == "home"][0]{...,navItems[]->}`,
tags: ['home', 'page']
})
return (
<main>
<nav>
<span>{home?.title}</span>
<ul>
{home?.navItems.map(navItem => ({
<li key={navItem._id}><a href={navItem?.slug?.current}>{navItem?.title}</a></li>
}))}
</ul>
</nav>
{children}
</main>
)
}
In order to get revalidateTag
to work you need to set up an API route in your Next.js app that handles an incoming request, typically made by a GROQ-Powered Webhook.
You can use this template to quickly configure the webhook for your Sanity project.
The code example below uses the built-in parseBody
function to validate that the request comes from your Sanity project (using a shared secret + looking at the request headers). Then it looks at the document type information in the webhook payload and matches that against the revalidation tags in your app:
// ./src/app/api/revalidate.ts
import {revalidateTag} from 'next/cache'
import {type NextRequest, NextResponse} from 'next/server'
import {parseBody} from 'next-sanity/webhook'
export async function POST(req: NextRequest) {
try {
const {isValidSignature, body} = await parseBody<{_type}>(
req,
process.env.SANITY_REVALIDATE_SECRET,
)
if (!isValidSignature) {
const message = 'Invalid signature'
return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401})
}
if (!body?._type) {
const message = 'Bad Request'
return new Response({message, body}, {status: 400})
}
// If the `_type` is `page`, then all `client.fetch` calls with
// `{next: {tags: ['page']}}` will be revalidated
await revalidateTag(body._type)
return NextResponse.json({body})
} catch (err) {
console.error(err)
return new Response(err.message, {status: 500})
}
}
You can choose to match tags based on any field or expression since GROQ-Powered Webhooks allow you to freely define the payload.
If you are using the Pages Router and want on-demand revalidation, you'll have to do this by targeting the URLs/slugs for the pages you want to revalidate. If you have nested routes, you will need to adopt the logic to accommodate for that. For example, using _type
to determine the first segment: /${body?._type}/${body?.slug.current}
.
// ./pages/api/revalidate.ts
import type {NextApiRequest, NextApiResponse} from 'next'
import {parseBody} from 'next-sanity/webhook'
// Export the config from next-sanity to enable validating the request body signature properly
export {config} from 'next-sanity/webhook'
export default async function revalidate(req: NextApiRequest, res: NextApiResponse) {
try {
const {isValidSignature, body} = await parseBody(req, process.env.SANITY_REVALIDATE_SECRET)
if (!isValidSignature) {
const message = 'Invalid signature'
return res.status(401).json({message, isValidSignature, body})
}
const staleRoute = `/${body.slug.current}`
await res.revalidate(staleRoute)
const message = `Updated route: ${staleRoute}`
return res.status(200).json({message, body})
} catch (err) {
console.error(err)
return res.status(500).json({message: err.message})
}
}
Check out our Personal website template to see a feature-complete example of how revalidateTag
is used together with Live Previews.
To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:
// ./next.config.js
module.exports = {
experimental: {
logging: 'verbose',
},
}
There are different ways to set up content previews with Sanity and Next.js.
Perspectives is a feature for Sanity Content Lake that lets you run the same queries but pull the right content variations for any given experience. The default value is raw
, which means no special filtering is applied, while published
and previewDrafts
can be used to optimize for preview and ensure that no draft data leaks into production for authenticated requests.
// ./src/utils/sanity/client.ts
import {createClient} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const token = process.env.SECRET_SANITY_VIEW_TOKEN
const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: true, // if you're using ISR or only static generation at build time then you can set this to `false` to guarantee no stale content
token,
perspective: 'published', // prevent drafts from leaking through even though requests are authenticated
})
Live Preview gives you real-time preview across your whole app for your Sanity project members. The Live Preview can be set up to give the preview experience across the whole app. Live Preview works on the data layer and doesn't require specialized components or data attributes. However, it needs a thin component wrapper to load server-side components into client-side, in order to rehydrate on changes.
Router-specific setup guides for Live Preview:
Since next-sanity/preview
is simply re-exporting LiveQueryProvider
and useLiveQuery
from @sanity/preview-kit
, you'll find advanced usage and comprehensive docs in its README.
The same is true for next-sanity/preview/live-query
.
Next.js gives you a built-in draftMode
variable that can activate features like Visual Edit or any preview implementation.
// ./src/utils/sanity/client.ts
import 'server-only'
import {draftMode} from 'next/headers'
import type {QueryParams} from '@sanity/client'
import {createClient, groq} from 'next-sanity'
import {draftMode} from 'next/headers'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: false,
})
// Used by `PreviewProvider`
export const token = process.env.SANITY_API_READ_TOKEN
const DEFAULT_PARAMS = {} as QueryParams
const DEFAULT_TAGS = [] as string[]
export async function sanityFetch<QueryResponse>({
query,
params = DEFAULT_PARAMS,
tags = DEFAULT_TAGS,
}: {
query: string
params?: QueryParams
tags: string[]
}): Promise<QueryResponse> {
const isDraftMode = draftMode().isEnabled
if (isDraftMode && !token) {
throw new Error('The `SANITY_API_READ_TOKEN` environment variable is required.')
}
const REVALIDATE_SKIP_CACHE = 0
const REVALIDATE_CACHE_FOREVER = false
return client.fetch<QueryResponse>(query, params, {
...(isDraftMode && {
token: token,
perspective: 'previewDrafts',
}),
next: {
revalidate: isDraftMode ? REVALIDATE_SKIP_CACHE : REVALIDATE_CACHE_FOREVER,
tags,
},
})
}
Be aware that you can get errors if you use the cache
and the revalidate
configurations for Next.js cache at the same time. Go to the Next.js docs to learn more.
Note
Content Source Maps are available as an API for select Sanity enterprise customers. Contact our sales team for more information.
The createClient
method in next-sanity
supports visual editing, it supports all the same options as @sanity/preview-kit/client
. Add studioUrl
to your client configuration and it'll automatically show up on Vercel Preview Deployments:
import {createClient, groq} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID // "pv8y60vp"
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET // "production"
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION // "2023-05-03"
const client = createClient({
projectId,
dataset,
apiVersion, // https://www.sanity.io/docs/api-versioning
useCdn: true, // if you're using ISR or only static generation at build time, then you can set this to `false` to guarantee no stale content
studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
encodeSourceMap: true, // Optional. Default to: process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview',
})
Go to our setup guide for a walkthrough on how to customize the experience.
Sanity Studio allows you to embed a near-infinitely configurable content editing interface into any React application. For Next.js, you can embed the Studio on a route (like /admin
). The Studio will still require authentication and be available only for members of your Sanity project.
This opens up many possibilities:
- Any service that hosts Next.js apps can now host your Studio.
- Building previews for your content is easier as your Studio lives in the same environment.
- Use Data Fetching to configure your Studio.
- Easy setup of Preview Mode.
The NextStudio
component loads up the import {Studio} from 'sanity'
component for you and wraps it in a Next-friendly layout. metadata
specifies the necessary <meta>
tags for making the Studio adapt to mobile devices, and prevents the route from being indexed by search engines.
To quickly scaffold the embedded studio and a Sanity project, you can run the following command in your project folder:
npx sanity@latest init
Make a file called sanity.config.ts
(or .js
for non-TypeScript projects) in the project's root (same place as next.config.ts
) and copy the example below. Both the Next /app
and /pages
examples use this config file:
// ./sanity.config.ts
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {schemaTypes} from './src/schema'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
export default defineConfig({
basePath: '/admin', // <-- important that `basePath` matches the route you're mounting your studio from, it applies to both `/pages` and `/app`
projectId,
dataset,
plugins: [deskTool()],
schema: {
types: schemaTypes,
},
})
This example assumes that there is a src/schema/index.ts
file that exports the schema definitions for Sanity Studio. However, you are free to structure Studio files as you see fit.
To run Sanity CLI commands, add a sanity.cli.ts
with the same projectId
and dataset
as your sanity.config.ts
to the project root:
// ./sanity.cli.ts
/* eslint-disable no-process-env */
import {loadEnvConfig} from '@next/env'
import {defineCliConfig} from 'sanity/cli'
const dev = process.env.NODE_ENV !== 'production'
loadEnvConfig(__dirname, dev, {info: () => null, error: console.error})
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
export default defineCliConfig({api: {projectId, dataset}})
Now you can run commands like npx sanity cors add
. Run npx sanity help
for a full list of what you can do.
// ./src/app/studio/[[...index]]/page.tsx
import {Studio} from './Studio'
// Ensures the Studio route is statically generated
export const dynamic = 'force-static'
// Set the right `viewport`, `robots` and `referer` meta tags
export {metadata} from 'next-sanity/studio/metadata'
export default function StudioPage() {
return <Studio />
}
// ./src/app/studio/[[...index]]/Studio.tsx
'use client'
import {NextStudio} from 'next-sanity/studio'
import config from '../../../sanity.config'
export function Studio() {
// Supports the same props as `import {Studio} from 'sanity'`, `config` is required
return <NextStudio config={config} />
}
How to customize meta tags:
// ./src/app/studio/[[...index]]/page.tsx
import type {Metadata} from 'next'
import {metadata as studioMetadata} from 'next-sanity/studio/metadata'
import {Studio} from './Studio'
// Set the right `viewport`, `robots` and `referer` meta tags
export const metadata: Metadata = {
...studioMetadata,
// Overrides the viewport to resize behavior
viewport: `${studioMetadata.viewport}, interactive-widget=resizes-content`,
}
export default function StudioPage() {
return <Studio />
}
// ./pages/studio/[[...index]].tsx
import Head from 'next/head'
import {NextStudio} from 'next-sanity/studio'
import {metadata} from 'next-sanity/studio/metadata'
import config from '../../sanity.config'
export default function StudioPage() {
return (
<>
<Head>
{Object.entries(metadata).map(([key, value]) => (
<meta key={key} name={key} content={value} />
))}
</Head>
<NextStudio config={config} />
</>
)
}
If you want to go to a lower level and have more control over the Studio, you can pass StudioProvider
and StudioLayout
from sanity
as children
:
import {NextStudio} from 'next-sanity/studio'
import {StudioProvider, StudioLayout} from 'sanity'
import config from '../../../sanity.config'
function StudioPage() {
return (
<NextStudio config={config}>
<StudioProvider config={config}>
{/* Put components here and you'll have access to the same React hooks as Studio gives you when writing plugins */}
<StudioLayout />
</StudioProvider>
</NextStudio>
)
}
- From
v4
tov5
- From
<0.4
tov4
MIT-licensed. See LICENSE.