Gathering detailed insights and metrics for next-sanity
Gathering detailed insights and metrics for next-sanity
Gathering detailed insights and metrics for next-sanity
Gathering detailed insights and metrics for next-sanity
@sanity-typed/next-sanity
next-sanity with typed GROQ Results
next-sanity-image
Utility for using responsive images hosted on the Sanity.io CDN with the Next.js image component.
next-sanity-client
sanity client for next.js with support for app directory
@sanity/next-loader
[![npm stat](https://img.shields.io/npm/dm/@sanity/next-loader.svg?style=flat-square)](https://npm-stat.com/charts.html?package=@sanity/next-loader) [![npm version](https://img.shields.io/npm/v/@sanity/next-loader.svg?style=flat-square)](https://www.npmjs
npm install next-sanity
next-sanity: v9.8.18
Published on 27 Nov 2024
next-sanity: v9.8.17
Published on 27 Nov 2024
next-sanity: v9.8.16
Published on 19 Nov 2024
next-sanity: v9.8.15
Published on 15 Nov 2024
next-sanity: v9.8.14
Published on 15 Nov 2024
next-sanity: v9.8.13
Published on 13 Nov 2024
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
786 Stars
2,119 Commits
94 Forks
16 Watching
23 Branches
58 Contributors
Updated on 27 Nov 2024
TypeScript (92.17%)
JavaScript (7.64%)
CSS (0.19%)
Cumulative downloads
Total Downloads
Last day
13.1%
19,281
Compared to previous day
Last week
11.1%
96,930
Compared to previous week
Last month
19.2%
390,682
Compared to previous month
Last year
73.7%
3,760,189
Compared to previous year
8
9
25
The all-in-one Sanity toolkit for production-grade content-editable Next.js applications.
Features:
Quicklinks: Sanity docs |Â Next.js docs |Â Clean starter template |Â Fully-featured starter template
Instantly create a new free Sanity project – or link to an existing one – from the command line and connect it to your Next.js application by the following terminal command in your Next.js project folder:
1npx sanity@latest init
If you do not yet have a Sanity account you will be prompted to create one. This command will create basic utilities required to query content from Sanity. And optionally embed Sanity Studio - a configurable content management system - at a route in your Next.js application. See the Embedded Sanity Studio section.
If you do not yet have a Next.js application, you can create one with the following command:
1npx create-next-app@latest
This README assumes you have chosen all of the default options, but should be fairly similar for most bootstrapped Next.js projects.
next-sanity
Inside your Next.js application, run the following command in the package manager of your choice to install the next-sanity toolkit:
1npm install next-sanity @sanity/image-url
1yarn add next-sanity @sanity/image-url
1pnpm install next-sanity @sanity/image-url
1bun install next-sanity @sanity/image-url
This also installs @sanity/image-url
for On-Demand Image Transformations to render images from Sanity's CDN.
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 installed next-sanity
. In yarn
v1
you can use install-peerdeps
:
1npx install-peerdeps --yarn next-sanity
The npx sanity@latest init
command offers to write some configuration files for your Next.js application. Most importantly is one that writes your chosen Sanity project ID and dataset name to your local environment variables. Note that unlike access tokens, the project ID and dataset name are not considered sensitive information.
Create this file at the root of your Next.js application if it does not already exist.
1# .env.local 2 3NEXT_PUBLIC_SANITY_PROJECT_ID=<your-project-id> 4NEXT_PUBLIC_SANITY_DATASET=<your-dataset-name>
Create a file to access and export these values
1// ./src/sanity/env.ts 2 3export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET! 4export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID! 5 6// Values you may additionally want to configure globally 7export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-07-11'
Remember to add these environment variables to your hosting provider's environment as well.
next-sanity
exports the defineQuery
function which will give you syntax highlighting in VS Code with the Sanity extension installed. It’s also used for GROQ query result type generation with Sanity TypeGen.
1// ./src/sanity/lib/queries.ts
2
3import {defineQuery} from 'next-sanity'
4
5export const POSTS_QUERY = defineQuery(`*[_type == "post" && defined(slug.current)][0...12]{
6 _id, title, slug
7}`)
8
9export const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{
10 title, body, mainImage
11}`)
You can use Sanity TypeGen to generate TypeScript types for your schema types and GROQ query results in your Next.js application. It should be readily available if you have used sanity init
and chosen the embedded Studio.
[!TIP] Sanity TypeGen will create Types for queries that are assigned to a variable and use the
groq
template literal ordefineQuery
function.
If your Sanity Studio schema types are in a different project or repository, you can configure Sanity TypeGen to write types to your Next.js project.
Create a sanity-typegen.json
file at the root of your project to configure Sanity TypeGen:
1// sanity-typegen.json 2{ 3 "path": "./src/**/*.{ts,tsx,js,jsx}", 4 "schema": "./src/sanity/extract.json", 5 "generates": "./src/sanity/types.ts" 6}
Note: This configuration is strongly opinionated that the generated Types and the schema extraction are both within the /src/sanity
directory, not the root which is the default. This configuration is complimented by setting the path of the schema extraction in the updated package.json scripts below.
Run the following command in your terminal to extract your Sanity Studio schema to a JSON file
1# Run this each time your schema types change 2npx sanity@latest schema extract
Run the following command in your terminal to generate TypeScript types for both your Sanity Studio schema and GROQ queries
1# Run this each time your schema types or GROQ queries change 2npx sanity@latest typegen generate
Update your Next.js project's package.json
to perform both of these commands by running npm run typegen
1"scripts": { 2 "predev": "npm run typegen", 3 "dev": "next", 4 "prebuild": "npm run typegen", 5 "build": "next build", 6 "start": "next start", 7 "lint": "next lint", 8 "typegen": "sanity schema extract --path=src/sanity/extract.json && sanity typegen generate" 9},
Sanity TypeGen creates TypeScript types for the results of your GROQ queries, which can be used as generics like this:
1import {client} from '@/sanity/lib/client' 2import {POSTS_QUERY} from '@/sanity/lib/queries' 3import {POSTS_QUERYResult} from '@/sanity/types' 4 5const posts = await client.fetch<POSTS_QUERYResult>(POSTS_QUERY) 6// ^? const post: POST_QUERYResult
However, it is much simpler to use automatic type inference. So long as your GROQ queries are wrapped in defineQuery
, the results should be inferred automatically:
1import {client} from '@/sanity/lib/client' 2import {POSTS_QUERY} from '@/sanity/lib/queries' 3 4const posts = await client.fetch(POSTS_QUERY) 5// ^? const post: POST_QUERYResult
Sanity content is typically queried with GROQ queries from a configured Sanity Client. Sanity also supports GraphQL.
To interact with Sanity content in a Next.js application, we recommend creating a client.ts
 file:
1// ./src/sanity/lib/client.ts
2import {createClient} from 'next-sanity'
3
4import {apiVersion, dataset, projectId} from '../env'
5
6export const client = createClient({
7 projectId,
8 dataset,
9 apiVersion, // https://www.sanity.io/docs/api-versioning
10 useCdn: true, // Set to false if statically generating pages, using ISR or tag-based revalidation
11})
To fetch data in a React Server Component using the App Router you can await results from the Sanity Client inside a server component:
1// ./src/app/page.tsx 2 3import {client} from '@/sanity/lib/client' 4import {POSTS_QUERY} from '@/sanity/lib/queries' 5 6export default async function PostIndex() { 7 const posts = await client.fetch(POSTS_QUERY) 8 9 return ( 10 <ul> 11 {posts.map((post) => ( 12 <li key={post._id}> 13 <a href={`/posts/${post?.slug.current}`}>{post?.title}</a> 14 </li> 15 ))} 16 </ul> 17 ) 18}
If you're using the Pages Router you can await results from Sanity Client inside a getStaticProps
function:
1// ./src/pages/index.tsx 2 3import {client} from '@/sanity/lib/client' 4import {POSTS_QUERY} from '@/sanity/lib/queries' 5 6export async function getStaticProps() { 7 const posts = await client.fetch(POSTS_QUERY) 8 9 return {posts} 10} 11 12export default async function PostIndex({posts}) { 13 return ( 14 <ul> 15 {posts.map((post) => ( 16 <li key={post._id}> 17 <a href={`/posts/${post?.slug.current}`}>{post?.title}</a> 18 </li> 19 ))} 20 </ul> 21 ) 22}
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 queries. Since Next.js has its own caching, using the Sanity CDN might not be necessary, but there are some exceptions.
In general, set useCdn
to true
when:
useEffect
hook or in response to a user interaction where the client.fetch
call is made in the browser.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. You can configure the 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.
This toolkit includes the @sanity/client
which fully supports Next.js fetch
based features for caching and revalidation. This ensures 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 re-deploying the app will not purge the cache. We recommend reading up on caching behavior in the Next.js docs.
sanityFetch()
helper functionIt can be beneficial to set revalidation defaults for all queries. In all of the following examples, a sanityFetch()
helper function is used for this purpose.
While this function is written to accept both Next.js caching options revalidate
and tags
, your application should only rely on one. For this reason, if tags
are supplied, the revalidate
setting will be set to false
(cache indefinitely) and you will need to bust the cache for these pages using revalidateTag()
.
In short:
revalidate
is good enough for most applications.
revalidatePath()
.tags
will give you more fine-grained control for complex applications.
revalidateTag()
.1// ./src/sanity/lib/client.ts
2
3import {createClient, type QueryParams} from 'next-sanity'
4
5import {apiVersion, dataset, projectId} from '../env'
6
7export const client = createClient({
8 projectId,
9 dataset,
10 apiVersion, // https://www.sanity.io/docs/api-versioning
11 useCdn: true, // Set to false if statically generating pages, using ISR or tag-based revalidation
12})
13
14export async function sanityFetch<const QueryString extends string>({
15 query,
16 params = {},
17 revalidate = 60, // default revalidation time in seconds
18 tags = [],
19}: {
20 query: QueryString
21 params?: QueryParams
22 revalidate?: number | false
23 tags?: string[]
24}) {
25 return client.fetch(query, params, {
26 next: {
27 revalidate: tags.length ? false : revalidate, // for simple, time-based revalidation
28 tags, // for tag-based revalidation
29 },
30 })
31}
Be aware that you can get errors if you use cache
 and revalidate
 configurations for Next.js together. See the Next.js documentation on revalidation.
Time-based revalidation is often good enough for the majority of applications.
Increase the revalidate
setting for longer-lived and less frequently modified content.
1// ./src/app/pages/index.tsx 2 3import {sanityFetch} from '@/sanity/lib/client' 4import {POSTS_QUERY} from '@/sanity/lib/queries' 5 6export default async function PostIndex() { 7 const posts = await sanityFetch({ 8 query: POSTS_QUERY, 9 revalidate: 3600, // update cache at most once every hour 10 }) 11 12 return ( 13 <ul> 14 {posts.map((post) => ( 15 <li key={post._id}> 16 <a href={`/posts/${post?.slug.current}`}>{post?.title}</a> 17 </li> 18 ))} 19 </ul> 20 ) 21}
For on-demand revalidation of individual pages, Next.js has a revalidatePath()
function. You can create an API route in your Next.js application to execute it, and a GROQ-powered webhook in your Sanity Project to instantly request it when content is created, updated or deleted.
Create a new environment variable SANITY_REVALIDATE_SECRET
with a random string that is shared between your Sanity project and your Next.js application. This is considered sensitive and should not be committed to your repository.
1# .env.local 2 3SANITY_REVALIDATE_SECRET=<some-random-string>
Create a new API route in your Next.js application
The code example below uses the built-in parseBody
function to validate that the request comes from your Sanity project (using a shared secret and 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 application
1// ./src/app/api/revalidate-path/route.ts 2 3import {revalidatePath} from 'next/cache' 4import {type NextRequest, NextResponse} from 'next/server' 5import {parseBody} from 'next-sanity/webhook' 6 7type WebhookPayload = {path?: string} 8 9export async function POST(req: NextRequest) { 10 try { 11 if (!process.env.SANITY_REVALIDATE_SECRET) { 12 return new Response('Missing environment variable SANITY_REVALIDATE_SECRET', {status: 500}) 13 } 14 15 const {isValidSignature, body} = await parseBody<WebhookPayload>( 16 req, 17 process.env.SANITY_REVALIDATE_SECRET, 18 ) 19 20 if (!isValidSignature) { 21 const message = 'Invalid signature' 22 return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401}) 23 } else if (!body?.path) { 24 const message = 'Bad Request' 25 return new Response(JSON.stringify({message, body}), {status: 400}) 26 } 27 28 revalidatePath(body.path) 29 const message = `Updated route: ${body.path}` 30 return NextResponse.json({body, message}) 31 } catch (err) { 32 console.error(err) 33 return new Response(err.message, {status: 500}) 34 } 35}
Create a new GROQ-powered webhook in your Sanity project.
You can copy this template to quickly add the webhook to your Sanity project.
The Projection uses GROQ's select()
function to dynamically create paths for nested routes like /posts/[slug]
, you can extend this example your routes and other document types.
1{ 2 "path": select( 3 _type == "post" => "/posts/" + slug.current, 4 "/" + slug.current 5 ) 6}
[!TIP] If you wish to revalidate all routes on demand, create an API route that calls
revalidatePath('/', 'layout')
Tag-based revalidation is preferable for instances where many pages are affected by a single document being created, updated or deleted.
For on-demand revalidation of many pages, Next.js has a revalidateTag()
function. You can create an API route in your Next.js application to execute it, and a GROQ-powered webhook in your Sanity Project to instantly request it when content is created, updated or deleted.
1// ./src/app/pages/index.tsx 2 3import {sanityFetch} from '@/sanity/lib/client' 4import {POSTS_QUERY} from '@/sanity/lib/queries' 5 6export default async function PostIndex() { 7 const posts = await sanityFetch({ 8 query: POSTS_QUERY, 9 tags: ['post', 'author'], // revalidate all pages with the tags 'post' and 'author' 10 }) 11 12 return ( 13 <ul> 14 {posts.map((post) => ( 15 <li key={post._id}> 16 <a href={`/posts/${post?.slug.current}`}>{post?.title}</a> 17 </li> 18 ))} 19 </ul> 20 ) 21}
Create a new environment variable SANITY_REVALIDATE_SECRET
with a random string that is shared between your Sanity project and your Next.js application. This is considered sensitive and should not be committed to your repository.
1# .env.local 2 3SANITY_REVALIDATE_SECRET=<some-random-string>
Create a new API route in your Next.js application
The code example below uses the built-in parseBody
function to validate that the request comes from your Sanity project (using a shared secret and 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 application
1// ./src/app/api/revalidate-tag/route.ts 2 3import {revalidateTag} from 'next/cache' 4import {type NextRequest, NextResponse} from 'next/server' 5import {parseBody} from 'next-sanity/webhook' 6 7type WebhookPayload = { 8 _type: string 9} 10 11export async function POST(req: NextRequest) { 12 try { 13 if (!process.env.SANITY_REVALIDATE_SECRET) { 14 return new Response('Missing environment variable SANITY_REVALIDATE_SECRET', {status: 500}) 15 } 16 17 const {isValidSignature, body} = await parseBody<WebhookPayload>( 18 req, 19 process.env.SANITY_REVALIDATE_SECRET, 20 ) 21 22 if (!isValidSignature) { 23 const message = 'Invalid signature' 24 return new Response(JSON.stringify({message, isValidSignature, body}), {status: 401}) 25 } else if (!body?._type) { 26 const message = 'Bad Request' 27 return new Response(JSON.stringify({message, body}), {status: 400}) 28 } 29 30 // If the `_type` is `post`, then all `client.fetch` calls with 31 // `{next: {tags: ['post']}}` will be revalidated 32 revalidateTag(body._type) 33 34 return NextResponse.json({body}) 35 } catch (err) { 36 console.error(err) 37 return new Response(err.message, {status: 500}) 38 } 39}
Create a new GROQ-powered webhook in your Sanity project.
You can copy this template to quickly add the webhook to your Sanity project.
To aid in debugging and understanding what's in the cache, revalidated, skipped, and more, add the following to your Next.js configuration file:
1// ./next.config.js 2module.exports = { 3 logging: { 4 fetches: { 5 fullUrl: true, 6 }, 7 }, 8}
Check out the Personal website template to see a feature-complete example of how revalidateTag
is used together with Visual Editing.
Interactive live previews of draft content are the best way for authors to find and edit content with the least amount of effort and the most confidence to press publish.
[!TIP] Visual Editing is available on all Sanity plans and can be enabled on all hosting environments.
[!NOTE] Vercel "Content Link" adds an "edit" button to the Vercel toolbar on preview builds and is available on Vercel Pro and Enterprise plans.
An end-to-end tutorial of how to configure Sanity and Next.js for Visual Editing using the same patterns demonstrated in this README is available on the Sanity Exchange.
The Live Content API can be used to receive real time updates in your application when viewing both draft content in contexts like Presentation tool, and published content in your user-facing production application.
[!NOTE] The Live Content API is currently considered experimental and may change in the future.
defineLive
Use defineLive
to enable automatic revalidation and refreshing of your fetched content.
1// src/sanity/lib/live.ts
2
3import {createClient, defineLive} from 'next-sanity'
4
5const client = createClient({
6 projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
7 dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
8 useCdn: true,
9 apiVersion: 'vX', // Target the experimental API version
10 stega: {studioUrl: '/studio'},
11})
12
13const token = process.env.SANITY_API_READ_TOKEN
14if (!token) {
15 throw new Error('Missing SANITY_API_READ_TOKEN')
16}
17
18export const {sanityFetch, SanityLive} = defineLive({
19 client,
20 serverToken: token,
21 browserToken: token,
22})
The token
passed to defineLive
needs Viewer rights in order to fetch draft content.
The same token can be used as both browserToken
and serverToken
, as the browserToken
is only shared with the browser when Draft Mode is enabled. Draft Mode can only be initiated by either Sanity's Presentation Tool or the Vercel Toolbar.
Good to know: Enterprise plans allow the creation of custom roles with more resticted access rights than the
Viewer
role, enabling the use of abrowserToken
specifically for authenticating the Live Content API. We're working to extend this capability to all Sanity price plans.
<SanityLive />
in the root layout.tsx
1// src/app/layout.tsx 2 3import {VisualEditing} from 'next-sanity' 4import {SanityLive} from '@/sanity/lib/live' 5 6export default function RootLayout({children}: {children: React.ReactNode}) { 7 return ( 8 <html lang="en"> 9 <body> 10 {children} 11 <SanityLive /> 12 {(await draftMode()).isEnabled && <VisualEditing />} 13 </body> 14 </html> 15 ) 16}
The <SanityLive>
component is responsible for making all sanityFetch
calls in your application live, so should always be rendered. This differs from the <VisualEditing />
component, which should only be rendered when Draft Mode is enabled.
sanityFetch
Use sanityFetch
to fetch data in any server component.
1// src/app/products.tsx 2 3import {defineQuery} from 'next-sanity' 4import {sanityFetch} from '@/sanity/lib/live' 5 6const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`) 7 8export default async function Page() { 9 const {data: products} = await sanityFetch({ 10 query: PRODUCTS_QUERY, 11 params: {limit: 10}, 12 }) 13 14 return ( 15 <section> 16 {products.map((product) => ( 17 <article key={product._id}> 18 <a href={`/product/${product.slug}`}>{product.title}</a> 19 </article> 20 ))} 21 </section> 22 ) 23}
generateMetadata
, generateStaticParams
and moresanityFetch
can also be used in functions like generateMetadata
in order to make updating the page title, or even its favicon, live.
1import {sanityFetch} from '@/sanity/lib/live' 2import type {Metadata} from 'next' 3 4export async function generateMetadata(): Promise<Metadata> { 5 const {data} = await sanityFetch({ 6 query: SETTINGS_QUERY, 7 // Metadata should never contain stega 8 stega: false, 9 }) 10 return { 11 title: { 12 template: `%s | ${data.title}`, 13 default: data.title, 14 }, 15 } 16}
Good to know: Always set
stega: false
when callingsanityFetch
within these:
generateMetadata
generateViewport
generateSitemaps
generateImageMetadata
1import {sanityFetch} from '@/sanity/lib/live' 2 3export async function generateStaticParams() { 4 const {data} = await sanityFetch({ 5 query: POST_SLUGS_QUERY, 6 // Use the published perspective in generateStaticParams 7 perspective: 'published', 8 stega: false, 9 }) 10 return data 11}
To support previewing draft content when Draft Mode is enabled, the serverToken
passed to defineLive
should be assigned the Viewer role, which has the ability to fetch content using the previewDrafts
perspective.
Click the Draft Mode button in the Vercel toolbar to enable draft content:
With drafts enabled, you'll see the Edit Mode button show up if your Vercel plan is eligible:
Ensure that browserToken
is setup if you want draft content that isn't yet published to also update live.
The defineLive
API also supports Presentation Tool and Sanity Visual Editing.
Setup an API route that uses defineEnableDraftMode
in your app:
1// src/app/api/draft-mode/enable/route.ts 2 3import {client} from '@/sanity/lib/client' 4import {token} from '@/sanity/lib/token' 5import {defineEnableDraftMode} from 'next-sanity/draft-mode' 6 7export const {GET} = defineEnableDraftMode({ 8 client: client.withConfig({token}), 9})
The main benefit of defineEnableDraftMode
is that it fully implements all of Sanity Presentation Tool's features, including the perspective switcher:
And the Preview URL Sharing feature:
In your sanity.config.ts
, set the previewMode.enable
option for presentationTool
:
1// sanity.config.ts 2 3import {defineConfig} from 'sanity' 4import {presentationTool} from 'next-sanity' 5 6export default defineConfig({ 7 // ... 8 plugins: [ 9 // ... 10 presentationTool({ 11 previewUrl: { 12 // ... 13 previewMode: { 14 enable: '/api/draft-mode/enable', 15 }, 16 }, 17 }), 18 ], 19})
Ensuring you have a valid viewer token setup for defineLive.serverToken
and defineEnableDraftMode
allows Presentation Tool to auto enable Draft Mode, and your application to pull in draft content that refreshes in real time.
The defineLive.browserToken
option isn't required, but is recommended as it enables a faster live preview experience, both standalone and when using Presentation Tool.
Standalone live preview has the following requirements:
defineLive.serverToken
must be defined, otherwise only published content is fetched.defineLive.browserToken
must be defined with a valid token.You can verify if live preview is enabled with the useIsLivePreview
hook:
1'use client' 2 3import {useIsLivePreview} from 'next-sanity/hooks' 4 5export function DebugLivePreview() { 6 const isLivePreview = useIsLivePreview() 7 if (isLivePreview === null) return 'Checking Live Preview...' 8 return isLivePreview ? 'Live Preview Enabled' : 'Live Preview Disabled' 9}
The following hooks can also be used to provide information about the application's current environment:
1import { 2 useIsPresentationTool, 3 useDraftModeEnvironment, 4 useDraftModePerspective, 5} from 'next-sanity/hooks'
Live components will re-render automatically as content changes. This can cause jarring layout shifts in production when items appear or disappear from a list.
To provide a better user experience, we can animate these layout changes. The following example uses framer-motion@12.0.0-alpha.1
, which supports React Server Components:
1// src/app/products.tsx 2 3import {AnimatePresence} from 'framer-motion' 4import * as motion from 'framer-motion/client' 5import {defineQuery} from 'next-sanity' 6import {sanityFetch} from '@/sanity/lib/live' 7 8const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`) 9 10export default async function Page() { 11 const {data: products} = await sanityFetch({ 12 query: PRODUCTS_QUERY, 13 params: {limit: 10}, 14 }) 15 16 return ( 17 <section> 18 <AnimatePresence mode="popLayout"> 19 {products.map((product) => ( 20 <motion.article 21 key={product._id} 22 layout="position" 23 animate={{opacity: 1}} 24 exit={{opacity: 0}} 25 > 26 <a href={`/product/${product.slug}`}>{product.title}</a> 27 </motion.article> 28 ))} 29 </AnimatePresence> 30 </section> 31 ) 32}
Whilst this is an improvement, it may still lead to users attempting to click on an item as it shifts position, potentially resulting in the selection of an unintended item. We can instead require users to opt-in to changes before a layout update is triggered.
To preserve the ability to render everything on the server, we can make use of a Client Component wrapper. This can defer showing changes to the user until they've explicitly clicked to "Refresh". The example below uses sonner
to provide toast functionality:
1// src/app/products/products-layout-shift.tsx 2 3'use client' 4 5import {useCallback, useState, useEffect} from 'react' 6import isEqual from 'react-fast-compare' 7import {toast} from 'sonner' 8 9export function ProductsLayoutShift(props: {children: React.ReactNode; ids: string[]}) { 10 const [children, pending, startViewTransition] = useDeferredLayoutShift(props.children, props.ids) 11 12 /** 13 * We need to suspend layout shift for user opt-in. 14 */ 15 useEffect(() => { 16 if (!pending) return 17 18 toast('Products have been updated', { 19 action: { 20 label: 'Refresh', 21 onClick: () => startViewTransition(), 22 }, 23 }) 24 }, [pending, startViewTransition]) 25 26 return children 27} 28 29function useDeferredLayoutShift(children: React.ReactNode, dependencies: unknown[]) { 30 const [pending, setPending] = useState(false) 31 const [currentChildren, setCurrentChildren] = useState(children) 32 const [currentDependencies, setCurrentDependencies] = useState(dependencies) 33 34 if (!pending) { 35 if (isEqual(currentDependencies, dependencies)) { 36 if (currentChildren !== children) { 37 setCurrentChildren(children) 38 } 39 } else { 40 setCurrentDependencies(dependencies) 41 setPending(true) 42 } 43 } 44 45 const startViewTransition = useCallback(() => { 46 setCurrentDependencies(dependencies) 47 setPending(false) 48 }, [dependencies]) 49 50 return [pending ? currentChildren : children, pending, startViewTransition] as const 51}
This Client Component is used to wrap the layout that should only be updated after the user has clicked the refresh button:
1// src/app/products/page.tsx 2 3import { AnimatePresence } from "framer-motion"; 4import * as motion from "framer-motion/client"; 5import {defineQuery} from 'next-sanity' 6import { sanityFetch } from "@/sanity/lib/live"; 7+import {ProductsLayoutShift} from './products-page-layout-shift.tsx' 8 9const PRODUCTS_QUERY = defineQuery(`*[_type == "product" && defined(slug.current)][0...$limit]`) 10 11export default async function Page() { 12 const {data: products} = await sanityFetch({ query: PRODUCTS_QUERY, params: {limit: 10} }); 13+ // If the list over ids change, it'll trigger the toast asking the user to opt-in to refresh 14+ // but if a product title has changed, perhaps to fix a typo, we update that right away 15+ const ids = products.map((product) => product._id) 16 return ( 17 <section> 18+ <ProductsLayoutShift ids={ids}> 19 <AnimatePresence mode="popLayout"> 20 {products.map((product) => ( 21 <motion.article 22 key={product._id} 23 layout="position" 24 animate={{ opacity: 1 }} 25 exit={{ opacity: 0 }} 26 > 27 <a href={`/product/${product.slug}`}>{product.title}</a> 28 </motion.article> 29 ))} 30 </AnimatePresence> 31+ </ProductsLayoutShift> 32 </section> 33 ); 34}
With this approach we've limited the use of client components to just a single component. All the server components within <ProductsLayoutShift>
remain as server components, with all their benefits.
The architecture for defineLive
works as follows:
sanityFetch
automatically sets fetch.next.tags
for you using opaque tags generated by our backend, prefixed with sanity:
.<SanityLive />
listens to change events using the Sanity Live Content API (LCAPI).<SanityLive />
invokes a Server Function that calls revalidateTag(
sanity:${tag})
.revalidateTag
integrating with ISR.With this setup, as long as one visitor accesses your Next.js app after a content change, the cache is updated globally for all users, regardless of the specific URL they visit.
If your content operations involve scenarios where you might not always have a visitor to trigger the revalidateTag
event, there are two ways to ensure your content is never stale:
revalidateTag(sanity)
All queries made using sanityFetch
include the sanity
tag in their fetch.next.tags
array. You can use this to call revalidateTag('sanity')
in an API route that handles a GROQ webhook payload.
This approach can be considered a "heavy hammer" so it's important to limit the webhook events that trigger it. You could also implement this in a custom component to manually purge the cache if content gets stuck.
<SanityLive />
alternativeYou can setup your own long-running server, using Express for example, to listen for change events using the Sanity Live Content API. Then, create an API route in your Next.js app:
1// src/app/api/revalidate-tag/route.ts 2import {revalidateTag} from 'next/cache' 3 4export const POST = async (request) => { 5 const {tags, isValid} = await validateRequest(request) 6 if (!isValid) return new Response('No no no', {status: 400}) 7 for (const _tag of tags) { 8 const tag = `sanity:${_tag}` 9 revalidateTag(tag) 10 // eslint-disable-next-line no-console 11 console.log(`revalidated tag: ${tag}`) 12 } 13}
Your Express app can then forward change events to this endpoint, ensuring your content is always up-to-date. This method guarantees that stale content is never served, even if no browser is actively viewing your app!
Sanity Studio is a near-infinitely configurable content editing interface that can be embedded into any React application. For Next.js, you can embed the Studio on a route (like /studio
). The Studio will still require authentication and be available only for members of your Sanity project.
This opens up many possibilities including dynamic configuration of your Sanity Studio based on a network request or user input.
[!WARNING] The convenience of co-locating the Studio with your Next.js application is appealing, but it can also influence your content model to be too website-centric, and potentially make collaboration with other developers more difficult. Consider a standalone or monorepo Studio repository for larger projects and teams.
next-sanity
exports a <NextStudio />
component to load Sanity's <Studio />
component wrapped in a Next.js 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 connect an existing - or create a new - Sanity project to your Next.js application, run the following command in your terminal. You will be prompted to create a route for the Studio during setup.
1npx sanity@latest init
Create a file sanity.config.ts
in the project's root and copy the example below:
1// ./sanity.config.ts 2 3import {defineConfig} from 'sanity' 4import {structureTool} from 'sanity/structure' 5 6const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID! 7const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET! 8 9export default defineConfig({ 10 basePath: '/studio', // `basePath` must match the route of your Studio 11 projectId, 12 dataset, 13 plugins: [structureTool()], 14 schema: {types: []}, 15})
Optionally, create a sanity.cli.ts
with the same projectId
and dataset
as your sanity.config.ts
to the project root so that you can run npx sanity <command>
from the terminal inside your Next.js application:
1// ./sanity.cli.ts 2 3import {defineCliConfig} from 'sanity/cli' 4 5const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID! 6const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET! 7 8export 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 can and should mount the Studio on an App Router route. Next.js supports both routers in the same app.
Create a new route to render the Studio, with the default metadata and viewport configuration:
1// ./src/app/studio/[[...tool]]/page.tsx 2 3import {NextStudio} from 'next-sanity/studio' 4import config from '../../../../sanity.config' 5 6export const dynamic = 'force-static' 7 8export {metadata, viewport} from 'next-sanity/studio' 9 10export default function StudioPage() { 11 return <NextStudio config={config} /> 12}
The default meta tags exported by next-sanity
can be customized if necessary:
1// ./src/app/studio/[[...tool]]/page.tsx 2 3import type {Metadata, Viewport} from 'next' 4import {metadata as studioMetadata, viewport as studioViewport} from 'next-sanity/studio' 5 6// Set the correct `viewport`, `robots` and `referrer` meta tags 7export const metadata: Metadata = { 8 ...studioMetadata, 9 // Overrides the title until the Studio is loaded 10 title: 'Loading Studio...', 11} 12 13export const viewport: Viewport = { 14 ...studioViewport, 15 // Overrides the viewport to resize behavior 16 interactiveWidget: 'resizes-content', 17} 18 19export default function StudioPage() { 20 return <NextStudio config={config} /> 21}
StudioProvider
and StudioLayout
If you need even more control over the Studio, you can pass StudioProvider
and StudioLayout
from sanity
as children
:
1// ./src/app/studio/[[...tool]]/page.tsx 2 3'use client' 4 5import {NextStudio} from 'next-sanity/studio' 6import {StudioProvider, StudioLayout} from 'sanity' 7 8import config from '../../../sanity.config' 9 10function StudioPage() { 11 return ( 12 <NextStudio config={config}> 13 <StudioProvider config={config}> 14 {/* Put components here and you'll have access to the same React hooks as Studio gives you when writing plugins */} 15 <StudioLayout /> 16 </StudioProvider> 17 </NextStudio> 18 ) 19}
[!IMPORTANT] You're looking at the README for v9, the README for v8 is available here as well as an migration guide.
v8
to v9
v7
to v8
v6
to v7
v5
to v6
v4
to v5
<0.4
to v4
MIT-licensed. See LICENSE.
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
30 commit(s) and 10 issue activity found in the last 90 days -- score normalized to 10
Reason
GitHub workflow tokens follow principle of least privilege
Details
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
security policy file detected
Details
Reason
0 existing vulnerabilities detected
Reason
branch protection is not maximal on development and all release branches
Details
Reason
dependency not pinned by hash detected -- score normalized to 2
Details
Reason
Found 0/1 approved changesets -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Score
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More