Real-time previews with Sanity: a guide for developers and content teams

updated
11 September 2026
11 September 2026
5 min read

An editor drafts a headline, hits save, and waits for it to go live. They open the staging link, notice a typo, jump back into the CMS, fix it, save again… and wait all over again. For many content teams, this back-and-forth has become normal. But it doesn’t have to be that way.

Illustration of a draft edited in a dark CMS window syncing live to the rendered web page next to it
Editors watch the live page update as they type in Sanity

Sanity’s real-time preview collapses that loop to near-zero, and editors can watch their changes render on the actual page as they type. This article covers both sides of that feature. If you’re a developer, start from the top — we’ll go deep on architecture, implementation, security, and framework integrations. If you’re a content editor or manager, jump straight to the content team guide.

{{banner}}

The problem Sanity preview solves

From experience, you perhaps know that traditional CMS workflows are painful in a way that’s easy to normalize. But this continues up to the moment you see the alternative. The biggest issue with the traditional approach is time. In a fast-paced editorial workflow, this back-and-forth drags down morale, slows publishing, and teaches teams to work around the tool instead of with it.

Sanity’s real-time preview shortens that process to near-zero. Editors see their changes reflected on an actual rendered page (real CSS, components, and data) as they type. No deploys, no waiting, no “I think it’ll look fine” guesswork.

For developers, this isn’t just a convenience feature you bolt on for editors. Done right, it becomes a first-class part of your content platform. If you’re still deciding whether Sanity is the right platform at all, our comparison of headless CMS options is a good place to start.

How Sanity preview works: the architecture

We understand that after reading about Sanity’s upsides, you want to start right away. But before writing a single line of code, spend five minutes understanding what’s actually happening under the hood.

The Content Lake and the draft/publish duality

Sanity stores all content in its Content Lake — a hosted, real-time document store. Every document exists in two potential states: published and draft. A published post with the ID post-abc-123 has a corresponding draft stored at drafts.post-abc-123. This is the core principle the entire preview system relies on.

Here’s how the preview pipeline works:

  • Listening to the Sanity real-time API via @sanity/client’s listen() method for mutations on drafts.*documents.
  • Pushing those mutation events into your frontend via the Visual Editing overlay or a React hook subscription.
  • Rendering your components with draft data merged on top of published data, giving you a coherent “current state” view.

The key Sanity client option that makes this possible is perspective: "previewDrafts". When this is enabled, the API automatically shows draft content instead of the published version when a draft exists, and falls back to the published version when it doesn’t. You don’t need to merge anything yourself.

Two preview strategies — know when to use each

Sanity supports two distinct approaches. Realizing the difference upfront will save you from the common mistake of implementing one when you actually need the other.

StrategyUse caseHow it works
Draft Mode / Preview ModeFull-page preview at a guarded URLYour frontend reads draft documents server-side via a secret-gated route
Visual Editing (Presentation Tool)Live in-context editing with click-to-edit overlays directly in the StudioSanity Studio embeds your frontend in an iframe and injects a JS overlay that reads Stega-encoded field references

For most teams, Visual Editing is the end goal. Why? Because it gives editors a WYSIWYG-like experience without sacrificing headless architecture. But Draft Mode is a valid, simpler starting point in two situations:

  • for teams that don’t need the full overlay experience;
  • when Visual Editing is difficult to implement within an existing frontend architecture.

Pro tip: Since both use the same underlying client configuration, you can ship Draft Mode first, validate the editorial workflow, then layer in Visual Editing.

Part 1: Developer implementation

Before setting up visual editing, make sure your environment is ready. You’ll need:

  • a Sanity project with Studio (sanity@3.x);
  • a compatible client version (@sanity/client version 6.x+);
  • a frontend framework to render the preview (see Part 3 for all supported frameworks);
  • a recent Node.js version.

Once that’s in place, install the core packages that power visual editing and handle preview security plus framework-specific helpers if you’re using something like Next.js.

Core packages

# Core Sanity Visual Editing package
npm install @sanity/visual-editing @sanity/client

