Implementation Patterns7 min read

How to Implement Real-Time Preview With Sanity and Next.js

Your editor publishes a change, then pings you: "Why isn't it live?" You open the browser, hit refresh, and nothing.

Published July 3, 2026

Your editor publishes a change, then pings you: "Why isn't it live?" You open the browser, hit refresh, and nothing. So you go dig through webhook logs, re-run a cache purge, and hand back a "should be fixed now" that you only half believe. Preview that lags behind reality is the quiet tax on every headless build. Editors stop trusting the CMS, writers copy-paste into a staging doc to see their work, and engineers become a human cache-invalidation service.

Sanity is the Content Operating System for the AI era, an intelligent backend that treats structured content in Content Lake as a single source of truth you can serve to any frontend in real time. Paired with Next.js, that means an editor can click a headline in the live page, land on the exact field in the Studio, edit it, and watch the page re-render within seconds, with no webhook wiring and no manual purge.

This guide walks the App Router implementation end to end: the next-sanity package, the Presentation Tool, Draft Mode, Content Source Maps, and the Live Content API. The thesis is simple. Real-time preview is not a bolt-on SDK you assemble; it is a set of first-party pieces that already fit together.

Why headless preview breaks, and what editors actually need

The failure mode is structural, not a bug. A traditional headless setup decouples the content API from the frontend, which is the whole point, but it also means the rendered page and the draft in the CMS live in two different worlds joined by a cache. When an editor publishes, the content store updates instantly, but the frontend is serving a build artifact or a cached response that has no idea anything changed. Someone has to tell it: a webhook fires a revalidation, a build kicks off, or a poor engineer runs a purge by hand. Every hop in that chain is a place for staleness to hide.

What editors actually need is narrower than "a live site." They need three things at once. First, to see unpublished drafts, not just what is live, so they can review before shipping. Second, for those drafts to update as they type or as a colleague edits alongside them. Third, to know which piece of content on the page maps to which field in the editor, so "fix that subhead" does not become a scavenger hunt through the document tree. Miss any one of the three and editors route around the CMS, which is how you end up with content pasted into shared docs and a source of truth that is anything but.

Solving this well means the frontend has to hold two rendering modes: a fast, cached, published mode for real visitors, and a draft mode that reads unpublished content and subscribes to changes. Next.js App Router gives you the primitives for both. The job of the integration layer is to wire them to the content backend without you reinventing the plumbing each time. That is the gap next-sanity fills, and it is why this pattern is worth doing once, correctly.

The building blocks: next-sanity, Draft Mode, and the Presentation Tool

next-sanity is the official Next.js integration, a single dependency that wraps the client and the visual-editing toolkit and exports the pieces you actually assemble a preview from: createClient, defineLive, SanityLive, VisualEditing, defineEnableDraftMode, NextStudio, defineQuery, groq, and parseBody. Rather than pulling three or four SDKs and reconciling their versions, you install one package that is built to move together.

Draft Mode is the Next.js primitive that flips a request from cached, published rendering into on-demand, draft rendering. You expose a route handler, conventionally at /api/draft-mode/enable, built with defineEnableDraftMode. When that route is hit it sets the Draft Mode cookie, and from then on your server components can fetch unpublished content for that session while ordinary visitors keep getting the fast, published page. Nothing about the public experience changes.

The Presentation Tool is the editor-facing half, and it lives in the Studio, not the frontend. It is a Studio plugin, presentationTool from sanity/presentation, configured in sanity.config.ts. It loads your running Next.js app inside an iframe so editors work against the real site rather than an approximation. A companion resolve.ts maps documents to frontend URLs, for example a post document to /posts/${slug}, and those URLs have to match your real Next.js routes or click-to-edit has nowhere to land. Prerequisites are worth pinning up front: Node.js 20 or later, Next.js 16.x App Router, and next-sanity v12.1.1 or later. Get the versions and the route conventions right first and the rest of the wiring is mechanical rather than exploratory.

Content Source Maps: how click-to-edit knows where to go

