Create Studio edit intent links
Learn how to construct URLs that open specific documents and fields in Sanity Studio for custom editorial interfaces and preview environments.
Edit intent links are URLs that open specific documents and fields in Sanity Studio. They are useful for building custom editorial interfaces or adding edit buttons to your preview environments.
When combined with Content Source Maps or steganography, you can automatically generate these URLs based on source map data for fully automated visual editing experiences. This article covers the manual approach for cases where you need direct control over the links. The resolveEditUrl helper covers the automatic approach, for cases where you have a Content Source Map but not the document ID.
Edit intent URL format
The basic format for an edit intent URL is:
<your-studio-url>/intent/edit/id=DOCUMENT_ID;type=DOCUMENT_TYPE;path=FIELD_PATHConstructing edit URLs programmatically
You can construct edit intent URLs programmatically. The following helper function builds URLs for documents, specific fields, and nested field paths:
// Helper function to create edit intent URLs
function createEditUrl({
studioUrl,
documentId,
documentType,
path = '',
}) {
// Intent parameters go in a single path segment, separated by semicolons
const params = [`id=${documentId}`, `type=${documentType}`]
if (path) {
params.push(`path=${encodeURIComponent(path)}`)
}
return `${studioUrl}/intent/edit/${params.join(';')}`
}
// Create link to edit a document
const editPostUrl = createEditUrl({
studioUrl: 'YOUR_STUDIO_URL',
documentId: 'post-123',
documentType: 'post',
})
// YOUR_STUDIO_URL/intent/edit/id=post-123;type=post
// Create link to edit a specific field
const editTitleUrl = createEditUrl({
studioUrl: 'YOUR_STUDIO_URL',
documentId: 'post-123',
documentType: 'post',
path: 'title',
})
// YOUR_STUDIO_URL/intent/edit/id=post-123;type=post;path=title
// Create link to edit nested field
const editAuthorNameUrl = createEditUrl({
studioUrl: 'YOUR_STUDIO_URL',
documentId: 'post-123',
documentType: 'post',
path: 'author.name',
})
// YOUR_STUDIO_URL/intent/edit/id=post-123;type=post;path=author.nameAdding edit buttons to a preview interface
You can use edit intent URLs to add edit buttons to your preview interface. Here are examples for document-level and field-level edit links:
// React component with edit button
function PostPreview({post}) {
const editUrl = createEditUrl({
studioUrl: process.env.NEXT_PUBLIC_STUDIO_URL,
documentId: post._id,
documentType: post._type,
})
return (
<article>
<header>
<h1>{post.title}</h1>
<a
href={editUrl}
target="_blank"
rel="noopener noreferrer"
className="edit-button"
>
Edit in Studio
</a>
</header>
<div>{post.body}</div>
</article>
)
}
// Field-level edit links
function EditableField({value, documentId, documentType, fieldPath}) {
const editUrl = createEditUrl({
studioUrl: process.env.NEXT_PUBLIC_STUDIO_URL,
documentId,
documentType,
path: fieldPath,
})
return (
<div className="editable-field">
<span>{value}</span>
<a href={editUrl} className="edit-icon" title="Edit this field">
✏️
</a>
</div>
)
}Resolve edit URLs from a Content Source Map
When you render query results, you don't always have the document ID and type at hand. A slug used to build a URL, for example, never appears on the page. resolveEditUrl from @sanity/client/csm takes a path into a query result and resolves the source document, type, and field path from the query's Content Source Map.
Alpha API
resolveEditUrl is marked @alpha in @sanity/client, and the createEditUrl function it calls is marked @internal. Both work today, but the signature and the URL they produce can change in a minor release.
Two settings are required before you can resolve a URL:
- Set
resultSourceMap: 'withKeyArraySelector'in the client config. Plaintruealso returns a source map, but links to array items break when the array is reordered. - Pass
filterResponse: falsetoclient.fetch(). The default response contains the result only, without the source map.
import { createClient } from '@sanity/client'
import { resolveEditUrl } from '@sanity/client/csm'
const client = createClient({
projectId: 'YOUR_PROJECT_ID',
dataset: 'production',
apiVersion: '2026-07-01',
useCdn: false,
// 'withKeyArraySelector' keeps links to array items stable when the array is reordered
resultSourceMap: 'withKeyArraySelector',
})
const { result, resultSourceMap } = await client.fetch(
'*[_type == "author" && slug.current == $slug][0]{name, slug, pictures}',
{ slug: 'john-doe' },
// Without this, the response contains the result only
{ filterResponse: false }
)
// resultSourceMap is undefined unless the client asks for it
const editAltTextUrl = resultSourceMap
? resolveEditUrl({
studioUrl: 'YOUR_STUDIO_URL',
resultSourceMap,
// A path into the query result, as a string or an array of segments
resultPath: 'pictures[0].alt',
})
: undefinedThe resolved URL carries the intent parameters as a path segment and repeats them as a query string:
YOUR_STUDIO_URL/intent/edit/mode=presentation;id=462efcc6-3c8b-47c6-8474-5544e1a4acde;type=author;path=pictures%5B_key%3D%3D%22cee5fbb69da2%22%5D.alt?baseUrl=YOUR_STUDIO_URL&id=462efcc6-3c8b-47c6-8474-5544e1a4acde&type=author&path=pictures%5B_key%3D%3D%22cee5fbb69da2%22%5D.alt&perspective=published
Differences from a manually built link
Links from resolveEditUrl differ from the ones you build yourself:
- The URL always sets
mode=presentation, so it opens the document in the Presentation tool rather than the default document editor. - A
resultPathis required. The underlyingcreateEditUrlthrowspath is required, so you can't build a document-only link this way. - The field path is percent-encoded, and
perspective=publishedis appended when the source document is published. - Unresolvable paths return
undefined. A value computed in the query maps to no document field, for example. Check the return value before you render a link.