Visual Editing & Preview7 min read

How to Set Up Visual Editing on a Headless CMS Frontend

You wired up a beautiful headless frontend, handed it to your editorial team, and the first question back was: "How do I see my change before it ships?" They're staring at a content form on one screen and a deployed site on another,…

Published June 19, 2026

You wired up a beautiful headless frontend, handed it to your editorial team, and the first question back was: "How do I see my change before it ships?" They're staring at a content form on one screen and a deployed site on another, alt-tabbing, guessing which field maps to which block, and refreshing a preview URL that's thirty seconds stale. Every typo fix becomes a round trip through a build pipeline. The promise of headless, clean separation of content and presentation, quietly became a usability tax on the people who actually produce the content.

That tax has a cost beyond annoyance. When editors can't trust the preview, they stop trusting the CMS, route changes back through engineering, and your decoupled architecture starts behaving like the monolith you left. The fix isn't abandoning headless; it's closing the loop between the structured content store and the rendered page without re-coupling them.

This guide walks through setting up visual editing on a headless frontend end to end, the click-to-edit overlay, the draft-aware data fetch, the secure preview route, and the real-time refresh, and uses Sanity's Presentation tool and Live Content API as the concrete reference for how the pieces fit.

Why headless previews break (and what 'visual editing' actually means)

The default headless preview is a dead end: an editor saves a draft, a webhook kicks a rebuild, and ninety seconds later a separate browser tab shows something approximating the result. There's no spatial relationship between the form field and the pixel it controls, no way to click a heading on the page and land on the field that owns it, and no confidence that what's on screen reflects unpublished work. For a marketing team shipping dozens of edits a day, that latency and ambiguity is the difference between owning their content and filing tickets.

Visual editing is the set of capabilities that closes that loop. Concretely it means three things working together. First, an overlay: the rendered frontend reports, per element, which document and which field produced it, so clicking the page navigates the editor to the right input. Second, draft-aware rendering: the same frontend can fetch unpublished content, on demand, so the preview shows the editor's in-progress state rather than the last published build. Third, live refresh: edits propagate to the preview in near real time, not on a rebuild cadence.

The hard part is doing all three without re-coupling content and presentation. A WYSIWYG page builder gives you click-to-edit by owning the rendering, but then your content is trapped in that vendor's layout model and can't be reused across an app, a kiosk, and a voice channel. The goal is the click-to-edit ergonomics of a page builder with the channel-independence of structured content underneath. Sanity's approach keeps the frontend yours, it's your Next.js, Astro, or Remix app, and layers the overlay and data plumbing on top, so the structured content in the Content Lake stays the single source of truth for every channel.

Architecture: how the overlay, the draft API, and the iframe fit together

Before touching code, get the topology straight, because most failed visual-editing setups are topology mistakes. There are three moving parts and they live in two places. The CMS hosts an authoring surface that embeds your frontend in an iframe. The frontend renders content but, in preview mode, annotates its output with metadata describing the source document and field. A message channel, postMessage between the iframe and the parent editor, carries clicks from the page back to the editor and carries content changes from the editor back to the page.

The annotation is the keystone. When your data fetch runs in preview mode, each returned value needs to carry its provenance: which document `_id`, which field path, which array key. The rendering layer stamps that provenance into the DOM as data attributes the overlay can read. Click an element, the overlay reads the attribute, posts the document-and-path back to the editor, and the editor focuses that exact field. Get the provenance wrong and clicks land on the wrong field or do nothing, which is why the data layer, not the CSS, is where this work actually happens.

In Sanity, the authoring surface is the Presentation tool, configured in `sanity.config.ts`, which loads your deployed or local frontend in its iframe. Your frontend opts into the overlay with the `@sanity/visual-editing` package, and your GROQ queries run through a draft-aware client so the same query returns published content for visitors and draft content inside Presentation. Because the provenance travels with the query result rather than being reverse-engineered from the markup, the overlay knows the field path for every annotated value, and GROQ's projections mean you fetch exactly the shape the component needs in one round trip, provenance included.

Step 1: Stand up a secure draft-mode route on the frontend

Visual editing starts on the frontend, not in the CMS, because the editor needs a URL it can safely embed that renders unpublished content. The naive version, a `?preview=true` query param that flips your client into draft mode, is a data leak: anyone who guesses the param sees unpublished drafts. The correct pattern is a server-side draft mode guarded by a secret.

In a Next.js App Router project this is the `draftMode()` API. You expose a route, conventionally `/api/draft-mode/enable`, that validates a signed token, calls `draftMode().enable()` to set an httpOnly cookie, and redirects to the requested path. Subsequent requests carry the cookie, your data-fetching layer reads `draftMode().isEnabled`, and only then does it fetch drafts. Astro and Remix have equivalent server-side gates; the principle is identical: the switch into draft rendering is a server decision keyed on a secret, never a client-readable flag.

Sanity ships this wiring in the official Next.js, Astro, and Remix starters so you're not assembling it from scratch. The enable route validates the request came from Sanity, flips draft mode, and hands control back to the page. On the data side you swap your read client for one configured with `perspective: 'drafts'` and a token, gated behind the draft-mode check, so production traffic never touches the authenticated draft path. The discipline to enforce here: the token lives only in server-side environment variables, the draft client is only ever constructed inside the draft-mode branch, and the public client stays token-free. That separation is what lets you embed a preview safely without widening your attack surface.

Step 2: Make queries draft-aware and stamp provenance onto the render

With the route in place, the data layer does the real work. Every query that feeds a previewable component needs two properties: it must resolve drafts when draft mode is on, and it must carry source provenance so the overlay can map pixels back to fields. These are the same query, you don't fork your data access for preview; you parameterise it.