# For the Presentation Tool URL secret validation
npm install @sanity/preview-url-secret

# For Next.js specifically (see Part 2 for full setup)
npm install next-sanity

# The Presentation Tool is bundled in sanity@3.20+
# No separate install needed for the Studio side

Part 2: Next.js — the deep dive

Next.js is the most mature integration in the Sanity ecosystem. The official next-sanity package provides a purpose-built client, a useLiveQuery hook, and Presentation Tool wiring with minimal boilerplate. It’s also where Sanity invests most of its first-party documentation effort, so it’s the best-documented path. Let’s take a look at how to build this integration.

Project structure

In Sanity setups, one of the most common (and risky) mistakes is scattering client configuration across components. This often leads to accidental exposure of API tokens in the browser. By organizing your project deliberately, you keep sensitive logic on the server, make preview behavior predictable, and keep your components clean and focused on rendering.

In this setup, everything related to data fetching, preview mode, and security lives in dedicated layers:

/app
  /api
    /draft-mode
      /enable/route.ts       ← validates preview secret, enables draft mode
      /disable/route.ts      ← clears draft mode cookie
  /(preview)
    /layout.tsx              ← wraps all preview routes in SanityLiveMode provider
  /[slug]/page.tsx           ← your standard page, preview-aware
/sanity
  /lib
    client.ts                ← public + preview client config
    queries.ts               ← all GROQ queries (no inline queries in components)
    token.ts                 ← server-only token access (import 'server-only')
  /components
    SanityLiveMode.tsx       ← thin wrapper for the client-side live mode provider
  sanity.config.ts           ← studio config including Presentation Tool

Step 1: Configure your Sanity clients

Configuring your Sanity clients is the most security-critical part of the setup. You need two clients: one for public (published) content served via CDN, and one for authenticated preview requests that must never reach the browser.

// sanity/lib/client.ts
import { createClient } from "@sanity/client";
import type { QueryParams } from "@sanity/client";

const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!;
const apiVersion = "2024-07-01"; // Pin this. Don't use "latest" in production.

// Public client — used for all non-preview fetches
// useCdn: true is important for performance and cost at scale
export const client = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: true,
});

// Preview client — server-only, never exposed to the browser
// Import this ONLY in Server Components, Route Handlers, and server actions
export const previewClient = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: false,         // Drafts are never in the CDN
  token: process.env.SANITY_API_READ_TOKEN,
  perspective: "previewDrafts", // The key flag: overlays drafts onto published docs
  stega: {
    enabled: true,
    studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL ?? "http://localhost:3333",
  },
});

Without perspective: "previewDrafts", your token-authenticated client would return published documents just like the public client, minus the CDN. This option tells Sanity’s API to construct a merged view: draft version where it exists, published version where it doesn’t. Meanwhile, your GROQ queries don’t need to change at all.

Pro tip: Always pin apiVersion to a specific date. Using "latest" means a Sanity API update can silently change your query behavior in production. Pin it to a known-good date and update intentionally.

Step 2: Protect your read token

The preview token is a Sanity API token with Viewer-level read access to unpublished content. You should treat it like a database password. If it leaks to the browser, anyone could access content that isn’t meant to be public.

The rule is simple: keep it server-only. The import 'server-only' guard enforces this. If the file is ever pulled into a Client Component or bundled for the browser, Next.js will fail the build. It’s a small but effective safeguard against accidental leaks.

// sanity/lib/token.ts
import "server-only"; // This module will throw at build time if imported client-side

export function getPreviewToken() {
  const token = process.env.SANITY_API_READ_TOKEN;
  if (!token) {
    throw new Error(
      "Missing SANITY_API_READ_TOKEN. Add it to your .env.local and Vercel env vars."
    );
  }
  return token;
}

Step 3: Draft Mode route handlers

Next.js includes a built-in Draft Mode that lets you securely switch between published and draft content using an HTTP-only cookie. Access is protected by a short-lived, signed secret that Sanity adds to your preview URLs.

