Go Behind the Experience to see how Tecovas brings the West to life with Sanity 🤠 July 18th

Previewing content in presentation

How to approach previews of unpublished and draft content from presentation contexts, and how to embed previews in the Studio

When editing content, it can be useful for content creators to be able to preview how it will appear before publishing.

There are different approaches to previewing unpublished content depending on your use case, the technology you're building with, and how complex your content models are.

With Sanity Studio, you can accommodate previews in different ways:

  • Generate a link in the user interface to an external preview or production environment with document.productionUrl in the Studio configuration
  • Build custom side-by-side previews by customizing the document view with Structure Builder
  • Embed a frontend presentation tailored for previews in a document view using the Iframe Pane plugin, or a custom view component.

Generating a link to a production environment

You can generate a link that will appear behind the three-dotted menu above the document view.

The three-dotted menu allows you to open a generated URL in a new tab

This is done by returning a URL via document.productionUrl where you configure your Studio, typically in sanity.config.ts. It takes a function that has any previous productionUrl values as the first argument, and context as it second, with useful values like the current document state, projectId, dataset, currentUser, a configured client for fetching referenced documents, and the Studio's schema.

You can also return a promise in case you want to fetch information from other documents or an external source to generate the URL.

// sanity.config.ts
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'

export default defineConfig({
  name: 'default',
  title: 'My Cool Project',
  projectId: 'my-project-id',
  dataset: 'production',
document: {
// prev is the result from previous plugins and thus can be composed
productionUrl: async (prev, context) => {
// context includes the client and other details
const {getClient, dataset, document} = context
const client = getClient({apiVersion: '2023-05-31'})
if (document._type === 'post') {
const slug = await client.fetch(
`*[_type == 'routeInfo' && post._ref == $postId][0].slug.current`,
{postId: document._id}
const params = new URLSearchParams()
params.set('preview', 'true')
params.set('dataset', dataset)
return `https://my-site.com/posts/${slug}?${params}`
return prev
}, })


Sanity Studio currently supports only one generated link. You can come into situations where plugins implement their own productionUrl setting. Depending on how you deal with the
prev value, your customization might override a plugin's.

How to think about unpublished changes

What are unpublished changes in the Studio?

It‘s useful to understand Sanity Studio‘s document publishing model before building content previews. Because usually, but not always, a preview renders the unpublished changes for the documents used to build the presentation.

For a content creator, a document can primarily have three states:

  • A draft document that has never been published
  • A published document with no changes
  • A published document with unpublished changes

From a Content Lake perspective, it‘s slightly different. Whenever you edit a document, the Studio will copy its current state into a new document and prepend drafts. to its _id, for example drafts.animal-123. The dot notation in an _id has a special behavior called paths. Corresponding to the scenarios above, you can query for documents that:

  • only has a drafts. _id: ['drafts.animal-123']
  • only has a published version: ['animal-123']
  • has both a published and a draft version: ['drafts.animal-123', 'animal-123']

Alternative publishing models

What is considered “published” might mean different things depending on the context. For example, if you are statically building a website using Sanity content, you might have documents that are published in the Studio but not on the website because it hasn't been rebuilt with the published changes.

You can also leverage GROQ/GraphQL and document fields to define what it means for a document to be published from the perspective of the presentation layer.


Unless you build custom permissions around alternative publishing conditions, any document that is not a path (like drafts.) will be queryable with unauthenticated requests if the dataset is public. This means that even though your presentation might filter out a document that you consider not published, someone might be able to query your API directly and find the content since it's published to the API.

Content previews outside of Sanity Studio

Previewing content restricted to a single document is mostly a matter of looking for the drafts. version of a document and using its values to render the presentation. This might be sufficient for most cases as long as content creators are informed of the limitations of the preview.

Drafts are, by default, non-public and only visible to authenticated requests with the right permissions. This means that to preview drafts outside of the Studio context; you will have to make authenticated requests to the Sanity API to fetch the draft document. As a consequence, you need to make sure that you can access the draft document from your front end.

Where things can get complicated is when you have referenced content that holds its own publish state. Practically, you can't resolve references to a draft unless you split your queries up into multiple requests. If you wish to have a real-time preview, this also means that you have to set up dedicated listeners for any referenced document to pick up its changes before merging them into the value used to render the presentation. It‘s technically possible; after all, this is how the Studio works, but it quickly becomes non-trivial.

Using preview tooling

We have found that a better model to accommodate real-time content previews in the presentation layer is to load the state of your dataset (or a selection of it) into an in-memory document store and stream any changes to it.

This way, you can query this store client-side with GROQ and get any unpublished changes across referenced content. Implementing previews then mostly means using its hooks and helper functions, as well as leveraging the user‘s authentication and/or adding a server-side token with view access to drafts.

  • @sanity/preview-kit is a generalized React library for accommodating live content previews. We plan to expand support for other JavaScript frameworks as well
  • For non-React JavaScript projects, you can use groq-store directly. It lets you set up a listener with a callback whenever a change happens
  • If you have a Next.js project, you can use the hook in our next-sanity toolkit to build previews.

Go to these libraries and explore their READMEs for further documentation.

Embedding previews in the Studio

You might have seen the Preview Anything blog post that features different types of previews beyond the visual representation of a website. It shows how you can preview how content appears in Search Engine Result Pages, social media, in different accessibility accommodations, and specialized renders of signage and physical publishing.

While you can use the document.productionUrl in the Studio configuration to generate helpful links for content creators, another approach is to add custom views to the document pane in the Studio. This is powerful, especially when you have support for real-time content previews.

You can add additional document views using the Structure Builder API. While you can add views to any document node when you define document list trees, the quickest way to add views is by using Desk tool‘s defaultDocumentNode property to return document views conditionally on a document‘s value (for example, its _type).

A document view takes any React component that receives document values for its different states in real-time (draft, displayed, historical, published) in props. You can use this with custom components to build previews or embed a remote preview web page in an <iframe>. If you want to do the latter, we recommend checking out the Iframe pane plugin available on sanity.io/exchange. Below is an example of how to implement it:

// sanity.config.ts
import {defineConfig} from 'sanity'
import { structureTool,type DefaultDocumentNodeResolver } from 'sanity/structure'
import Iframe from 'sanity-plugin-iframe-pane'
import {SanityDocument} from 'sanity'

// Customize this function to show the correct URL based on the current document
function getPreviewUrl(doc: SanityDocument) {
  return doc?.slug?.current
    ? `${window.location.host}/${doc.slug.current}`
    : window.location.host

const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
  // Only show preview pane on `movie` schema type documents
  switch (schemaType) {
    case `movie`:
      return S.document().views([
            url: (doc: SanityDocument) => getPreviewUrl(doc),
      return S.document().views([S.view.form()])

export default defineConfig({
  // ...other config settings
  plugins: [
    // ...other plugins

Was this article helpful?