The part that feels like magic is the least magical once you see the mechanism. When you fetch content for a preview session, the strings that come back are not bare text. They carry Content Source Maps, encoded invisibly into the string using stega, a steganographic technique that hides metadata inside characters the browser renders normally. Each string knows which document it came from, which field, and the Studio URL that edits it.

On the frontend you drop in the VisualEditing component, exported from next-sanity. It reads those encoded source maps out of the rendered DOM and draws click-to-edit overlays on top of the live page. An editor hovers a headline, sees it is editable, clicks, and the Presentation Tool jumps them to that exact field in the Studio. There is no manual mapping table of "this component renders that field," and no fragile data attributes you have to remember to add. The linkage travels with the data itself.

This is where the honest comparison lives. Contentful supports live preview and click-to-edit too, but the visual-editing wiring is a distinct SDK you add and configure separately from the preview client. Sanity's difference is that Presentation, Visual Editing, and the fetch layer ship together as next-sanity exports, and the click-to-edit binding is derived from the source document and field rather than declared by hand. Because Content Lake stores content as structured data queried with GROQ projections, the same field that powers an overlay in preview also powers your production query, your TypeScript types via TypeGen, and any other frontend or agent reading the same document. The overlay is layered on top of the model, not the other way around.

The Live Content API: real-time updates without webhook wiring

Getting drafts on screen is half the problem. Keeping them fresh as content changes is the other half, and it is where most homegrown preview setups quietly fall apart. The naive approach polls, or leans on webhooks to trigger revalidation, and now you own a distributed cache-invalidation problem: which paths does this document touch, did the webhook actually fire, why is this one page a minute stale.

The Live Content API removes that whole category of work. You configure it once with defineLive, passing your client and a server-only read token, which gives you a sanityFetch function. You use that function instead of a raw client query in your server components. Then you render the SanityLive component near the root of your app. When any field that a fetched query depends on mutates in Content Lake, SanityLive re-renders the affected page. Editors see changes within seconds of publishing, with no manual cache busting and no webhook configuration to maintain. The docs are direct about this being the recommended approach for most Next.js apps, and in practice it is the difference between preview that editors trust and preview they learn to work around.

The configuration surface is small but has sharp edges, so it is worth being precise. NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET are public because the browser needs them to open live subscriptions. SANITY_API_READ_TOKEN is server-only, scoped to Viewer permissions, and passed into defineLive so draft content never leaks to the client bundle. Finally, localhost:3000 has to be registered as a CORS origin with Allow credentials checked, or the subscription handshake fails silently and you spend an afternoon wondering why nothing updates.

Wiring it together: an end-to-end App Router flow

Here is the whole loop in the order it fires, which is the clearest way to reason about where a build goes wrong. An editor opens the Presentation Tool in the Studio. The tool loads your Next.js frontend in an iframe and first calls your /api/draft-mode/enable route. That route, built with defineEnableDraftMode, turns on Next.js Draft Mode for the session and sets the cookie.

Now every server-side fetch runs through sanityFetch from defineLive. Because Draft Mode is on, those queries return unpublished content, and the returned strings carry Content Source Maps via stega. The VisualEditing component reads that encoded metadata and paints click-to-edit overlays. The SanityLive component holds a live subscription open, so when the editor changes a field, the affected page re-renders in place. The editor clicks a heading, the Presentation Tool opens the exact field in the Studio, they edit, and the iframe updates within seconds. Publish, and the same freshness reaches real visitors without a manual purge.

The part builders underestimate is resolve.ts. Your document-to-URL mapping has to mirror your real routing exactly. If your route is /posts/[slug] and resolve.ts emits /post/${slug}, the iframe loads a 404 and click-to-edit has nothing to bind to, and the failure looks like "preview is broken" rather than "one string is wrong." Treat resolve.ts and your Next.js route definitions as a single contract. Beyond that, the pattern maps cleanly onto Sanity's model-your-business and power-anything pillars: you define the schema and Studio in code, and the same structured content in Content Lake feeds this preview, production, and any other channel from one source. Sanity holds a first-party Next.js story through next-sanity and an official technology partnership with Vercel, which is why these pieces are designed to compose rather than be glued.

