Singletons in Sanity are finally first-class
- sanity
- cms
- typescript
Nearly every Sanity Studio I've built has a "Site settings" document. There should only ever be one of it, it should always have the same ID, and nobody should be able to create a second copy by accident. For years, Sanity had no built-in idea of that. You had to fake it.
That changed in Studio 6.17, and Sanity's November release notes put singletons front and center (still marked beta). Here's what I was doing before, what it looks like now, and a few things to watch out for.
The old workaround
When I moved this site's content into Sanity last week, the homepage and site settings got the usual three-part treatment:
- Pin the document ID in the structure, so the sidebar opens one document instead of a list.
- Filter the type out of the templates, so it never shows up in the "new document" menu.
- Filter the document actions, so editors can't duplicate or delete it.
export const singletonTypes = new Set(['homePage', 'siteSettings'])
export default defineConfig({
// ...
schema: {
types: schemaTypes,
// Singletons can't be created from the "new document" menu
templates: (templates) =>
templates.filter(
({ schemaType }) => !singletonTypes.has(schemaType)
),
},
document: {
// ...nor duplicated or deleted
actions: (actions, { schemaType }) =>
singletonTypes.has(schemaType)
? actions.filter(
({ action }) =>
action &&
['publish', 'discardChanges', 'restore'].includes(
action
)
)
: actions,
},
})S.listItem()
.title('Site settings')
.id('siteSettings')
.icon(CogIcon)
.child(
S.document()
.schemaType('siteSettings')
.documentId('siteSettings')
),It works. But it's three places to keep in step, and the only thing tying them together is a Set of type names. Forget one and you get a second settings document, or a delete button where there shouldn't be one.
The new way
Singletons now live in the Studio config, not on the schema type. The schema stays an ordinary document type. You register it once:
export default defineConfig({
// ...
schema: { types: schemaTypes },
document: {
singletons: ['homePage', 'siteSettings'],
},
})The string shorthand means the definition ID, document ID and schema type are all the same. If you need more control, defineSingleton() takes a document ID, schema type, title and initial value. That also lets several singletons share one schema type, like a spring and summer campaign.
The structure gets simpler too:
S.list()
.title('Content')
.items([
S.listItem().singleton('homePage').title('Homepage'),
S.listItem().singleton('siteSettings'),
S.divider(),
S.documentTypeListItem('post').title('Blog posts'),
S.documentTypeListItem('project').title('Projects'),
S.documentTypeListItem('role').title('Roles'),
])With that in place, Studio handles the rest:
- It keeps the singleton out of the global create menu, structure panes and reference inputs.
- It removes the duplicate action.
- It takes the title and icon from the schema type, so
.child()and the ID wiring go away. S.documentTypeListItems()skips singleton types automatically, so the oldfilter()trick to avoid listing them twice can go.- It adds a "Singleton" badge in the editor and passes
context.singletonto document actions, badges, inspectors and so on.
If you use the default structure, you don't need any structure code at all. Singletons get added to the content list in the order you registered them.
The singleton guide in the Sanity docs has the full API if you want to dig in.