The /enable route turns Draft Mode on. It validates the incoming request with validatePreviewUrl, which checks the secret against your Sanity dataset using the server-only preview token. If valid, it sets the cookie and redirects the user to the preview page. If not, it returns an error. The /disable route simply clears the cookie and redirects the user back to the published view.

// app/api/draft-mode/enable/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
import { validatePreviewUrl } from "@sanity/preview-url-secret";
import { client } from "@/sanity/lib/client";
import { getPreviewToken } from "@/sanity/lib/token";

export async function GET(request: Request) {
  // validatePreviewUrl checks a cryptographically signed, time-limited secret
  // that Sanity generates and appends to your preview URL.
  // It uses the viewer token to read the secret document from your Content Lake.
  const { isValid, redirectTo = "/" } = await validatePreviewUrl(
    client.withConfig({ token: getPreviewToken() }),
    request.url
  );

  if (!isValid) {
    return new Response("Invalid or expired preview secret", { status: 401 });
  }

  // Sets a secure HTTP-only cookie. All subsequent requests in this browser
  // session will have draftMode().isEnabled === true.
  draftMode().enable();
  redirect(redirectTo);
}
// app/api/draft-mode/disable/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";

export function GET(request: Request) {
  draftMode().disable();
  // Redirect to origin if provided, otherwise home
  const { searchParams } = new URL(request.url);
  const redirectTo = searchParams.get("redirect") ?? "/";
  redirect(redirectTo);
}

Pro tip: Don’t use ?preview=true flags. A naive implementation that just checks for a query parameter in a Server Component is exploitable, as anyone can add ?preview=true to a URL and read your drafts. The validatePreviewUrl approach uses a signed secret that expires after a single use.

Step 4: Preview-aware data fetching

In preview-aware data fetching, the core pattern is a single query function that switches clients based on whether Draft Mode is active. At the page level, switching between the standard and preview clients is based on draftMode(), and the same query is run either way. Here, you should keep GROQ queries centralized (/sanity/lib/queries.ts), so they stay easy to manage, update, and keep in sync with your Sanity schema.

Caching is the key detail: preview requests must use { cache: "no-store" } to avoid stale drafts, while published content can use ISR (e.g., revalidate every 60 seconds).

// sanity/lib/queries.ts
import { groq } from "next-sanity";

// Co-locate your GROQ queries with the schema they mirror.
// This makes schema changes visible alongside their query impact.
export const postBySlugQuery = groq`
  *[_type == "post" && slug.current == $slug][0] {
    _id,
    _type,
    title,
    body,
    publishedAt,
    "slug": slug.current,
    mainImage {
      asset -> {
        _id,
        url,
        metadata {
          dimensions,
          lqip  // Low-quality image placeholder — useful for blur-up loading
        }
      },
      alt
    },
    author -> {
      name,
      "slug": slug.current,
      image { asset -> { url } }
    },
    seo {
      metaTitle,
      metaDescription,
      ogImage { asset -> { url } }
    }
  }
`;
// app/[slug]/page.tsx
import { draftMode } from "next/headers";
import { client, previewClient } from "@/sanity/lib/client";
import { postBySlugQuery } from "@/sanity/lib/queries";
import { PostPreview } from "./PostPreview";
import { PostPage } from "./PostPage";
import { notFound } from "next/navigation";
import type { Post } from "@/types/sanity";

interface Props {
  params: { slug: string };
}

export default async function Page({ params }: Props) {
  const { isEnabled: isDraftMode } = draftMode();
  const activeClient = isDraftMode ? previewClient : client;

  const post = await activeClient.fetch<Post | null>(
    postBySlugQuery,
    { slug: params.slug },
    // Cache options differ between preview and production
    isDraftMode
      ? { cache: "no-store" }             // Never cache draft content
      : { next: { revalidate: 60 } }      // ISR: revalidate published content every 60s
  );

  if (!post) {
    notFound();
  }

  if (isDraftMode) {
    return <PostPreview initial={post} params={params} />;
  }

  return <PostPage post={post} />;
}

Step 5: The real-time preview component

