BuildBot

Structured Content

Drafts, preview & webhooks

Lesson 5 of 5

What you'll learn

  • Understand draft/published document pairs and the drafts. id prefix
  • Use the perspective option to query published content or a drafts overlay
  • Wire a GROQ-powered webhook to tag-based revalidation

When an editor changes a published document, Sanity doesn't touch the published version. It writes a sibling document whose _id is the published id with a drafts. prefix: post-123 keeps serving production while drafts.post-123 accumulates edits. Publishing copies the draft over the published document and deletes the draft; unpublished work exists only as a drafts.* document.

Which of the pair a query sees is the perspective:

  • perspective: 'published' — published documents only. Production traffic.
  • perspective: 'drafts' — each draft overlays its published twin, and unpublished drafts appear too. Exactly what an editor expects preview to show.
export const previewClient = client.withConfig({
  perspective: 'drafts',
  useCdn: false,
  token: process.env.SANITY_VIEWER_TOKEN, // drafts require auth
})

Preview mode

Sanity's Presentation tool embeds your site next to the Studio: editors click into the page, edit fields, and watch the live site update before publishing. Under the hood it's Next.js draft mode — a route enables draftMode(), and your data layer switches client:

import {draftMode} from 'next/headers'

const {isEnabled} = await draftMode()
const data = await (isEnabled ? previewClient : client).fetch(query, params)

Webhooks close the loop

Lesson 3 cached pages until revalidateTag fires — a GROQ-powered webhook is what fires it. In sanity.io/manage you give the webhook a filter (which documents trigger it) and a projection (what the payload contains):

// Filter: fire on publish/unpublish of posts
_type == "post"
// Projection: exactly what the revalidate route needs
{ "tag": _type, "slug": slug.current }

Your route handler verifies the signature (parseBody from next-sanity/webhook), then calls revalidateTag(body.tag). Publish → webhook → eviction → next request renders fresh. Seconds of latency, zero rebuild queue.

Preview must never leak

The drafts perspective needs a token with read access to drafts — keep that client server-side only, and gate it behind draftMode(). A drafts-perspective fetch on a public page is unpublished content in production.

The challenge models the heart of all this: one content lake, two perspectives. The same query returns different worlds depending on who's asking.

Perspectives over draft pairs (JS model)

Run it. 'published' ignores drafts entirely; 'drafts' overlays each draft on its published twin and includes never-published drafts. Try 'publishing' p2 by copying its draft into a published doc.

Loading editor…
Knowledge check

How does Sanity store an in-progress edit to a published document?

That's the course: content modeled in code, queried with GROQ, cached and revalidated in Next.js, rendered from structured blocks, and kept honest by drafts, preview, and webhooks — a CMS pipeline you can reason about end to end.

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