next-sanity
The official Sanity.io toolkit for Next.js apps.
[!IMPORTANT]
You're looking at the README for v8, the README for v7 is available here as well as an migration guide.
Features:
Table of contents
Installation
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
Common dependencies
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
Peer dependencies for embedded Sanity Studio
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
Usage
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.
Quick start
To start running GROQ queries with next-sanity
, we recommend creating a client.ts
file:
import {createClient} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
})
App Router Components
To fetch data in a React Server Component using the App Router:
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>
)
}
Page Router Components
If you're using the Pages Router, then you can do the following from a page component:
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>
)
}
Should useCdn
be true
or false
?
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 the client.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
or getStaticPaths
. - 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.
How does apiVersion
work?
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).
Cache revalidation
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
Time-based revalidation is best for less complex cases and where content updates don't need to be immediately available.
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
}}
})
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 revalidation webhook
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.
import 'server-only'
import {createClient, type QueryParams} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: false,
})
export async function sanityFetch<QueryResponse>({
query,
params = {},
tags,
}: {
query: string
params?: QueryParams
tags?: string[]
}) {
return client.fetch<QueryResponse>(query, params, {
next: {
tags,
},
})
}
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:
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}) {
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:
import {revalidateTag} from 'next/cache'
import {type NextRequest, NextResponse} from 'next/server'
import {parseBody} from 'next-sanity/webhook'
type WebhookPayload = {
_type: string
}
export async function POST(req: NextRequest) {
try {
const {isValidSignature, body} = await parseBody<WebhookPayload>(
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(JSON.stringify({message, body}), {status: 400})
}
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.
Slug-based revalidation webhook
If you want on-demand revalidation, without using tags, 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}
.
import {revalidatePath} from 'next/cache'
import {type NextRequest, NextResponse} from 'next/server'
import {parseBody} from 'next-sanity/webhook'
type WebhookPayload = {
_type: string
slug?: {
current?: string
}
}
export async function POST(req: NextRequest) {
try {
const {isValidSignature, body} = await parseBody<WebhookPayload>(
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 || !body?.slug?.current) {
const message = 'Bad Request'
return new Response(JSON.stringify({message, body}), {status: 400})
}
const staleRoute = `/${body.slug.current}`
revalidatePath(staleRoute)
const message = `Updated route: ${staleRoute}`
return NextResponse.json({body, message})
} catch (err) {
console.error(err)
return new Response(err.message, {status: 500})
}
}
Working example implementation
Check out our Personal website template to see a feature-complete example of how revalidateTag
is used together with Live Previews.
Debugging caching and revalidation
To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}
Preview
There are different ways to set up content previews with Sanity and Next.js.
Using Perspectives
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.
import {createClient} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
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,
useCdn: true,
token,
perspective: 'published',
})
Live Preview
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
.
Using draftMode()
to de/activate previews
Next.js gives you a built-in draftMode
variable that can activate features like Visual Edit or any preview implementation.
import 'server-only'
import {draftMode} from 'next/headers'
import {createClient, type QueryOptions, type QueryParams} from 'next-sanity'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-03'
const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: false,
})
export const token = process.env.SANITY_API_READ_TOKEN
export async function sanityFetch<QueryResponse>({
query,
params = {},
tags,
}: {
query: string
params?: QueryParams
tags: string[]
}) {
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',
} satisfies QueryOptions)),
next: {
revalidate: isDraftMode ? REVALIDATE_SKIP_CACHE : REVALIDATE_CACHE_FOREVER,
tags,
},
})
}
Using cache
and revalidation
at the same time
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.
Visual Editing with Content Source Maps
Note
Vercel Visual Editing is available on Vercel's Pro and Enterprise plans and on all Sanity plans.
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
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION
const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
stega: {
enabled: NEXT_PUBLIC_VERCEL_ENV === 'preview',
studioUrl: '/studio',
},
})
Go to our setup guide for a walkthrough on how to customize the experience.
Embedded Sanity Studio
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.
See it live
Configuring Sanity Studio on a route
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
Manual installation
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:
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',
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:
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.
Studio route with App Router
Even if the rest of your app is using Pages Router, you should still mount the Studio on an App Router route. Next supports both routers in the same app.
import {Studio} from './Studio'
export const dynamic = 'force-static'
export {metadata, viewport} from 'next-sanity/studio'
export default function StudioPage() {
return <Studio />
}
'use client'
import {NextStudio} from 'next-sanity/studio'
import config from '../../../sanity.config'
export function Studio() {
return <NextStudio config={config} />
}
How to customize meta tags:
import type {Metadata, Viewport} from 'next'
import {metadata as studioMetadata, viewport as studioViewport} from 'next-sanity/studio'
import {Studio} from './Studio'
export const metadata: Metadata = {
...studioMetadata,
title: 'Loading Studio…',
}
export const viewport: Viewport = {
...studioViewport,
interactiveWidget: 'resizes-content',
}
export default function StudioPage() {
return <Studio />
}
Lower level control with StudioProvider
and StudioLayout
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
:
'use client'
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>
)
}
Migration guides
License
MIT-licensed. See LICENSE.