Migrate plugins to support Content Releases
Guide to supporting Content Releases and perspectives in Sanity Studio plugins
The introduction of Content Releases into Sanity Studio introduces some new core concepts available through the sanity package in Sanity Studio.
Handle version document IDs
Before Content Releases, a document _id was either a published ID or a draft ID. A release adds a third form, the version document: one copy of the document per release. Your plugin can now encounter all three forms:
- Published: no prefix, as in
7f3a1c2e-9b21-4c6d-8a10-5e2f0b7d4c88. - Draft: prefixed with
drafts., as indrafts.7f3a1c2e-9b21-4c6d-8a10-5e2f0b7d4c88. - Version: prefixed with the release ID, in the form
versions.<releaseId>.<publishedId>, as inversions.rSummerDrop.7f3a1c2e-9b21-4c6d-8a10-5e2f0b7d4c88.
The sanity package re-exports helpers that read and build all three forms. Use them instead of checking prefixes yourself:
getPublishedId(id): returns the published ID, whichever of the three forms you pass it.getVersionId(id, releaseId): returns the version ID for a document in the named release. It throwsVersion can not be "published" or "drafts"if you pass either of those names as the release ID.getVersionFromId(id): returns the release ID from a version ID, andundefinedfor draft and published IDs.isVersionId(id): returnstruefor a version ID.isDraftId(id): returnstruefor a draft ID.isPublishedId(id): returnstruewhen the ID carries neither adrafts.nor aversions.prefix.
Hand-rolled prefix handling is the most common cause of breakage, because a drafts. check reports a version document as published. Replace it with the helpers:
function getIds(documentId: string) {
// Both of these are wrong for `versions.rSummerDrop.7f3a1c2e`:
// `replace` leaves the prefix in place, and the document is reported
// as published because it carries no `drafts.` prefix.
return {
publishedId: documentId.replace('drafts.', ''),
isDraft: documentId.startsWith('drafts.'),
}
}import {getPublishedId, getVersionFromId, isDraftId} from 'sanity'
function getIds(documentId: string) {
return {
publishedId: getPublishedId(documentId),
isDraft: isDraftId(documentId),
// `undefined` for draft and published IDs
releaseId: getVersionFromId(documentId),
}
}Outside the studio, in a script or your front end, use the @sanity/id-utils package, which offers the same operations with branded ID types. For the ID and path rules the Content Lake enforces, see IDs and paths.
Read the current perspective with usePerspective
To read the current perspective, use usePerspective. It returns the closest perspective context: the global Studio perspective, or the document-scoped perspective when called inside a document pane that overrides it. The hook throws usePerspective must be used within a PerspectiveProvider outside a provider. usePerspective is currently in beta; its return shape may change in a minor release. For example:
import {usePerspective} from 'sanity'
function MyComponent() {
const {perspectiveStack} = usePerspective()
// ...
}usePerspective returns:
interface PerspectiveContextValue {
/* The selected perspective name; either a release or `published` */
selectedPerspectiveName: 'published' | ReleaseId | undefined
/**
* The release id as `r<string>`; undefined if the selected
* perspective is `published` or `drafts`
*/
selectedReleaseId: ReleaseId | undefined
/* The current global release */
selectedPerspective: TargetPerspective
/**
* The stacked perspective ids, ordered chronologically, representing the
* state of documents at a point in time. Pass it as the client
* `perspective` param. e.g. ["published"] | ["drafts"] |
* ["releaseId2", "releaseId1", "drafts"]
*/
perspectiveStack: PerspectiveStack
/* The excluded perspectives */
excludedPerspectives: string[]
/* The selected bundle: `published`, `drafts`, or a release id */
bundle: PerspectiveBundle
}Further, you can use a ReleaseId to query document versions within a release, as described in Content Releases API.
Custom input component plugins
Plugins that make custom input components available through custom input types have particular concerns. Before Content Releases, a document form might have made its inputs read-only while data was loading, being re-synced, or in a transient state. Perspectives now let you view the document form of the published document version. That form is read-only in all cases except liveEdit. In those cases, your plugin must pass the readOnly prop available when rendering custom components:
import {defineField, defineType, type InputProps} from 'sanity'
function ProductCodeInput(props: InputProps) {
const {readOnly} = props
// Spread `readOnly` into your Sanity UI input, or use it to disable your own control
return props.renderDefault(props)
}
export const productCode = defineType({
name: 'productCode',
type: 'object',
fields: [
defineField({
name: 'value',
type: 'string',
components: {input: ProductCodeInput},
}),
],
})Troubleshooting
Custom input ignores read-only state
A custom input that calls onChange while the form is read-only throws Attempted to patch a read-only document. The patch never reaches the document, so the edit is discarded. Any local state your input holds still looks changed until the next render, which makes the edit appear to have worked.
What the editor sees depends on where your component calls onChange:
- From an event handler: a toast titled Uncaught error with the message as its description. React error boundaries don't catch errors in event handlers, and nothing appears in the browser console. Repeat attempts collapse into the same toast.
- From a
useEffect: the throw happens inside React, so the error boundary replaces the document pane with an error screen. - During the initial render: a different error,
Attempted to patch the Sanity document during initial render or in an `useInsertionEffect`. Input components should only call `onChange()` in a useEffect or an event handler.
The form is read-only whenever the selected perspective is published and the document type doesn't set liveEdit. Nothing warns you during development that your input is ignoring the prop, and the built-in read-only labels come from inputs that honor it, so your component shows none of them. The first signal is the toast after an editor tries to type.
Read readOnly from the props your component receives and disable the input while it's true. To test the path, switch the Studio perspective to published and try to edit a document whose type doesn't set liveEdit.