
How to Connect Sanity With Next.js
To connect Sanity with Next.js, install next-sanity in your Next.js project, add your project ID and dataset as NEXT_PUBLIC_ environment variables, and create a client with createClient() from next-sanity, passing projectId, dataset, a date-based apiVersion and useCdn. You then call client.fetch() with a GROQ query inside an async Server Component and render the result. The fastest route is running npx sanity@latest init inside the Next.js folder, which links or creates a Sanity project, writes the config files and can embed Sanity Studio at /studio for you.
Getting the connection right matters because everything else builds on it: page rendering, caching, previews, images and Visual Editing all reuse the same client and environment variables. This guide covers the two ways to structure the project, automatic and manual setup, the client configuration options that trip people up, your first query and page, Sanity images in next/image, CORS and tokens, and how to choose a caching strategy. Once you're connected, the guide to building a blog with Next.js and Sanity takes you through a complete site.
What You Need Before You Start
The current tooling has firm version requirements, so check these first:
| Requirement | Version | Why |
|---|---|---|
| Node.js | 22.12 or later | Required by the sanity package and CLI |
| Next.js | 16.x with the App Router | next-sanity v13 requires Next.js 16 |
| React | 19.2 or later | Peer dependency of next-sanity v13 |
next-sanity | 13.x | Client, live content, Studio and Visual Editing helpers |
You also need a Sanity project with a dataset and at least one document type. If you don't have one yet, set up your first Sanity project and create your first schema, then come back here.
Choose a Project Structure
There are two common ways to organise Sanity and Next.js:
- Separate Studio and front end: the Studio lives in its own folder or repository (often a
studio/folder next to afrontend/folder in a monorepo) and is deployed to*.sanity.studioor your own host. The Next.js app only reads content. - Embedded Studio: the Studio runs as a route inside the Next.js app, usually
/studio, so there's one codebase and one deployment.
| Separate Studio | Embedded Studio | |
|---|---|---|
| Deployments | Two (site and Studio) | One |
| Studio auto-updates | Available on Sanity hosting | No, you upgrade sanity and redeploy |
| Content model coupling | Encourages a site-agnostic model | Easier to drift towards website-shaped content |
| Best for | Larger teams, multiple front ends | Small to medium sites, solo developers |
Neither choice changes how your pages fetch data. The client, queries and components in this guide work identically in both setups. If you want the embedded option, the guide to embedding Sanity Studio in a Next.js app covers it in depth.
Option 1: Automatic Setup With the CLI
Run the Sanity CLI inside an existing Next.js project:
npx create-next-app@latest my-site --ts --app --src-dir --tailwind --eslint --import-alias "@/*"
cd my-site
npx sanity@latest init
The CLI detects Next.js and walks you through a few prompts:
- Log in or create a Sanity account.
- Create a new project or choose an existing one, and pick a dataset.
- Decide whether to embed the Studio and at which route (the default is
/studio). - Write the config files:
sanity.config.ts,sanity.cli.ts, a Studio route and asrc/sanity/folder with the client, environment helpers and schema types. - Add
http://localhost:3000to your project's CORS origins if it isn't there already.
It also writes your project ID and dataset to .env.local. When it finishes, run npm run dev and you have a working connection. Read on to understand what was generated, or skip the CLI and do it by hand.
Option 2: Manual Setup
Step 1: Install the Packages
npm install next-sanity @sanity/image-url
next-sanity bundles @sanity/client with Next.js-specific helpers. With npm 7 or later, its peer dependencies (sanity and styled-components) are installed automatically, which you only need if you embed the Studio.
Step 2: Add Environment Variables
Find your project ID at sanity.io/manage or by running npx sanity projects list, then add it to .env.local:
# .env.local
NEXT_PUBLIC_SANITY_PROJECT_ID=abc123xy
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_API_VERSION=2025-02-19
The project ID and dataset aren't secrets. They appear in every API request your site makes, and the NEXT_PUBLIC_ prefix makes them available to client-side code such as live updates and an embedded Studio. Tokens are different, and we'll come to those shortly.
Step 3: Centralise and Validate the Values
A small env.ts file fails fast with a clear message when a variable is missing, instead of producing a confusing error later:
// src/sanity/env.ts
function assertValue<T>(value: T | undefined, message: string): T {
if (value === undefined) throw new Error(message)
return value
}
export const projectId = assertValue(
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID',
)
export const dataset = assertValue(
process.env.NEXT_PUBLIC_SANITY_DATASET,
'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET',
)
export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2025-02-19'
Step 4: Create the Client
// src/sanity/lib/client.ts
import { createClient } from 'next-sanity'
import { apiVersion, dataset, projectId } from '../env'
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true,
})
That's the whole connection. Every other feature, from live updates to Visual Editing, extends this client rather than replacing it.
Understand the Client Options
Four options control how the client behaves, and each one is worth understanding:
- projectId and dataset: which project and dataset to read. Use a different dataset per environment if you have staging content.
- apiVersion: a date string that pins API behaviour. Use a fixed recent date rather than today's date, so a deploy next month doesn't silently change results. Versions from
2025-02-19onwards return only published documents by default. - useCdn:
truesends queries to Sanity's API CDN, which is faster and cached at the edge.falsequeries the live API directly, which is always fresh. The guide to the CDN vs the live API goes into the trade-offs. - perspective: which view of the content to return, such as
publishedordrafts. You rarely set this on the base client, because the default is alreadypublishedwith a modern API version.
You can create variations of the client for specific jobs with withConfig(), which returns a new instance and leaves the original unchanged:
import { client } from '@/sanity/lib/client'
// Always fresh, for build-time work
export const freshClient = client.withConfig({ useCdn: false })
Write Your First Query and Page
Define queries with defineQuery() from next-sanity. It returns the query string unchanged, but lets Sanity TypeGen find the query and generate a result type for it:
// src/sanity/lib/queries.ts
import { defineQuery } from 'next-sanity'
export const POSTS_QUERY = defineQuery(`*[_type == "post" && defined(slug.current)]
| order(publishedAt desc)[0...12]{
_id,
title,
"slug": slug.current,
publishedAt
}`)
If GROQ is new to you, the GROQ cheat sheet covers the filter, order and projection syntax used here. Now fetch it in a Server Component. App Router pages are Server Components by default, so you can await directly in the component body:
// src/app/page.tsx
import Link from 'next/link'
import { client } from '@/sanity/lib/client'
import { POSTS_QUERY } from '@/sanity/lib/queries'
const options = { next: { revalidate: 60 } }
export default async function HomePage() {
const posts = await client.fetch(POSTS_QUERY, {}, options)
return (
<main className="mx-auto max-w-3xl p-8">
<h1 className="mb-8 text-4xl font-bold">Posts</h1>
<ul className="space-y-4">
{posts.map((post) => (
<li key={post._id}>
<Link href={`/posts/${post.slug}`} className="hover:underline">
{post.title}
</Link>
</li>
))}
</ul>
</main>
)
}
The third argument to fetch() passes Next.js caching options through to the underlying request. Here the result is cached and refreshed at most once a minute. Because this runs on the server, no Sanity code or query is shipped to the browser.
Pass Parameters, Never Concatenate
For a single post, pass the slug as a parameter rather than building the query string yourself. This avoids injection problems and lets the API cache queries properly:
export const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{
title,
body,
mainImage
}`)
const post = await client.fetch(POST_QUERY, { slug }, options)
The post on GROQ parameters explains why this matters.
Display Sanity Images
Sanity serves images from cdn.sanity.io with on-demand transformations. To use them with next/image, first allow the domain:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: 'https', hostname: 'cdn.sanity.io' }],
},
}
export default nextConfig
Then create a URL builder with the named createImageUrlBuilder export from @sanity/image-url:
// src/sanity/lib/image.ts
import { createImageUrlBuilder, type SanityImageSource } from '@sanity/image-url'
import { dataset, projectId } from '../env'
const builder = createImageUrlBuilder({ projectId, dataset })
export function urlFor(source: SanityImageSource) {
return builder.image(source)
}
import Image from 'next/image'
import { urlFor } from '@/sanity/lib/image'
<Image
src={urlFor(post.mainImage).width(1200).height(630).url()}
alt={post.mainImage?.alt ?? ''}
width={1200}
height={630}
preload
/>
In Next.js 16, preload replaces the older priority prop for above-the-fold images. For cropping, responsive sizes and the next-sanity/image component that avoids double processing, see Sanity's image URL builder.
CORS, Tokens and Security
Server-side fetches from Next.js don't need CORS, because CORS only applies to browsers. You do need a CORS origin when the browser talks to Sanity directly, which happens with an embedded Studio, live updates and Visual Editing:
npx sanity cors add http://localhost:3000 --credentials
npx sanity cors add https://www.example.com --credentials
Tokens are needed when you read private data, such as drafts in a public dataset or anything in a private dataset. Create a token with Viewer permissions under API > Tokens in your project settings and store it without the public prefix:
# .env.local
SANITY_API_READ_TOKEN=sk...
A few rules keep tokens safe:
- Never prefix tokens with NEXT_PUBLIC_: anything with that prefix is inlined into the browser bundle.
- Use the least privilege: a Viewer token for reading, and a separate Editor token only on server code that writes.
- Add tokens per request:
client.withConfig({ token })in server code, rather than on the shared client.
The guide to securing your Sanity project covers tokens and CORS in more detail.
Pick a Caching Strategy
How fresh your pages are depends on how you cache Sanity responses. There are three realistic options:
| Strategy | How it works | Freshness | Effort |
|---|---|---|---|
| Time-based | next: { revalidate: 60 } on each fetch | Within the interval | Low |
| On-demand | Tag fetches, call revalidateTag() from a webhook | Seconds after publishing | Medium |
| Sanity Live | defineLive() returns sanityFetch and SanityLive | Updates driven by the Live Content API | Low to medium |
Time-based revalidation is a fine way to start. For most production sites, Sanity recommends defineLive from next-sanity/live, which wraps your client and handles cache tags and revalidation for you:
// src/sanity/lib/live.ts
import { defineLive } from 'next-sanity/live'
import { client } from './client'
const token = process.env.SANITY_API_READ_TOKEN
export const { sanityFetch, SanityLive } = defineLive({
client,
serverToken: token,
browserToken: token,
})
You render <SanityLive /> once in your root layout and use sanityFetch({ query, params }) instead of client.fetch(). The details, including Draft Mode and Cache Components, are in the guide to using Sanity with the Next.js App Router.
Troubleshooting the Connection
When nothing comes back, or the wrong thing does, work through these:
- Empty array: check the dataset name, that documents are published (drafts aren't returned without a token) and that the
_typein your filter matches the schema name exactly. - Missing environment variable error: restart
next devafter editing.env.local, and add the same variables in your hosting dashboard for production builds. - Old content after publishing: something is caching. Check your
revalidatevalue, and remember thatuseCdn: truecan add a short delay of its own. - CORS error in the browser console: add the exact origin, including protocol and port, with
--credentials. - Hostname not configured for next/image: add
cdn.sanity.iotoimages.remotePatternsand restart the dev server.
FAQ: Connecting Sanity With Next.js
You can use @sanity/client on its own, since next-sanity's createClient wraps it. But next-sanity adds the pieces you'll want in a Next.js app, such as defineQuery, defineLive for live content, Draft Mode helpers, the embedded Studio component and Visual Editing, so it's the recommended choice.
Start with true for published content, because the API CDN is faster and reduces load. Use false for build-time work that must be fresh, webhook-triggered revalidation and draft previews. If you use sanityFetch from defineLive, it chooses the right setting per request based on the perspective.
Yes. The project ID and dataset name are visible in every API request your site makes, so they aren't secrets. What protects private content is dataset visibility and tokens, which must stay in server-only environment variables.
It does, but next-sanity v13 and Sanity's current guides target the App Router. If you have a Pages Router app, you can still fetch with the client in getStaticProps or getServerSideProps, and you can mount an embedded Studio on an App Router route alongside your pages.
Use a fixed date string, such as 2025-02-19 or a more recent date, and keep it the same across your client, live setup and scripts. Versions from 2025-02-19 onwards default to the published perspective. Only change it deliberately, after checking the API changelog.
By default you only get published content, which is what visitors should see. To show drafts you need a token with read access and the drafts perspective, which is what Draft Mode and Visual Editing set up for you during previews.
Conclusion
Connecting Sanity to Next.js takes three files: validated environment variables, a createClient() call from next-sanity, and a Server Component that awaits client.fetch() with a GROQ query. Running npx sanity@latest init creates them for you, while the manual route shows exactly what each piece does. Add cdn.sanity.io to your image config, keep tokens out of NEXT_PUBLIC_ variables and add CORS origins for anything that runs in the browser.
From there, the main decision is caching. Time-based revalidation gets you started, and defineLive gives you automatic, content-driven updates when you're ready. With the connection in place, you can move on to a full Next.js and Sanity blog, click-to-edit Visual Editing or the details of the next-sanity toolkit.