This step connects the server-rendered preview to Sanity’s live update stream. The useLiveQuery hook from next-sanity subscribes to Sanity’s real-time event stream via WebSocket and re-renders when mutations arrive — typically within 200–500ms of the editor making a change. How does it work?

The preview component receives initial data from the server, then hands off to the live subscription. This means the page renders instantly with current content and updates seamlessly as edits happen. It’s also important to make preview mode visible. A simple banner helps editors avoid confusing draft content with the live site.

// app/[slug]/PostPreview.tsx
"use client";
import { useLiveQuery } from "next-sanity/preview";
import { postBySlugQuery } from "@/sanity/lib/queries";
import { PostPage } from "./PostPage";
import type { Post } from "@/types/sanity";

interface Props {
  initial: Post;
  params: { slug: string };
}

export function PostPreview({ initial, params }: Props) {
  // useLiveQuery establishes a real-time subscription to the query.
  // `initial` is rendered immediately (from the server fetch), then
  // seamlessly replaced with live data as mutations arrive.
  // The `loading` flag is true only during the initial subscription setup.
  const [post, loading] = useLiveQuery(initial, postBySlugQuery, {
    slug: params.slug,
  });

  return (
    <div>
      {/*
        Always surface a visible preview indicator.
        Editors have been burned by thinking they're seeing the live site.
        A banner prevents that confusion.
      */}
      <div
        style={{
          position: "fixed",
          top: 0,
          left: 0,
          right: 0,
          zIndex: 9999,
          background: "#F5A623",
          color: "#000",
          textAlign: "center",
          padding: "8px",
          fontSize: "13px",
          fontWeight: 600,
        }}
      >
        Draft Preview — Changes are not yet published.{" "}
        <a href="/api/draft-mode/disable" style={{ textDecoration: "underline" }}>
          Exit Preview
        </a>
      </div>
      <div style={{ paddingTop: "40px" }}>
        <PostPage post={post} />
      </div>
    </div>
  );
}

The last thing to keep in mind is that passing initial from the server avoids a flash of empty content on load. The server fetch gives you immediate data, and the client subscription keeps it up to date. This combination is key to a smooth preview experience.

Step 6: The Presentation Tool and Visual Editing

This is where preview becomes interactive. The Presentation Tool embeds your frontend in an iframe inside the Studio, overlays a click-to-edit UI on every piece of content, and lets editors click any text on the page to jump directly to that field in the editor.

The setup is straightforward. In sanity.config.ts, you configure the Presentation Tool with your frontend URL and point it to the route that enables Draft Mode. This connects Studio to your live preview environment. And here is how you can do that:

// sanity.config.ts
import { defineConfig } from "sanity";
import { presentationTool } from "sanity/presentation";
import { structureTool } from "sanity/structure";
import { schemaTypes } from "./schemaTypes";

const isDev = process.env.NODE_ENV === "development";

export default defineConfig({
  projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
  dataset: process.env.SANITY_STUDIO_DATASET!,
  schema: { types: schemaTypes },
  plugins: [
    structureTool(),
    presentationTool({
      previewUrl: {
        origin: isDev
          ? "http://localhost:3000"
          : process.env.SANITY_STUDIO_PREVIEW_URL!, // Your production frontend URL
        draftMode: {
          enable: "/api/draft-mode/enable",
        },
      },
    }),
  ],
});

Behind the scenes, Visual Editing relies on Stega (steganographic encoding). With its help, Sanity embeds invisible field references into the text strings it returns. Using zero-width Unicode characters, Sanity encodes the document ID, field path, and base URL directly into every string value in your query response. The Visual Editing overlay reads these encoded references to know which Studio field to navigate to when an editor clicks on any piece of text.

When stega.enabled: true is set on your preview client, encoding happens automatically. In most cases, you don’t need to do anything. Just render your content normally, and the overlay will work. The only exception is when strings are used in places where invisible characters can break things (like <title>, meta tags, logs, or URLs). In those cases, use stegaClean() to strip the metadata.

// components/PostBody.tsx
"use client";
import { PortableText } from "@portabletext/react";
import { stegaClean } from "@sanity/client/stega";
import type { Post } from "@/types/sanity";