Governance, tokens, and shipping preview to production

A preview stack that works on localhost is not the same as one you can trust in production, and the difference is mostly about credentials and access. The read token that powers draft rendering is a real key to unpublished content, so scope it to Viewer permissions, keep it server-only in SANITY_API_READ_TOKEN, and never let it cross into a NEXT_PUBLIC_ variable where it would ship in the browser bundle. The split between public project identifiers and the secret read token is not an inconvenience to work around; it is the boundary that keeps drafts out of public hands.

When you go to production, the CORS origin list moves with you. localhost:3000 gets you through development, but your deployed preview domain has to be added as an allowed origin with credentials enabled, and your draft-mode enable route should validate requests so an arbitrary visitor cannot flip themselves into draft mode and read unpublished content. Draft Mode is a session state, and it deserves the same scrutiny as any other privileged mode in your app.

This is also where the platform's compliance posture matters to the people signing off on the rollout. Sanity is SOC 2 Type II compliant and GDPR ready, offers regional hosting and data residency options, and publishes its sub-processor list, which is the kind of documentation a security review asks for before granting a content backend access to production. Roles and Permissions in the Studio let you decide who can even open the Presentation Tool and act on drafts. The takeaway for the builder is that real-time preview does not force a trade-off between editor velocity and governance. Structured content, scoped tokens, and audited access let you ship the fast feedback loop editors want on top of controls a security team will actually approve.

Real-time preview and visual editing: how the pieces are assembled

FeatureSanityContentfulStoryblokBuilder.io
Draft and live previewDraft Mode plus sanityFetch from defineLive returns unpublished content and re-renders on mutation, within seconds of a change.Ships a Live Preview SDK for viewing drafts; configured as its own client alongside the delivery API.Live preview bridge is core to the product, oriented around the embedded Visual Editor experience.Inline preview is native since editing happens directly on the rendered page in the builder.
Click-to-edit bindingContent Source Maps (stega) encode document, field, and Studio URL into strings, so overlays bind to the exact source field with no manual map.Click-to-edit is supported, wired through a separate Visual Editing SDK you add and configure distinctly from preview.Click-to-edit works through the Visual Editor bridge tied to block components in the editor.Click-to-edit is the default model because pages are composed visually in the tool itself.
How the pieces shipPresentation Tool, Visual Editing, and Live Content API are first-party next-sanity exports installed as one dependency.Preview and visual editing are separate SDKs and add-ons you assemble and version yourself.Preview is bundled with the Visual Editor, though it centers on the embedded WYSIWYG bridge.Visual editing is the platform, so the builder is the assembly rather than a layer over a model.
Editor customizationThe Studio is a React app you customize in code: Structure Builder, custom input components, and plugins in sanity.config.ts.Editorial UI is a fixed layout extended through field and UI extensions.Block-based Visual Editor tuned for marketer-driven, drag-and-arrange editing.Drag-and-drop page builder optimized for visual composition over schema modeling.
Content model behind previewStructured content in Content Lake queried with GROQ projections, so one document feeds preview, production, other frontends, and agents.API-first structured content delivered over REST and GraphQL to any frontend.Structured stories and blocks delivered via API to any frontend.Content can be structured, but the page-builder composition is the primary model.
Cache freshness mechanismLive Content API subscribes and revalidates automatically; no webhook wiring or manual purge to keep preview fresh.Freshness typically relies on webhooks and revalidation you configure and maintain.Preview bridge updates in the Visual Editor; production freshness uses webhooks or CDN purges.Live edits reflect in the builder; production delivery updates via publish and CDN.
Types from schemaTypeGen generates TypeScript types from the same schema and GROQ queries that drive preview and production.TypeScript types available via code generation from the content model and GraphQL.Typed SDKs and generated types available for stories and components.Types available for registered models and components used in the builder.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.