The provenance is what most teams underestimate. A component that renders `post.title` needs to know that this particular string came from document `abc123`, field `title`, so a click resolves to that input. When content is deeply nested, a hero inside a page-builder array inside a localized object, the path matters precisely, and reconstructing it from rendered HTML is brittle. The robust approach is to let the query result carry the path, so the rendering layer reads provenance from data rather than inferring it from structure.

This is where GROQ earns its place. A single GROQ query pulls the page, follows references with `->`, projects nested arrays, and, through Sanity's content-source-map support, returns the document `_id` and field path alongside each value. Your frontend wraps the result with `@sanity/client/stega` or the framework helper so each string is encoded with its origin, and the `@sanity/visual-editing` overlay decodes it at click time. Portable Text blocks get the same treatment: a rich-text annotation or a custom mark renders to your design system, and clicking it still resolves to the exact span in the editor. Because the provenance rides inside the query result, you don't maintain a parallel mapping table that drifts every time the schema changes, the `defineType` schema, the GROQ projection, and the overlay stay in sync by construction.

Step 3: Embed the frontend in the editor and turn on live updates

Now connect the two halves. In the CMS, configure the authoring surface to load your draft-mode URL in an iframe and register the overlay handshake so clicks in the page focus fields in the form and vice versa. A stale preview undermines the whole exercise, so this step is also where you make updates flow without a rebuild.

The naive refresh is polling or a full reload on every keystroke, janky and slow. The right mechanism is a subscription: the frontend listens for content changes and patches only what changed, so typing in the title field updates the rendered heading within a render frame, not a deploy cycle. That requires a content store that can stream changes, not just answer one-shot reads.

In Sanity, you point the Presentation tool's `previewUrl` at your draft-mode enable route in `sanity.config.ts`, and the iframe loads your real frontend. The `@sanity/visual-editing` package, mounted in your app, completes the click-to-edit handshake over postMessage: click a stega-encoded element and Presentation opens that document at that field; edit the field and the change flows back. For live updates you subscribe through the Live Content API so the preview reflects in-progress edits in real time rather than on a build. Because Presentation embeds the actual production frontend, your components, your CSS, your routing, editors preview the real thing instead of a CMS-side approximation, and the content never leaves the structured Content Lake to make that happen. The result is page-builder ergonomics on an architecture that still serves the same content to your app, your emails, and whatever channel comes next.

Common failure modes and how to harden the setup

Once the happy path works, the issues that surface in production are predictable, and worth pre-empting. The first is the leaked draft: a preview URL or token that escapes into client-readable code or an unauthenticated route. Audit that the draft client is only constructed server-side behind the draft-mode gate, that the token lives in server env vars, and that the enable route verifies the request's origin. A draft visible to an anonymous visitor is a content incident, not a bug.

The second is provenance drift. When stega encoding is missing on a value, clicking that element does nothing; when the field path is stale after a schema rename, clicks land on the wrong input. The defense is keeping the schema, the query projection, and the rendering aligned, and here codegen helps: Sanity's TypeGen turns your `defineType` schemas and GROQ queries into TypeScript types, so a field rename that breaks a projection fails at compile time instead of silently misrouting an editor's click.

The third is encoded artifacts leaking into production. Stega-encoded strings carry invisible characters that are fine inside Presentation but must never reach live visitors; the encoding has to be gated on draft mode and stripped from production output, with special care for strings used in URLs, dates, or anything parsed rather than displayed. The fourth is iframe and cross-origin friction, frame-ancestors CSP headers, third-party cookie behavior, and auth flows that don't survive being embedded. Test the preview both locally and against your deployed frontend, since same-origin local setups hide cross-origin bugs you'll only meet in production. Harden these four and visual editing stops being a demo and becomes infrastructure your editors rely on daily.

Visual editing approaches across headless platforms

FeatureSanityContentful
Click-to-edit on your own frontendPresentation tool embeds your real Next.js/Astro/Remix app in an iframe; @sanity/visual-editing wires click-to-edit over postMessage to the actual components you ship.Live Preview SDK with an inspector mode overlays your app; click-to-edit available via the field-tagging SDK that annotates rendered elements.
How provenance maps pixels to fieldsContent source maps ride inside the GROQ result; stega-encoded values carry document _id and field path, so the overlay resolves exact paths including nested arrays.Field tagging via data attributes you attach in components, mapping entry id, field, and locale by hand at the render layer.
Draft-aware fetch in the same querySame GROQ query, parameterised by perspective: 'drafts' behind a server draft-mode gate; one query shape serves visitors and preview alike.Separate Preview API endpoint and preview access token, distinct from the Content Delivery API used in production.
Live refresh without a rebuildLive Content API streams changes so edits patch the preview in near real time rather than on a build cycle.Live Preview SDK refreshes the embedded preview as fields change via its messaging channel.
Rich text fidelity in previewPortable Text annotations and custom marks render to your design system and remain click-resolvable to the exact span in the editor.Rich Text field renders via a renderer library; click-to-edit maps to the field, with span-level targeting less granular.
Type safety from schema to queryTypeGen generates TypeScript from defineType schemas and GROQ queries, so a rename that breaks a projection fails at compile time before it misroutes a click.TypeScript types generated from content models via the CLI / community tooling, decoupled from the preview tagging.
Channel independence of the contentContent stays structured in the Content Lake and is reused across app, email, and other channels; the frontend is yours, not owned by the preview tool.Structured content reusable across channels via the delivery API; preview is an overlay on your own app.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.