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:
npm install @sanity/clientConfigure a Sanity Client
Create a utility at lib/sanity.js:
// lib/sanity.jsimport { createClient } from '@sanity/client'export const client = createClient({projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,dataset: 'production',apiVersion: '2025-01-01', // lock version for stabilityuseCdn: process.env.NODE_ENV === 'production', // cache in prod, fresh in dev})Add to .env.local:
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_idWrite GROQ Queries to Fetch Content
Start with a query to fetch the content fields your application needs from Sanity.
export const postsQuery = `*[_type == "post"] | order(publishedAt desc) {_id,title,slug,publishedAt,excerpt,mainImage {asset-> { url }}}`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:
export const postDetailQuery = `*[_type == "post" && slug.current == $slug][0]{_id,title,slug,publishedAt,body,mainImage {asset-> { url }}}`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.
// lib/api.jsimport { client } from './sanity'import { postsQuery } from './queries'// Fetch all blog posts (summary content for listing pages)export async function getAllPosts() {return client.fetch(postsQuery)}// Fetch single post details by slug (full content including body)export async function getPostBySlug(slug) {const query = `*[_type == "post" && slug.current == $slug][0]{_id,title,slug,publishedAt,body,mainImage {asset-> { url }}}`return client.fetch(query, { slug })}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.
// lib/previewClient.js (server-only)import { createClient } from '@sanity/client'export const previewClient = createClient({projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',apiVersion: '2025-01-01',useCdn: false, // Disable CDN for fresh draft contenttoken: process.env.SANITY_READ_TOKEN, // Server-only token with read access to drafts})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:
import { getAllPosts } from '../../lib/api'export default function Blog({ posts }) {return (<div><h1>Blog</h1><ul>{posts.map(post => (<li key={post._id}><a href={`/blog/${post.slug.current}`}>{post.title}</a></li>))}</ul></div>)}export async function getStaticProps() {const posts = await getAllPosts()return { props: { posts }, revalidate: 60 }}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:
import { client } from '../../lib/sanity'export default function Post({ post }) {if (!post) return <p>Post not found</p>return (<article><h1>{post.title}</h1><p>{post.publishedAt}</p><div>{post.body}</div></article>)}export async function getStaticPaths() {const slugs = await client.fetch(`*[_type == "post"]{ "slug": slug.current }`)const paths = slugs.map(slug => ({ params: { slug: slug.slug } }))return { paths, fallback: 'blocking' }}export async function getStaticProps({ params }) {const post = await client.fetch(`*[_type == "post" && slug.current == $slug][0]{title,publishedAt,body}`,{ slug: params.slug })return { props: { post }, revalidate: 60 }}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:
export default function handler(req, res) {if (req.query.secret !== process.env.SANITY_PREVIEW_SECRET) {return res.status(401).json({ message: 'Invalid secret' })}res.setPreviewData({})res.redirect(req.query.slug ? `/blog/${req.query.slug}` : '/')}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.
// lib/sanity.jsexport const client = createClient({projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,dataset: 'production',apiVersion: '2025-01-01',useCdn: false, // always fetch fresh data in preview})Step 3: Secure Setup
Add an environment variable in .env.local:
SANITY_PREVIEW_SECRET=mySecret123