interface Props {
  post: Post;
}

export function PostBody({ post }: Props) {
  return (
    <article>
      {/*
        post.title contains invisible Stega-encoded field references when in preview mode.
        For rendering in visible UI: leave it as-is. The overlay reads the encoding.
        stegaClean() is needed ONLY when the string goes somewhere that
        would break or expose the invisible characters:
          - <title> tags
          - <meta> description / og:title / og:description
          - console.log output
          - JSON.stringify for analytics events
          - URL slugs or query params
      */}
      <h1>{post.title}</h1>
      {/* Example of where you DO need stegaClean */}
      {/* <Head><title>{stegaClean(post.title)}</title></Head> */}
      <PortableText value={post.body} />
    </article>
  );
}

Visual Editing works out of the box for most text fields. The overlay is smart enough to identify annotated text nodes in the DOM and attach click handlers. Where it doesn’t work out of the box is non-string fields (numbers, booleans, images) and fields that are dynamically assembled on the client. For those, Sanity provides a lower-level createDataAttribute API for manual annotation — see the Visual Editing docs for details.

Step 7: Environment variables

In this step, we’ll cover the environment variables that power preview. Public variables (prefixed with NEXT_PUBLIC_) are safe for the browser (things like project ID, dataset, and Studio URL). Private variables stay on the server. The most important is a read-only API token (Viewer role), which allows access to draft content during preview. You’ll also need a secret for signing preview URLs, which is used to securely enable Draft Mode and should be long and random.

# .env.local

# Public — safe to expose to the browser
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_STUDIO_URL=http://localhost:3333  # or your deployed studio URL

# Server-only — NEVER prefix with NEXT_PUBLIC_
# Generate from: Sanity dashboard → API → Tokens → Add API token
# Role: "Viewer" — read-only access to draft documents is all you need
SANITY_API_READ_TOKEN=sk...

# Used by validatePreviewUrl for secret-based preview URL signing
SANITY_REVALIDATE_SECRET=a-long-random-string-generate-with-openssl-rand-hex-32

Generate your revalidate secret with:

openssl rand -hex 32

Performance considerations

Once your preview setup is up and running, there are a few performance factors to keep in mind:

  • CDN vs. API origin: useCdn: false means you’re hitting the Sanity API origin directly instead of the CDN edge. This is correct for drafts — CDN won’t have them — but it means higher latency and faster API quota consumption. You should keep useCdn: true on the public client for all non-preview traffic and be careful not to accidentally flip useCdn: false globally.
  • Stega overhead on metadata: stegaClean() is a cheap operation, but forgetting it on <title> or Open Graph tags is a silent bug. Search engines and social scrapers will receive the invisible characters, which may affect how your page titles render in SERPs and link previews. To avoid it, run a quick audit on all places you use the post title: page <title>, og:title, og:description, twitter:title.
  • WebSocket connections in preview: Each useLiveQuery call opens a separate listener on Sanity’s real-time API. If you have a complex page that fetches data in multiple components, each with its own useLiveQuery, you’ll create multiple connections. This adds unnecessary overhead, which is why it’s better to consolidate queries at the page level and pass data down via props where possible.
  • CORS configuration: This one bites people during Presentation Tool setup. To deal with it, add your frontend origin to the Sanity project’s CORS allowlist (project dashboard → API → CORS Origins). Without it, the iframe will be blocked by the browser’s CORS policy, and you’ll see a blank preview pane with a console error.

To ensure the preview can fetch data correctly instead of failing with a blank screen or console errors, add your frontend origins to the Sanity project’s CORS allowlist (Project dashboard → API → CORS Origins), for example:

  https://your-production-domain.com
  http://localhost:3000
  https://your-studio.sanity.studio    (if studio is on a subdomain)

Preview performance is only one part of the picture. For teams that want ongoing visibility after launch, Halo Lab’s Sanity Lighthouse plugin brings PageSpeed Insights into the Studio.

{{banner-2}}

Part 3: Other framework integrations

