Use Sanity with Next.js

Query 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.

Prerequisites

To follow this guide, you need:

  • next-sanity 13 or later, which requires Next.js 16 or later.
  • A Sanity client (client.ts) and a live content setup (live.ts) in a sanity/lib/ folder. Running npx sanity@latest init in your Next.js project creates both.
  • Optional: Sanity TypeGen, if you want TypeScript to infer query result types.

Write and type GROQ queries

Write queries with 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 defineQuery result to client.fetch, TypeScript infers the return type from the query once you generate types with Sanity TypeGen. No manual type annotations needed.
  • Syntax highlighting: with the Sanity VS Code extension installed, GROQ inside defineQuery gets 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, generate types in two steps. Extract your schema with npx sanity@latest schema extract, then generate types with npx sanity@latest typegen generate. For manual setup, see the TypeGen documentation.

Fetch content

Choose 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 tags each request for cache revalidation, and <SanityLive /> uses those tags to refresh content when it changes. With Cache Components enabled, sanityFetch doesn't create a cache boundary, so call it inside your own 'use cache' function. 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 you must pass caching options such as next.revalidate on every call. 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. For production, use one of the sanityFetch approaches in "Choose a fetch method":

// 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>
  )
}

Pages Router

Use getStaticProps to fetch data at build time. This example includes revalidate: 60 to enable ISR, refreshing the data every 60 seconds. For ISR, set useCdn: false on the client you fetch with, so revalidation doesn't return content cached by the Sanity CDN:

// 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>
  )
}

Next steps

Continue with these guides:

Was this page helpful?

Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change site theme