In modern web applications, Next.js manages rendering, routing, and performance, while Sanity provides a structured backend for storing and delivering content. It keeps the frontend focused on user experience while content is managed independently.

Content is retrieved from Sanity with GROQ queries and consumed by Next.js to pre-render pages, generate routes, and support live previews. Editors update content directly in Sanity, and changes appear on the frontend without requiring a redeploy. This makes the workflow efficient for both content teams and developers.

Prerequisites

Before integrating Sanity with Next.js, make sure you have:

  • Set up a Sanity Project: You can create a new Sanity project by following this official documentation: Installation | Sanity Docs
  • Basic Next.js Knowledge (Routing, `getStaticProps`, etc.)
  • Install Node.js on your development machine
  • Set Up a Next.js Project: You can create a new Next.js project by following this official documentation: Getting Started: Installation

Fetching Content from Sanity

To connect Sanity and Next.js, the first step is to set up a client and write queries. This makes structured content available to your application in a predictable format, which keeps rendering logic consistent across pages.

Installing Dependencies

In your Next.js project, install the Sanity client:

code
npm install @sanity/client
code

Configure a Sanity Client

Create a utility at lib/sanity.js:

code
// lib/sanity.js
code
import { createClient } from '@sanity/client'
code
export const client = createClient({
code
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
code
dataset: 'production',
code
apiVersion: '2025-01-01', // lock version for stability
code
useCdn: process.env.NODE_ENV === 'production', // cache in prod, fresh in dev
code
})
code

Add to .env.local:

code
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id

Write GROQ Queries to Fetch Content

Start with a query to fetch the content fields your application needs from Sanity.

code
export const postsQuery = `*[_type == "post"] | order(publishedAt desc) {
code
_id,
code
title,
code
slug,
code
publishedAt,
code
excerpt,
code
mainImage {
code
asset-> { url }
code
}
code
}`

This query fetches posts with their IDs, titles, slugs, publication dates, excerpts, and main image URLs, which is optimal for listing pages where you want summary information.

If you want to fetch full content including the body (usually for single post pages), you would add body like this:

code
export const postDetailQuery = `*[_type == "post" && slug.current == $slug][0]{
code
_id,
code
title,
code
slug,
code
publishedAt,
code
body,
code
mainImage {
code
asset-> { url }
code
}
code
}`

This separates summary fetches from detail fetches cleanly and matches common Sanity + Next.js usage patterns.

Create a Reusable API Helper

Summarizing GROQ queries into API helper functions avoids duplication and centralizes access to your structured content data; the individual pieces of information (like titles, slugs, excerpts, and images) that compose your documents in Sanity.

code
// lib/api.js
code
import { client } from './sanity'
code
import { postsQuery } from './queries'
code
// Fetch all blog posts (summary content for listing pages)
code
export async function getAllPosts() {
code
return client.fetch(postsQuery)
code
}
code
// Fetch single post details by slug (full content including body)
code
export async function getPostBySlug(slug) {
code
const query = `*[_type == "post" && slug.current == $slug][0]{
code
_id,
code
title,
code
slug,
code
publishedAt,
code
body,
code
mainImage {
code
asset-> { url }
code
}
code
}`
code
return client.fetch(query, { slug })
code
}

Now, these functions can be imported wherever your Next.js app needs consistent, predictable access to blog post content. It reduces repetitive query code and improves maintainability.

Drafts / Preview Fetches

To support live previewing of unpublished draft content in your Next.js app, use a server-side authenticated Sanity client configured with a read token and useCdn: false. This ensures fresh draft data is fetched securely without using cached published content.

code
// lib/previewClient.js (server-only)
code
import { createClient } from '@sanity/client'
code
export const previewClient = createClient({
code
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
code
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
code
apiVersion: '2025-01-01',
code
useCdn: false, // Disable CDN for fresh draft content
code
token: process.env.SANITY_READ_TOKEN, // Server-only token with read access to drafts
code
})

This specialized client complements the reusable API helpers by enabling your app to fetch draft content securely and efficiently during preview mode, improving the editing and review workflow for content teams.

Displaying Content in Next.js

With content available, the next step is to render it. Using static generation ensures pages load fast and are search-friendly, while incremental static regeneration (ISR) allows content updates from Sanity to appear on the site without redeploying.

Creating a Page (e.g., /blog) to List Posts

Create a file at pages/blog/index.js:

code
import { getAllPosts } from '../../lib/api'
code
export default function Blog({ posts }) {
code
return (
code
<div>
code
<h1>Blog</h1>
code
<ul>
code
{posts.map(post => (
code
<li key={post._id}>
code
<a href={`/blog/${post.slug.current}`}>{post.title}</a>
code
</li>
code
))}
code
</ul>
code
</div>
code
)
code
}
code
export async function getStaticProps() {
code
const posts = await getAllPosts()
code
return { props: { posts }, revalidate: 60 }
code
}

Dynamic Routes for Each Post

Dynamic routes let you render each post individually, ensuring scalability as the number of posts grows. Create a dynamic route at pages/blog/[slug].js:

code
import { client } from '../../lib/sanity'
code
export default function Post({ post }) {
code
if (!post) return <p>Post not found</p>
code
return (
code
<article>
code
<h1>{post.title}</h1>
code
<p>{post.publishedAt}</p>
code
<div>{post.body}</div>
code
</article>
code
)
code
}
code
export async function getStaticPaths() {
code
const slugs = await client.fetch(`*[_type == "post"]{ "slug": slug.current }`)
code
const paths = slugs.map(slug => ({ params: { slug: slug.slug } }))
code
return { paths, fallback: 'blocking' }
code
}
code
export async function getStaticProps({ params }) {
code
const post = await client.fetch(
code
`*[_type == "post" && slug.current == $slug][0]{
code
title,
code
publishedAt,
code
body
code
}`,
code
{ slug: params.slug }
code
)
code
return { props: { post }, revalidate: 60 }
code
}

Optional: Enabling Live Preview

Preview mode lets editors see draft changes in real time on the frontend. This reduces the risk of publishing errors and creates a smoother workflow for content teams.

Step 1: Enable Preview Mode in Next.js

Next.js has a built-in Preview Mode. You need to create an API route at pages/api/preview.js:

code
export default function handler(req, res) {
code
if (req.query.secret !== process.env.SANITY_PREVIEW_SECRET) {
code
return res.status(401).json({ message: 'Invalid secret' })
code
}
code
res.setPreviewData({})
code
res.redirect(req.query.slug ? `/blog/${req.query.slug}` : '/')
code
}
code

Step 2: Adjust Client for Draft Content

When in preview mode, set useCdn: false in your Sanity client to fetch the most recent draft content.

code
// lib/sanity.js
code
export const client = createClient({
code
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
code
dataset: 'production',
code
apiVersion: '2025-01-01',
code
useCdn: false, // always fetch fresh data in preview
code
})

Step 3: Secure Setup

Add an environment variable in .env.local:

code
SANITY_PREVIEW_SECRET=mySecret123