Next.js has the richest first-party support, but Sanity’s preview system is framework-agnostic at its core. Any framework capable of server-side rendering and client-side event subscriptions can support both Draft Mode and Visual Editing. The patterns are consistent; only the routing and cookie APIs differ. In the following sections, we’ll look at how the same preview architecture translates across popular JavaScript frameworks.

Remix

Remix’s loader model maps cleanly to preview mode. The main difference is that this framework doesn’t have a built-in Draft Mode API, so you manage the preview state via a cookie yourself. Here is a short integration overview:

// In your Remix loader
export async function loader({ request }: LoaderFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const previewCookie = await previewCookieParser.parse(cookieHeader);
  const isPreview = Boolean(previewCookie?.enabled);

  const sanityClient = isPreview ? previewClient : publicClient;
  const post = await sanityClient.fetch(postQuery, { slug: params.slug });

  return json({ post, isPreview });
}

Remix’s useRevalidator hook is particularly useful here, since you can call it on the client side after a Sanity mutation event to trigger a fresh loader fetch, effectively simulating the same real-time update behavior as Next.js’s useLiveQuery.

Nuxt (Vue)

The @nuxtjs/sanity module integrates with Nuxt’s server-side rendering pipeline and provides a composable-based API that feels natural in Vue. The integration details include:

  • Package: @nuxtjs/sanity
  • Strategy: The module exposes useSanityPreview() for reactive preview data
  • Visual Editing: Fully supported via @sanity/visual-editing/nuxt
  • Module docs: sanity.nuxtjs.org
  • Visual Editing guide: sanity.io/docs/visual-editing
<script setup>
// useSanityQuery automatically handles preview mode when the module is configured
const { data: post } = await useSanityQuery(postQuery, { slug: route.params.slug })
</script>

<template>
  <article>
    <h1>{{ post?.title }}</h1>
  </article>
</template>

The Nuxt module handles the perspective: "previewDrafts" switching behind the composable API, which makes the client-side code notably cleaner than the manual switching you do in Next.js.

SvelteKit

SvelteKit works smoothly with Sanity even without an official adapter. You can use @sanity/client directly in server load functions and combine it with Svelte stores to handle reactive client-side updates. The main integration details are:

  • Package: @sanity/client (native, no adapter needed)
  • Strategy: Check for a preview cookie in +layout.server.ts; pass isPreview to page load functions; use a Svelte store with client.listen() for live updates
  • Visual Editing: Supported via @sanity/visual-editing
  • Official guide: Visual Editing with SvelteKit
  • Visual Editing docs: sanity.io/docs/visual-editing
// +page.server.ts
export async function load({ params, cookies }) {
  const isPreview = cookies.get("sanity-preview") === "true";
  const activeClient = isPreview ? previewClient : publicClient;
  const post = await activeClient.fetch(postQuery, { slug: params.slug });
  return { post, isPreview };
}

SvelteKit’s granular server/client separation is actually a good fit for the preview architecture. The +page.server.ts / +page.ts split maps naturally to the “token stays server-side” constraint.

Astro

Astro’s partial hydration (“islands”) architecture requires some extra thought for preview mode, because most of the page is rendered as static HTML with no client-side reactivity by default.

The key architectural decision in Astro is isolating the live-update functionality in a dedicated island component, since the static shell can’t subscribe to events. The Sanity Astro package handles the middleware setup; you provide the island components. Here are some of this framework’s integration details:

  • Package: @sanity/astro
  • Strategy: Use Astro middleware to check for a preview cookie; render preview-specific island components using Astro’s client:load directive for the live-update parts
  • Integration docs: The official Sanity integration for Astro
  • Visual Editing: Supported, but requires care around which islands get annotated

Gatsby

It’s not exactly news that Gatsby’s popularity as a framework has declined significantly. However, its Sanity integration remains viable for teams that have existing Gatsby sites. The key integration points are as follows:

  • Package: gatsby-source-sanity
  • Strategy: The source plugin handles draft content fetching; preview mode requires separate configuration
  • Docs: Gatsby source plugin

For new projects, strongly consider Next.js or one of the above alternatives over Gatsby. The Sanity ecosystem’s tooling is significantly more mature than the others.

