BuildBot

Structured Content

Next.js integration

Lesson 3 of 5

What you'll learn

  • Configure the next-sanity client with CDN and API version options
  • Centralize data access in a sanityFetch helper that attaches cache tags
  • Choose between time-based ISR and tag-based revalidation

The next-sanity package wraps Sanity's client for the App Router. You configure it once — project, dataset, a pinned apiVersion (a date string, so query behavior never shifts under you), and useCdn to serve reads from Sanity's edge cache:

// sanity/client.ts
import {createClient} from 'next-sanity'

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  apiVersion: '2026-07-01',
  useCdn: true,
})

Rather than calling client.fetch all over the app, the house pattern is one sanityFetch helper that every page goes through. It forwards Next.js cache options — and this is where freshness strategy lives:

// sanity/fetch.ts
export async function sanityFetch<T>({
  query,
  params = {},
  tags = [],
}: {query: string; params?: object; tags?: string[]}): Promise<T> {
  return client.fetch<T>(query, params, {
    next: {revalidate: tags.length ? false : 60, tags},
  })
}

A server component then reads like this — no useEffect, no loading spinner, just awaited data at render time:

const posts = await sanityFetch<Post[]>({
  query: `*[_type == "post"]|order(publishedAt desc){title, "slug": slug.current}`,
  tags: ['post'],
})

Two freshness strategies

ISR (time-based): revalidate: 60 re-renders the page at most once a minute. Simple, but edits sit stale for up to the full window, and pages rebuild even when nothing changed.

Tag-based: pass tags: ['post'] and cache forever (revalidate: false). When content changes, something — in lesson 5, a webhook — calls revalidateTag('post'), and every cached fetch carrying that tag is evicted at once. Pages stay instant and exact: fresh within seconds of publish, zero wasted rebuilds in between.

Tag by type, then by document

A good starter scheme is one tag per type (post) plus one per document (post:hello-world). List pages take the type tag; detail pages take both. A single edit then flushes exactly the pages that show it.

The challenge models Next's data cache directly: entries carry tags, repeated fetches hit the cache, and revalidateTag evicts by tag — the exact lifecycle your pages will live through.

Tag-based revalidation (JS model)

Run it. Watch the second fetch hit the cache, then revalidateTag('post') evict it so the third fetch goes back to the network. Try tagging the author query with 'post' too.

Loading editor…
Knowledge check

With tag-based caching, what makes a page reflect a just-published edit?

Next: what's actually inside that body field — Portable Text, and rendering it with your own components.

Saved on this device. Sign in to sync your progress everywhere.