Querying content in Next.js
Use the next-sanity library to write typed GROQ queries with defineQuery and fetch content in App Router or Pages Router.
The next-sanity library wraps @sanity/client with helpers designed specifically for Next.js. Use it to write typed GROQ queries, fetch content in both App Router and Pages Router, and enable live content updates. It bundles:
- GROQ helpers that make TypeGen setup easier.
- Live content and visual editing tooling for fetching fresh content from different perspectives.
Writing and typing GROQ queries
defineQuery
Use defineQuery to write GROQ queries with TypeGen support and syntax highlighting:
// src/sanity/lib/queries.ts
import { defineQuery } from 'next-sanity'
export const POSTS_QUERY = defineQuery(
`*[_type == "post" && defined(slug.current)][0...12]{
_id, title, slug
}`
)
export const POST_QUERY = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{
title, body, mainImage
}`
)defineQuery enables two things:
- Automatic type inference: when you pass a
defineQueryresult toclient.fetch, TypeScript infers the return type from the query. No manual type annotations needed. - Syntax highlighting: with the Sanity VS Code extension installed, GROQ inside
defineQuerygets full syntax highlighting.
next-sanity also exports a groq template tag for backward compatibility, but defineQuery is recommended for new projects.
TypeGen setup
Sanity TypeGen generates TypeScript types for your schema and GROQ query results. If you used sanity init with an embedded Studio, TypeGen is ready to use. For manual setup, see the TypeGen documentation.
Fetching content
Choosing a fetch method
Recommended: defineLive's sanityFetch. If you're using the Live Content API (most apps should), import sanityFetch from your live.ts file. This function handles caching, revalidation, and live updates automatically. See the Visual Editing or Live Content guide.
Alternative: manual sanityFetch helper. For apps that don't use the Live Content API, you build a wrapper around client.fetch with explicit caching options. See Caching and revalidation.
Basic: client.fetch directly. Works for simple cases but gives you no caching control beyond Next.js defaults. See the next-sanity client configuration for additional settings.
App Router
The following example uses client.fetch directly in a Server Component. This works for prototyping, but for production you should use one of the sanityFetch approaches described above:
// src/app/page.tsx
import { client } from '@/sanity/lib/client'
import { POSTS_QUERY } from '@/sanity/lib/queries'
export default async function PostIndex() {
const posts = await client.fetch(POSTS_QUERY)
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={`/posts/${post?.slug.current}`}>{post?.title}</a>
</li>
))}
</ul>
)
}For production, replace client.fetch with one of the sanityFetch approaches described above.
Pages Router
Use getStaticProps to fetch data at build time. This example includes revalidate: 60 to enable ISR, refreshing the data every 60 seconds:
// src/pages/index.tsx
import type { InferGetStaticPropsType } from 'next'
import { client } from '@/sanity/lib/client'
import { POSTS_QUERY } from '@/sanity/lib/queries'
export async function getStaticProps() {
const posts = await client.fetch(POSTS_QUERY)
return { props: { posts }, revalidate: 60 }
}
export default function PostIndex({
posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={`/posts/${post?.slug.current}`}>{post?.title}</a>
</li>
))}
</ul>
)
}The Live Content API (defineLive, SanityLive) is App Router only. Pages Router apps can use client.fetch directly with ISR (revalidate in getStaticProps) for cache management.