Join live – Get insights, tips, + Q&A from Sanity developers on our latest releases

Configuring the Presentation tool

The Presentation tool enables visual editing with live previews. This is how you set it up with in your studio.

The Presentation tool for Sanity Studio enables visual editing with live previews. This article walks you through the configuration:

  • How to install the Presentation tool in your Studio
  • How to set up the base URL for your preview(s)
  • How to set up and configure document locations to show where a document is used in a presentation
  • How to add support for “preview/draft mode" in frameworks like Next.js

Prerequisites

To use the Presentation tool, you have to upgrade your Studio‘s sanity dependency to v3.20.0 or preferably latest.

To get the full visual editing and live preview experience in the Presentation tool, you will also need to install support in your front end(s) as well. You can go here to learn more about how to implement this for most JavaScript-based web frameworks.

You might also want to install rxjs as a dependency to your Studio project to enable real-time document locations with Observables:

npm install sanity@latest rxjs

Configuration

Similarly to the Structure tool (formerly known as the Desk tool), you add Presentation to your Studio's configuration file (code example below).

Basic configuration

  1. Import presentationTool from sanity/presentation
  2. Add it to the plugins array
  3. Add the base URL for your front end to the previewUrl attribute
// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
// ...other imports // We recommend configuring the preview location base URL using // environment variables to support multiple environments
const SANITY_STUDIO_PREVIEW_URL = (
process.env.SANITY_STUDIO_PREVIEW_URL
|| 'http://localhost:3000'
)
export default defineConfig({ // Your configuration for the project ... plugins: [ // Add 'presentationTool' to the 'plugins' array
presentationTool({
// Required: set the base URL to the preview location in the front end
previewUrl: SANITY_STUDIO_PREVIEW_URL,
}),
// ...other plugins ], })

Configuration reference

The Presentation tool takes the following configuration attributes:

  • REQUIREDpreviewUrlstring

    The relative or absolute path to the base location in the front end you want to enable in the Presentation tool. If your Studio shares the same base URL as your front end, you can use relative URLs. We recommend implementing this as an environment variable to make it easier to work in development and production.

    Examples:

    • http://localhost:3000
    • https://your-production-url.com/
    • /

    You may also configure the Presentation tool to handle enabling/disable draft automatically.

  • iconReact component

    Sets an icon to represent the Presentation tool in the Studio's navigation bar. To use Sanity icons, install the @sanity/icons package.

  • namestring

    Sets a name for the Presentation tool. It's not visually represented in the Studio UI, but it’s included in the Studio's URL for the Presentation tool.

    This is useful if you want to implement multiple instances of Presentation, for example, for different channels, domains, or preview environments.

    Default value: presentation

    Examples: presentation, preview, website, app

  • titlestring

    Sets the title that is displayed in the Studio's navigation bar. Keep it short and descriptive to help editorial users understand what they can do with the tool.

    Default value: Presentation

    Examples: Website Preview, Newsletter Preview, US Site Preview

  • locateObservable<DocumentLocationsState>

    This takes a callback function that returns a list of document locations. These are typically the URLs where content from a selected document is used.

    For more information about the locate callback, see Configure Location resolver below.

Configuring Document Locations

Typically, a page on your front end will contain content from multiple documents when using structured content. For example, a marketing landing page may include content from a landing page document as well as content from referenced products, people, shared content modules, and more. You can also save time and effort by having documents spanning multiple locations across several digital channels.

The Document Locations Resolver generates a list of the locations where a document is used. This helps editors understand how a change in a single document can affect the different places it is used.

The Locations UI showing where a product has been used

The locate callback property is a resolver that takes a document id and type parameters, and returns a state object that contains the locations that can be previewed for a given document.

Basic example

The example below shows you how to implement document locations for a simple blog post document type where its id is used as part of the URL scheme:

// locate.ts

export const locate: DocumentLocationResolver = ({id, type}) => {
	// Set up locations for documents of the type "post"
  if (type === 'post') {
    return {
      // '/post' is an example path.
      // Replace it with an actual relative or absolute value
      // depending on your environment
      locations: [
        {title: `Post #${id}`, href: `/post/${id}`},
        {title: 'Posts', href: '/posts'},
      ],
    }
  }
  return null
}

We recommend containing the Document Location Resolver in a dedicated file to reduce clutter in your Studio configuration file. In the example above, it's exported as a named JavaScript variable so it can be imported to the studio configuration file like this:

// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
import {locate} from './locate'
// ...other imports export default defineConfig({ // ...configuration for the project plugins: [ presentationTool({ previewUrl: SANITY_STUDIO_PREVIEW_URL,
locate
}), // ...other plugins ], })

