Presentation Resolver API
Programmatically generate shortcuts from document forms to relevant routes in the Presentation Tool
The Presentation Tool lets you add shortcuts for quickly opening a document and its preview. This is done by adding configuration to resolve.mainDocuments and resolve.locations where you define what content goes into a specific route.
Main Document Resolvers lets you express the most important document for any given route. Moreover, the Locations Resolvers lets you define patterns for other routes that content from any given document might be used.
Main document resolvers
The Main Document Resolver API provides a method of resolving a main document from a given route or route pattern.
Often, a route in an application will be closely tied to a document in Content Lake. For instance, a blog post will likely draw most of its content from a single post document. We could describe this as the "main" document for that post route.
// sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {defineDocuments, presentationTool} from 'sanity/presentation'
export default defineConfig({
/* ... */
plugins: [
presentationTool({
/* ... */
resolve: {
mainDocuments: defineDocuments([
{
route: '/posts/:slug',
filter: `_type == "post" && slug.current == $slug`,
},
]),
},
}),
structureTool(),
],
})When a main document is defined, Presentation will automatically display it when navigating to the matching route in our application.
The resolve.mainDocuments property of the Presentation Tool's configuration accepts an array of objects, each with a route pattern and method for resolving a document. This can be a document type, an object with a GROQ filter and optional params, or a resolve function that returns one.
Internally, when Presentation detects a navigation event, it will compare the current URL against one of these resolver methods. The first matching document will be considered the main document, and be automatically displayed as the navigation occurs.
// sanity.config.ts
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {defineDocuments, presentationTool} from 'sanity/presentation'
export default defineConfig({
/* ... */
plugins: [
presentationTool({
/* ... */
resolve: {
mainDocuments: defineDocuments([
// Document type, useful shorthand for singleton documents.
{
route: '/products',
type: 'productsListing',
},
// GROQ filter with static parameters.
{
route: '/products/a-unique-product',
filter: `_type == "product" && slug.current == $slug`,
params: {slug: 'has-a-different-slug'},
},
// GROQ filter, infer the parameters from the route definition.
{
route: '/products/:slug',
filter: `_type == "product" && slug.current == $slug`,
},
// Resolve function for more complex cases, for example modifying the slug.
{
route: '/pages/:type/:slug',
resolve(ctx) {
const {params} = ctx
return {
filter: `_type == $type && slug.current == $slug`,
params: {
type: params.type,
slug: params.slug.replaceAll('_', '-'),
},
}
},
},
// Supports both array and absolute URL route definitions
{
route: ['https://sanity.io/:slug', 'https://sanity.studio/:slug'],
filter: `_type == "page" && slug.current == $slug`,
},
]),
},
}),
structureTool(),
],
})Pro tip
defineDocuments() is an optional helper function for TypeScript users. You can provide an array directly if you don't need type safety.
The paths of each route value are parsed using path-to-regexp v8 to extract the parameters. Studio v6.7.0 and later use path-to-regexp v8. Earlier versions, including Studio v5 and Studio v6.0.0 through v6.6.0, use path-to-regexp v6, which parses wildcards differently.
If an origin is provided, it will not be parsed for parameters, i.e. this will be matched literally: https://:subdomain.sanity.io
Wildcard and catch-all routes
To match an unknown number of path segments, name a wildcard parameter with /*name. The /:name* syntax from path-to-regexp v6 is not valid in v8, and a route that uses it throws "/:name*" is not a valid route pattern when Presentation matches a navigation event. A bare * is invalid too: every wildcard must be named.
A wildcard parameter resolves to an array of path segments rather than a string. TypeScript does not catch this, because DocumentResolverContext types every route parameter as a string, so handle both shapes in resolve:
// mainDocuments.ts
import {defineDocuments} from 'sanity/presentation'
export const mainDocuments = defineDocuments([
// List specific routes before the catch-all: the first match wins.
{
route: '/posts/:slug',
filter: `_type == "post" && slug.current == $slug`,
},
{
route: '/*pathSegments',
resolve(ctx) {
// A wildcard returns an array of path segments, even though
// `DocumentResolverContext` types every parameter as a string.
const raw = ctx.params.pathSegments
const segments: string[] = Array.isArray(raw)
? raw
: typeof raw === 'string'
? raw.split('/').filter(Boolean)
: []
// Nothing to match on, so this route has no main document.
if (segments.length === 0) return undefined
const slug = segments[segments.length - 1]
const section = segments.length === 1 ? 'top' : segments.slice(0, -1).join('|')
return {
filter: `_type == "page" && section == $section && slug.current == $slug`,
params: {slug, section},
}
},
},
])/*pathSegments matches one or more segments: /about resolves to ['about'], and /docs/guides/install resolves to ['docs', 'guides', 'install']. It does not match the site root, /. To match the root as well, wrap the wildcard in braces to make it zero or more: {/*pathSegments}. At the root the parameter is then undefined, which the example above handles by returning no main document.
Presentation uses the first route that matches, so list a catch-all last. A catch-all matches every path, and any route defined after it never runs.
Document location resolvers
The Document Locations Resolver API allows you to define where data is being used in your application(s).
It gives content editors visibility of the pages that display the content they are editing, and the potential impact of the changes they make.
The resolve.locations property of the Presentation Tool's configuration accepts an object whose keys each correspond to a document type in your schema. The corresponding value provides a method for resolving document location state.
If you're used to configuring preview options in your schema, the API will feel familiar.
The select object defines the fields that should be returned in the document object passed to the resolve function. The resolve function itself accepts a document and should return some DocumentLocationsState.
Alternatively, you can directly pass DocumentLocationsState.
Pro tip
defineLocations() is an optional helper function for TypeScript users. You can provide the object directly if you don't need type safety.
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {defineLocations, presentationTool} from 'sanity/presentation'
import {EnvelopeIcon} from '@sanity/icons/Envelope'
export default defineConfig({
/* ... */
plugins: [
presentationTool({
/* ... */
resolve: {
locations: {
// Resolve locations using values from the matched document
product: defineLocations({
select: {
title: 'title',
slug: 'slug.current',
},
resolve: (doc) => ({
locations: [
{
title: doc?.title || 'Untitled',
href: `/products/${doc?.slug}`,
icon: EnvelopeIcon, // Custom icon (optional)
showHref: false, // Show/hide URL (default: true)
},
{
title: 'Products',
href: '/products',
},
],
}),
}),
// Define static locations
productListing: defineLocations({
select: {title: 'title'},
resolve: () => ({
locations: [
{ title: 'Products', href: '/products'},
]
})
}),
// Provide a notice when a document is used across all pages
siteSettings: defineLocations({
message: 'This document is used on all pages',
tone: 'caution',
}),
},
},
}),
structureTool(),
],
})Advanced location resolvers
The above pattern should cover the vast majority of use cases. However, if you need more fine-grained control, resolve.locations also accepts a DocumentLocationResolver function.
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. You can also add @sanity/id-utils to make capturing the draft and/or version IDs easier.
npm install rxjs @sanity/id-utils
pnpm add rxjs @sanity/id-utils
yarn add rxjs @sanity/id-utils
bun add rxjs @sanity/id-utils
// locations.ts
import {DocumentLocationResolver} from 'sanity/presentation'
import {DocumentId, getDraftId} from '@sanity/id-utils'
import {map} from 'rxjs'
// Pass 'context' as the second argument
export const locations: DocumentLocationResolver = (params, context) => {
// Set up locations for post documents
if (params.type === 'post') {
const query = {
fetch: `*[_id==$id][0]{slug,title}`,
listen: `*[_id in [$id,$draftId]]`,
}
const queryParams = {id: params.id, draftId: getDraftId(DocumentId(params.id))}
// Subscribe to the latest slug and title
const doc$ = context.documentStore.listenQuery(
query,
queryParams,
{perspective: 'drafts'}, // 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
}The Document Location Resolver is a function that receives an object with the document's id and type parameters, and returns a state object that contains the locations that can be previewed for a given document.
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 {locations} from './locations'
// ...other imports
export default defineConfig({
// ...configuration for the project
plugins: [
presentationTool({
previewUrl: process.env.SANITY_STUDIO_PREVIEW_URL,
resolve: {
locations
}
}),
// ...other plugins
],
})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:
// locations.ts
import {
DocumentLocationResolver,
DocumentLocationsState,
} from 'sanity/presentation'
import { map, Observable } from 'rxjs'
export const locations: 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: 'drafts' },
) 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 state properties
In addition to the example above, the Document Location Resolver can return customized top-level messages and visual cues:
message
string
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} pagesExamples:
Unable to resolve locations for this documentUsed 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.
Optional. When omitted, the locations banner renders with no tone (no background color) and no tone icon.
locations
DocumentLocation[]
An array of document locations objects with
titleandhrefproperties. Thehrefcan be absolute or relative and will open in the Presentation Tool.
