Setting Up Sanity Visual Editing with the Presentation Tool
This guide, part of Sanity Studio Customization, sets up visual editing: the frontend shown inside Sanity Studio’s Presentation tool, with overlays that let editors click any piece of content to open the field that holds it, and live updates as they type. It covers the Studio configuration, the frontend’s draft mode, content source maps with stega encoding, live refresh and the security rules that keep preview machinery out of production.
Form-based editing forces editors to translate between fields and the page: which field is the subtitle under the hero, where does this card’s text come from. Visual editing removes that translation. It works by having the frontend render draft content with invisible markers, stega-encoded content source maps, that tell the overlay which document and field produced each string. The Presentation tool reads the markers, draws the overlays and opens the right field on click.
The Problem
A travel publisher’s editors previewed articles in a separate browser tab, switched back to the Studio to change a sentence, saved, and reloaded the preview, often losing their scroll position in long articles. Finding which field held a particular card title on a destination page took several minutes for new editors. Corrections that should take seconds took much longer, and editors avoided small improvements.
How Visual Editing Works
Presentation tool in the Studio. Add the presentationTool plugin with the frontend’s preview URL and an endpoint that enables draft mode. The tool shows the frontend in an iframe next to a document panel.
Draft mode on the frontend. A route validates a secret issued by the Studio and enables draft mode. In draft mode, the frontend queries with the drafts perspective, the viewer token and without caching.
Stega-encoded source maps. The Sanity client can return content source maps and embed them into strings as invisible characters. The overlay script decodes them to map rendered text to document ids and field paths. Enable stega only in draft mode.
Live updates. A live-content component subscribes to changes and refreshes the page data as editors type, so the preview reflects edits within moments without manual reloads.
Implementation
In the Studio, register the Presentation tool with the preview origin and the draft-mode endpoint.
// sanity.config.ts (excerpt)
import { presentationTool } from "sanity/presentation";
export default defineConfig({
// ...
plugins: [
structureTool(),
presentationTool({
previewUrl: {
origin: process.env.SANITY_STUDIO_PREVIEW_ORIGIN, // e.g. https://www.example.com
previewMode: { enable: "/api/draft-mode/enable" },
},
}),
],
});
On the frontend, with next-sanity, a route enables draft mode after validating the Studio’s request, and the client turns on stega encoding in draft mode only.
// app/api/draft-mode/enable/route.ts
import { defineEnableDraftMode } from "next-sanity/draft-mode";
import { client } from "@/lib/sanity/client";
export const { GET } = defineEnableDraftMode({
client: client.withConfig({ token: process.env.SANITY_VIEWER_TOKEN }),
});
// lib/sanity/fetch.ts
import { draftMode } from "next/headers";
import { client } from "./client";
export async function sanityFetch<T>(query: string, params: Record<string, unknown> = {}, tags: string[] = []) {
const isDraft = (await draftMode()).isEnabled;
return client.fetch<T>(query, params, isDraft
? { perspective: "drafts", useCdn: false, stega: { enabled: true, studioUrl: "/studio" }, token: process.env.SANITY_VIEWER_TOKEN, cache: "no-store" }
: { perspective: "published", useCdn: true, stega: false, next: { tags } });
}
In the root layout, render the visual editing component only in draft mode, so its script never loads for readers:
// app/layout.tsx (excerpt)
import { draftMode } from "next/headers";
import { VisualEditing } from "next-sanity";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const isDraft = (await draftMode()).isEnabled;
return (
<html lang="en">
<body>
{children}
{isDraft && <VisualEditing />}
</body>
</html>
);
}
For live updates, newer next-sanity versions provide a live-content setup that pairs fetching and subscriptions; enable it in draft mode the same way, with the viewer token only on the server and a browser token limited to draft viewing if the setup requires one.
Stega and non-display strings
Stega markers are invisible characters appended to strings, which is harmless in text but breaks values used as data: URLs, CSS class names, dates parsed by code, or strings compared in logic. Exclude such fields from encoding with the client’s stega filter, or clean values with stegaClean before using them as data. Metadata tags such as the page title also need cleaning, since they end up in the document head.
Mapping locations to documents
The Presentation tool can also show, for a document open in the Studio, where it appears on the site: which pages use this author, which landing pages include this product. Configure document locations with a resolver that, for each document type, queries the pages that reference it and returns their titles and URLs. Editors then open an author and jump straight to any page that shows them, which is particularly useful for shared content such as banners, legal notices and reusable blocks, where a change affects many pages at once. Keep the resolver’s queries light and limited to a handful of results; its purpose is orientation, not a complete usage report, and it runs every time an editor opens a document.
Configuration Reference
| Setting | Recommendation | Why |
|---|---|---|
| Draft route | validated by the Studio’s secret | Only the Studio can enable draft mode. |
| Stega | enabled only in draft mode | Production HTML stays clean. |
| Stega filter | exclude URLs, slugs, dates, enums | Encoded data breaks logic. |
| Viewer token | server-side only | Drafts are not public. |
| Framing | CSP frame-ancestors includes the Studio origin |
Presentation loads the site in an iframe. |
| Overlays | loaded only in draft mode | No preview script for readers. |
Gotchas & Edge Cases
- Frame blocking. A strict
frame-ancestorsorX-Frame-Optionsheader prevents the Presentation tool from loading the site. Allow the Studio’s origin for draft routes, and nothing else. - Third-party cookies. Draft mode relies on cookies inside an iframe from another origin; they must be
SameSite=None; Secure. - Strings used as keys. Encoded strings used as React keys or map keys cause mismatches; clean them first.
- Server-rendered layouts cached in draft mode. Ensure every fetch path respects draft mode, including layouts and metadata, or parts of the preview show published data.
Worked Example
The travel publisher enabled the Presentation tool with draft mode, stega encoding filtered to exclude slugs and URLs, and live updates. Editors could click any heading, card or caption on a destination page to open the right field, and saw changes appear in the preview as they typed. The average time to complete a small correction, measured in a sample of editing sessions, fell from several minutes to under one, and new editors stopped needing a guide to the content model to find fields. Production pages were unaffected: a check of the public HTML confirmed that no stega characters or overlay scripts were present.
Performance and Scaling of Preview
Visual editing adds load that production does not see: every editor’s preview makes uncached draft queries, and live updates refetch as they type. For small teams this is negligible; for large editorial teams it can become noticeable in API usage and server load. Keep draft queries as trimmed as production ones, debounce live refetches, and make sure preview rendering does not trigger expensive work that production pages avoid through caching, such as generating Open Graph images or large sitemaps. Monitor API usage split by perspective, so growth in preview traffic is visible, and consider a separate preview deployment when editorial load is high, so preview traffic never competes with readers for server capacity.
Rollout Checklist
- Add the Presentation tool with the preview origin and draft-mode endpoint.
- Validate the Studio’s secret before enabling draft mode.
- Fetch drafts with the viewer token, no caching and stega encoding in draft mode only.
- Filter or clean stega from values used as data.
- Load overlays and live updates only in draft mode.
- Allow framing from the Studio origin for draft routes.
Frequently Asked Questions
Does visual editing replace form editing?
No. Editors still use forms for fields that are not visible, such as SEO settings. Visual editing makes visible content quick to find and change.
Can visual editing work without Next.js?
Yes. Sanity provides libraries for other frameworks; the principles of draft mode, source maps and overlays are the same.
Is stega safe for SEO?
It is only enabled in draft mode, so production pages and crawlers never see the markers at all.
Does visual editing work for localized content?
Yes. The source maps include the field path with the language, so clicking a German heading opens the German field. Load the preview in the locale the editor is currently working on.
What if a component renders content that is not from Sanity?
Only strings with source maps become editable. Other text renders normally without overlays, and editors simply cannot click on it.