Generate locations based on document data

Most often, URLs are built up from document fields (like slug.current). The callback passes a second context parameter with a function that has methods for interacting with documents in real time. In most cases, you will call context.documentStore.listenQuery with a GROQ query that returns the queried documents as an Observable.

We recommend installing rxjs as a dependency in your Studio project to make it easier to interact with the Observable object returned from listenQuery:

npm install rxjs

Now, you can use the parameters you get from the selected document to access the document data and generate the URLs based on it. Below is an example of how to do this for a blog post document where the slug field is used for URLs:

// locate.ts
import { DocumentLocationResolver } from "sanity/presentation";
import { map } from "rxjs";

// Pass 'context' as the second argument
export function locate(params, context): DocumentLocationResolver {
  // Set up locations for post documents
  if (params.type === "post") {
    // Subscribe to the latest slug and title
    const doc$ = context.documentStore.listenQuery(
      `*[_id == $id][0]{slug,title}`,
      params,
      { perspective: "previewDrafts" } // returns a draft article if it exists
    );
    // Return a streaming list of locations
    return doc$.pipe(
      map((doc) => {
        // If the document doesn't exist or have a slug, return null
        if (!doc || !doc.slug?.current) {
          return null;
        }
        return {
          locations: [
            {
              title: doc.title || "Untitled",
              href: `/post/${doc.slug.current}`,
            },
            {
              title: "Posts",
              href: "/",
            },
          ],
        };
      })
    );
  }
  return null;
}

Show all locations where a document is being used

Typically, with structured content, content from a document might be used in multiple locations by means of references. With GROQ, you can query for all documents that refer to a specific document (or use other join logic) and use that to build a list of locations for where a document is being used.

Below is an example showing how to build a list of locations for a "person" document that is being referred to by a "post" document as its author:

// locate.ts
import {
  DocumentLocationResolver,
  DocumentLocationsState,
} from 'sanity/presentation'
import { map, Observable } from 'rxjs'

export const locate: DocumentLocationResolver = (params, context) => {
  if (params.type === 'post' || params.type === 'person') {
    /* 
      Listen to all changes in the selected document 
      and all documents that reference it
    */
    const doc$ = context.documentStore.listenQuery(
      `*[_id==$id || references($id)]{_type,slug,title, name}`,
      params,
      { perspective: 'previewDrafts' },
    ) as Observable<
      | {
          _type: string
          slug?: { current: string }
          title?: string | null
          name?: string | null
        }[]
      | null
    >
    // pipe the real-time results to RXJS's map function
    return doc$.pipe(
      map((docs) => {
        if (!docs) {
          return {
            message: 'Unable to map document type to locations',
            tone: 'critical',
          } satisfies DocumentLocationsState
        }
        // Generate all the locations for person documents
        const personLocations = docs
          .filter(({ _type, slug }) => _type === 'person' && slug?.current)
          .map(({ name, slug }) => ({
            title: name || 'Name missing',
            href: `/authors/${slug.current}`,
          }))

        // Generate all the locations for post documents
        const postLocations: Array<any> = docs
          .filter(({ _type, slug }) => _type === 'post' && slug?.current)
          .map(({ title, slug }) => ({
            title: title || 'Name missing',
            href: `/posts/${slug.current}`,
          }))

        return {
          locations: [
            ...personLocations,
            ...postLocations,
            // Add a link to the "All posts" page when there are post documents
            postLocations.length > 0 && {
              title: 'All posts',
              href: '/posts',
            },
            // Add a link to the "All authors" page when there are person documents
            personLocations.length > 0 && {
              title: 'All authors',
              href: '/authors',
            },
          ].filter(Boolean),
        } satisfies DocumentLocationsState
      }),
    )
  }

  return null
}

Document Location Resolver Reference

In addition to the example above, the Document Location Resolver can return customized top-level messages and visual cues:

  • messagestring

    Override the top-level text in the document locations UI. Useful if you want to customize the string (replace "pages") or display warnings.

    Default value: Used on ${number} pages

    Examples:

    • Unable to resolve locations for this document
    • Used in ${docs.length} email campaign${docs.length === 1 ?? 's'}
  • tone'positive' | 'caution' | 'critical'

    Gives the document locations UI a background color. It can be used to signal criticality.

    Default: positive

  • locationsDocumentLocation[]

    An array of document locations objects with title and href properties. The href can be absolute or relative and will open in the Presentation tool.

Was this article helpful?