Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
next-sanity
Advanced tools
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:
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 Visual Editing:
npm install @sanity/image-url @sanity/react-loader
yarn add @sanity/image-url @sanity/react-loader
pnpm install @sanity/image-url @sanity/react-loader
bun install @sanity/image-url @sanity/react-loader
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'
export 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>
)
}
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:
useEffect
hook or in response to a user interaction where the client.fetch
call is made in the browser.And it makes sense to set useCdn
to false
when:
getStaticProps
or getStaticPaths
.stale-while-revalidate
caching is in place that keeps API requests on a consistent low, even if traffic to Next.js spikes.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).
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 {createClient, type QueryParams} 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,
})
export async function sanityFetch<QueryResponse>({
query,
params = {},
tags,
}: {
query: string
params?: QueryParams
tags?: string[]
}) {
return client.fetch<QueryResponse>(query, params, {
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/route.ts
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})
}
// If the `_type` is `page`, then all `client.fetch` calls with
// `{next: {tags: ['page']}}` will be revalidated
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 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}
.
// ./src/app/api/revalidate/route.ts
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})
}
}
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 = {
logging: {
fetches: {
fullUrl: true,
},
},
}
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
.
draftMode()
to de/activate previewsNext.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 {createClient, type QueryOptions, type QueryParams} 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,
})
// Used by `PreviewProvider`
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,
},
})
}
cache
and revalidation
at the same timeBe 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
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 // "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
stega: {
enabled: NEXT_PUBLIC_VERCEL_ENV === 'preview', // this can also be controlled in `client.fetch(query, params, {stega: boolean})`
studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
},
})
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:
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:
// ./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
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.
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.
// ./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, viewport} from 'next-sanity/studio'
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, Viewport} from 'next'
import {metadata as studioMetadata, viewport as studioViewport} from 'next-sanity/studio'
import {Studio} from './Studio'
// Set the right `viewport`, `robots` and `referer` meta tags
export const metadata: Metadata = {
...studioMetadata,
// Overrides the title until the Studio is loaded
title: 'Loading Studio…',
}
export const viewport: Viewport = {
...studioViewport,
// Overrides the viewport to resize behavior
interactiveWidget: 'resizes-content',
}
export default function StudioPage() {
return <Studio />
}
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>
)
}
MIT-licensed. See LICENSE.
FAQs
Sanity.io toolkit for Next.js
We found that next-sanity demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 64 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.