Sanity preview production checklist

Before handing preview off to your content team, run through these checks. Each covers a configuration issue that can easily slip through QA and cause problems in production.

  • The preview route uses validatePreviewUrl, and draft content is inaccessible without a valid preview secret.
  • SANITY_API_READ_TOKEN has the Viewer role and is imported only from server-only modules.
  • The public and preview clients use separate configurations: useCdn: true for published content and useCdn: false with perspective: "previewDrafts" for preview.
  • Preview requests use cache: "no-store" rather than ISR, revalidation, or the CDN.
  • stegaClean() is applied to strings used in <title>, metadata, Open Graph, Twitter cards, URLs, and analytics.
  • CORS origins and the Presentation Tool’s previewUrl.origin are configured for every environment.
  • Editors can clearly see when Draft Mode is active and can exit through a working /api/draft-mode/disable route.
  • The full workflow has been tested in production-like conditions, including draft updates, Visual Editing, and unauthorized access.

For content teams: your practical guide

This section requires no technical knowledge. If you are a content editor or manager, it will serve as a field guide for using real-time preview in your daily workflow.

Sanity Studio Presentation tool with a rendered blog post on the left and its document fields on the right
Presentation Tool renders your real frontend beside the editor

What is real-time preview?

When you edit content in Sanity, you’re working in a draft — a private version of your document that only your team can see until you hit Publish. Real-time preview lets you see exactly how that draft will look on the live website, right inside the Studio, before you publish anything.

Think of it like Google Docs: your changes save automatically and you can see exactly what the document looks like as you work. Except instead of a Google Doc, you’re seeing your actual website page.

How to open the preview

First, open your document in Sanity Studio. Then, look for a “Presentation” panel — it’s typically in the left sidebar or accessible via a button in the top toolbar labeled “Preview” or an eye icon. Click it, and your website page loads in a panel on the right side of the screen. After that, you can start editing your content on the left, and remember that the preview updates automatically as you type.

Pro tip: If you don’t see the Presentation panel, your developer may not have enabled it yet. Drop them a message — it’s a one-time setup on their end.

Once open, you’ll work in a split view:

  • Left side: Your document fields — title, body text, images, metadata
  • Right side: Your actual website, rendering with your real design and layout
Sanity preview panel with a click-to-edit overlay highlighting a project block on the draft page
Split-screen preview with Visual Editing on the draft page

What’s more, you can resize the preview panel by dragging the divider. Make it narrow or wider to see how your content looks on a mobile screen or a desktop.

Click-to-edit (Visual Editing)

If your developer has set up Visual Editing, you’ll notice that when you hover over any piece of text or an image in the preview panel, it highlights with a subtle outline. Click it, and the Studio editor on the left automatically jumps to that specific field.

This is especially useful on complex pages. This way, instead of hunting through a long form trying to figure out which field controls the text you want to change, you can just click directly on it in the preview.

What updates automatically vs. what needs a refresh

Preview updates don’t all behave the same way. Some content changes appear instantly through Sanity’s real-time connection, while others depend on how your application handles routing, data fetching, or global components. The table below shows what editors can expect during the preview workflow.

Content typePreview behavior
Text and rich text editsUpdates instantly as you type
Image field changesUpdates when you select a new image
Slug and URL changesUsually requires a manual refresh
Page structure changes (adding/removing sections)Reflects after saving the draft
Navigation menus and global componentsMay require a full preview reload — ask your developer

The difference between preview and the live site

The preview always shows your draft — the version with your latest changes. However, visitors to your website still see the published version. You can experiment freely, since your draft is completely private until you click Publish.

You can make sweeping changes to a page in your draft, preview exactly how it will look, get approvals from stakeholders, and publish when you’re ready — all while the live site remains untouched. Therefore, the preview is a safe space to experiment.

It is much easier to approve a page when everyone is looking at the same thing.

Sharing a preview with stakeholders

