Visual Editing

Build a complete visual editing integration

Build a complete framework-agnostic visual editing integration step-by-step with Vite and a Node.js HTTP server.

This guide walks through building a complete visual editing integration from scratch using vanilla TypeScript and Vite. By the end, you'll have a working setup where content editors can preview draft content, click on elements to edit them in Sanity Studio, and see changes reflected in real time.

The example uses standard Web APIs and no frontend framework, so you can adapt the patterns to any server-side runtime or framework.

What you'll build

A minimal web application with:

  • A Sanity client configured for visual editing with stega encoding.
  • Server-side draft mode with secure enable/disable endpoints.
  • Click-to-edit overlays powered by @sanity/visual-editing.
  • Real-time content updates via the Presentation Tool's comlink connection.
  • A Presentation Tool configuration in Sanity Studio.

Each step builds on the previous one and produces a testable result.

Prerequisites

  • A Sanity project with at least one document type (this example uses a post type with title, slug, and body fields). Follow the Studio quick start to get up and running.
  • Node.js 20 or later.
  • A Sanity Studio deployed or running locally.

Step 1: create the project and fetch content

Start by scaffolding a Vite project and configuring the Sanity client. We’ll use Vite to bundle the client-side visual editing components to make this example easier to follow.

Create the project

The @portabletext/to-html package renders Portable Text (the array format Sanity uses for rich text) as HTML. It preserves all text content, including stega-encoded zero-width characters, so overlays work within body content with no extra configuration.

Configure the client

Create a client with stega encoding enabled. The configuration below creates a base client, then exposes a client configured for visual editing. The studioUrl tells the overlay system where your Studio lives:

Create a server for rendering

This example uses a basic Node.js HTTP server to avoid confusion. In a real project, you'd use your framework's server (Express, Hono, Fastify, or similar):

Test it: run npx tsx server.ts and visit http://localhost:3000/posts/your-post-slug. You should see the published content rendered on the page.

Important

For more on client configuration, see setting up the Sanity client for visual editing.

Step 2: add draft mode

Add secure endpoints that the Presentation Tool calls to toggle draft mode.

Install dependencies

Still in the visual-editing-demo directory:

Create the enable endpoint

Update the server to handle draft mode

This step isn't independently testable yet because the Presentation Tool (configured in Step 4) triggers the enable endpoint automatically. For now, verify the code compiles and move to Step 3. Once the full integration is wired up, you can confirm the sanity-preview-perspective cookie is set and that draft content renders correctly.

For the details on how this preview mode implementation works, see implementing preview/draft mode.

Step 3: add click-to-edit overlays and live updates

Add the overlay system so editors can click on content elements to jump to the corresponding field in the Studio. The enableVisualEditing() function handles both overlays and real-time content updates through the Presentation Tool's Comlink connection.

Install dependencies

Still in the visual-editing-demo directory:

Note that react and react-dom are required peer dependencies of @sanity/visual-editing:

Create the preview module

Create a module that initializes visual editing. This file is a client component, and it is only loaded when draft mode is active (the server conditionally includes it):

Update the server to include the preview script

Add the conditional script tag to your server's HTML template. This loads the preview module only when draft mode is active:

The server conditionally includes the preview script only when draft mode is active. The client-side code never needs to detect draft mode because it only runs when the server includes it.

Start both development servers

You need two processes running: the Node server that renders HTML, and the Vite dev server that serves the preview module. Both must be running for visual editing to work.

Open two terminal windows:

When draft mode is active, the Node server includes <script type="module" src="http://localhost:5173/src/preview.ts"> in the HTML. Vite's dev server compiles and serves this module (and its dependencies like React and @sanity/visual-editing) on the fly. If Vite isn't running, the script tag silently fails and overlays won't appear.

Add data attributes for non-string content

For content that can't carry stega encoding (like images), use createDataAttribute():

Because stega encoding is active in draft mode, the rendered text already contains invisible metadata. The overlay system detects this automatically and draws transparent overlays on content elements. Data attributes are only needed for non-string content.

For the full overlay API, see enabling overlays and click-to-edit. For more on real-time update patterns, see real-time content updates.

Step 4: configure the Presentation Tool

Set up the Studio plugin that ties everything together.

Update your Studio configuration

Navigate to your Studio directory and update the config.

This configuration:

  • previewUrl: tells the Presentation Tool where your frontend is running and which endpoints toggle draft mode.
  • mainDocuments: maps URL patterns to documents, so navigating to /posts/my-post in the preview automatically opens the corresponding post document in the editor.
  • locations: maps document types to frontend URLs, so the Studio shows editors where each document appears on the site.

For the full Presentation Tool configuration, see configuring the Presentation Tool.

Test the full system

Run each part of the system:

In your studio:

In the visual-editing-demo directory:

Open another terminal window in visual-editing-demo, and run:

Open the Presentation Tool in your Studio. The preview should load your frontend in an iframe, activate draft mode automatically, and show click-to-edit overlays. Clicking an overlay should open the document in the editor pane. Editing a field should update the preview in real time. Navigating in the preview should sync with the Studio's document panel.

What you've built

Your integration now supports the complete visual editing workflow:

  • Draft mode: the Presentation Tool activates draft mode via a secure handshake, switching your frontend to the Studio's requested perspective (which may be drafts or a stacked perspective for content releases) with stega encoding active.
  • Click-to-edit: editors click on any content element in the preview to jump directly to the corresponding field in the Studio.
  • Real-time updates: content changes in the Studio are reflected in the preview instantly via the Comlink connection managed by enableVisualEditing().
  • Navigation sync: navigating in the preview updates the Studio's document panel, and clicking document locations in the Studio navigates the preview.

Production considerations

This example uses Vite's dev server to serve client-side modules during development. For production:

  • Build the client-side code: run npx vite build to produce optimized bundles. Serve the built assets from your production server.
  • Conditional script loading: the server already conditionally includes the preview script only when draft mode is active. In production, point the src attribute to your built asset path instead of the Vite dev server.
  • Environment variables: store SANITY_API_READ_TOKEN as a server-side environment variable. Never expose it to the client.

Next steps

This example covers the core integration. Here are some areas to explore further:

Was this page helpful?