Apart from being comfortable for editors, the preview has another benefit. Whenever you need to share a preview link with someone who doesn’t have Studio access, a client, a manager, a legal reviewer — ask your developer to generate a preview URL for the page. It’s a special link that shows the draft version without requiring a Studio login.

Pro tip: preview links are time-limited (they expire after a short period), and they’re specific to one page. They’re not meant for permanent sharing — think of them as a “review this before we publish” link, not a permanent staging environment.

Common content teams’ questions

If a preview workflow feels unfamiliar at first, you’re not alone — especially when changes don’t appear instantly, or draft content behaves differently from the live site. These are some of the most common questions editors have when working with Sanity previews:

  • My preview stopped updating. What do I do? First, try making a small edit to the draft (even adding and removing a space). If the preview still isn’t updating, try the refresh button in the preview toolbar. If it’s still stuck after that, let your developer know. It may be a connection issue or a configuration problem on the frontend.
  • The preview looks different from the live site. Is that normal? Sometimes. The preview is showing your draft content in your real design, but certain dynamic elements, like personalized recommendations, may not appear in preview mode. Ask your developer which parts of your pages are “preview-safe.”
  • Can I preview a brand-new page before it’s ever been published? Yes. Create a new document, fill in your content, open the preview, and you’ll see exactly how that page would look when live. Nobody else sees it until you publish.
  • Who can see my drafts? Anyone with access to your Sanity Studio can see drafts in the editor. Anyone your developer sends a preview URL to can see the draft for that specific page in the browser. But nobody visiting your actual website sees anything until you publish.
  • What’s the difference between “Save” and “Publish?” Saving stores your draft. Publishing replaces the live version with your draft and makes your changes visible to the world.

Preview makes a CMS work for editors, not just developers

Real-time preview with Sanity closes the gap between the CMS and the live site. Developers get a secure, predictable way to render draft content, while editors can see changes in context, experiment without affecting the published page, and share work for approval before it goes live.

The result is a headless CMS that feels less like a backend tool and more like a shared editorial workspace. Writers and engineers can work in parallel, with fewer handoffs, less guesswork, and more confidence in what reaches production.

{{banner-3}}

FAQ

Does real-time preview slow down my production site?

No. Preview runs through a separate, token-authenticated client and only activates in Draft Mode or the Studio. Your public traffic keeps using the CDN-backed client, so visitors see the same fast, cached pages as before.

Is it safe to expose draft content this way?

Yes. Drafts are never served to anonymous visitors: access requires a signed, single-use secret, and the token never reaches the browser.

Do I have to use Next.js?

No. Sanity’s preview system is framework-agnostic. Next.js has the deepest first-party support, but Remix, Nuxt, SvelteKit, Astro, and Gatsby all support Draft Mode and Visual Editing; only the routing and cookie handling differ.

What’s the difference between Draft Mode and Visual Editing?

Draft Mode renders a full draft page at a secret-gated URL. Visual Editing adds a click-to-edit overlay in the Studio, letting editors click any element to open its field.

Will Stega’s invisible characters hurt my SEO?

Only if you forget to strip them. Stega encodes editing metadata into strings for the overlay; it’s harmless in visible page text, but must be removed with stegaClean() before it reaches <title>, meta descriptions, or Open Graph tags.

How long does it take to set up?

For a Next.js project, budget a day or two for the core Draft Mode pipeline and a few more hours to wire up Visual Editing. Most of the effort is the secure client configuration; once that’s right, adding preview to new page types is quick.

Short on time?

We build Sanity CMS platforms with preview wired in from day one.

Explore Sanity development

Need live preview in Sanity?

We wire it securely into your CMS and frontend.

Explore Sanity development

Tired of publish-and-pray?

We build CMS platforms with preview workflows editors can trust.

Explore CMS development

copy iconcopy icon
copy iconcopy icon
Sum UP
Get a free checklist
Please, enter your full name
Please, enter your email
Please, enter your job title
Download now
Check out your email inbox
Oops! Something went wrong while submitting the form.
Get a free guide
Please, enter your full name
Please, enter your email
Please, enter your job title
Download now
Check out your email inbox
Download guide
Oops! Something went wrong while submitting the form.