Singletons Sanity
Un « singleton » est un document avec un id fixe qui peut être créé exactement une fois. Exemples courants : paramètres de site, navigation, contenu de pied de page. Studio supporte les singletons comme primitive de première classe via le registre de configuration document.singletons plus un petit ensemble d'assistants Structure Tool.
Nécessite sanity 6.17.0 ou version ultérieure. Vérifiez la version installée dans le fichier de verrouillage avant d'utiliser cette API. Sur les anciens Studios, mettez à jour d'abord ou utilisez le motif userland (id de document fixe dans une structure personnalisée, plus filtrage manuel de newDocumentOptions et duplicate) décrit dans sanity-best-practices → studio-structure.
Quand utiliser cette skill
- Un développeur demande comment créer un « document de paramètres » ou « singleton ».
- Une configuration de studio a besoin d'une entrée du registre
document.singletons. - Structure Tool doit afficher un singleton comme élément de liste ou volet de document.
- Vous migrez une implémentation userland de singleton existante vers l'API de première classe.
- Vous migrez à partir d'un plugin singleton tiers tel que
sanity-plugin-singleton-management(ou son prédécesseursanity-plugin-singleton-tools).
Enregistrer un singleton
Les singletons sont enregistrés en configuration, pas sur le type de schéma. Une définition de singleton lie un id de définition à un id de document et un type de schéma :
import {defineConfig, defineSingleton} from 'sanity'
export default defineConfig({
schema: {
types: [/* siteSettings and campaign are ordinary document types */],
},
document: {
singletons: [
// String shorthand — expands to
// {id: 'siteSettings', documentId: 'siteSettings', schemaType: 'siteSettings'}
'siteSettings',
// Definition object. `defineSingleton` is an optional typed identity
// helper; a plain object literal works too. `id` is omitted here, so it
// inherits each `documentId`.
defineSingleton({
documentId: 'springCampaign',
schemaType: 'campaign',
title: 'Spring campaign', // optional display metadata
initialValue: {season: 'spring'}, // optional per-singleton initial value
}),
defineSingleton({
documentId: 'summerCampaign',
schemaType: 'campaign',
title: 'Summer campaign',
initialValue: {season: 'summer'},
}),
],
},
})
Points clés :
defineSingletonproduit une définition de singleton, pas un schéma de document. Le type de schéma qu'il référence est défini séparément avecdefineTypeet n'a besoin d'aucune configuration spécifique aux singletons.- Plusieurs singletons peuvent partager un type de schéma (comme les deux campagnes ci-dessus), chacun avec son propre
title,icon, etinitialValue(avec recours au type de schéma), et un type de schéma partagé peut aussi soutenir des documents ordinaires. - L'id de définition est l'identité stable du singleton, et il est optionnel — il hérite de
documentIdquand omis. Le code Structure le référence (S.listItem().singleton('springCampaign')). Réglez-le explicitement seulement pour être verbeux, pour vous protéger contre les collisions futures, ou pour adresser un singleton universellement quand différents workspaces ou environnements le mappent à des ids de document différents. - Une fonction de résolveur est aussi acceptée :
singletons: (prev, context) => [...prev, ...]. Les résolveurs ne reçoivent que des définitions entièrement résolues (les raccourcis chaîne sont d'abord développés).
Validation au moment de la résolution de config (tous les échecs s'agrègent en un seul ConfigResolutionError) :
id: non-vide, unique entre les définitions (les ids hérités comptent — un id explicite ne peut pas entrer en collision avec celui d'une autre définition). Utilisé verbatim comme id de nœud Structure Tool, donc limité à[a-zA-Z0-9._-]et ne peut pas commencer par__edit__. Il ne peut pas non plus correspondre au nom d'un type de document qu'aucun singleton ne revendique, car la liste de contenu par défaut indexe à la fois par id (le raccourci chaîne ne déclenche jamais cela — le type qu'il nomme est revendiqué par la définition elle-même).documentId: non-vide, unique, format d'id publié (pas de préfixedrafts./versions., seulement[a-zA-Z0-9._-]).schemaType: doit nommer un type de document existant. N'a pas besoin d'être unique.- Templates : un template non étiqueté ne doit pas réutiliser un id de définition (le template singleton généré le revendique) ; au maximum un template peut porter une balise
singletondonnée ; les balises doivent référencer des définitions enregistrées.
Ce que Studio fait automatiquement
- Génère un template de valeur initiale dédiée par singleton — étiqueté avec l'id de définition via
ResolvedTemplate.singleton, valeur dedefinition.initialValue ?? schemaType.initialValue— au lieu du template simple par type, et supprime les templates étiquetés de toutes les surfaces « créer nouveau » (menu de création global, volets de structure, entrées de référence). Le template étiqueté reste danssource.templates, donc ouvrir le singleton avant qu'il n'existe applique toujours sa valeur initiale. - Injecte
context.singleton(l'id de définition) dans les contextes de configuration document-scoped —document.actions,badges,inspectors, field actions,comments.enabled,askToEdit.enabled— recherchés par id de document publié, donc les brouillons et les versions de release du singleton correspondent, peu importe comment le document a été ouvert (structure, intent, deep link, search). - Filtre l'action de document
duplicate, après que tous les résolveurs utilisateur s'exécutent, donc elle ne peut pas être réintroduite viadocument.actions. - Cache le type de schéma de la liste de contenu implicite par défaut dans Structure Tool, et ajoute le singleton lui-même à cette liste — un élément par définition, après les listes de types de document, dans l'ordre d'enregistrement, chacun équivalent à
S.listItem().singleton(id). Un studio qui n'a pas configuré Structure Tool n'a besoin d'aucun code de structure du tout. Ceci s'applique seulement à la structure par défaut ; un résolveurstructurela remplace en bloc.
Ce que Studio ne fait délibérément pas :
- Delete, unpublish, et discard restent disponibles. Supprimer un singleton est une « réinitialisation » légitime : la structure pointe toujours sur l'id fixe, et l'édition recréé le document. Pour cacher delete aussi :
document: {actions: (prev, context) => (context.singleton ? prev.filter((action) => action.action !== 'delete') : prev)}. - La structure explicite n'est jamais filtrée.
S.documentTypeList(typeName)fonctionne pour les types de schéma singleton et liste intentionnellement les documents singleton aux côtés des ordinaires. - Pas d'avertissement pour les singletons « en suspension ». Structure se résout paresseusement, donc Studio ne peut pas détecter un singleton enregistré qu'une structure personnalisée n'ajoute jamais. (La structure par défaut les ajoute tous, donc ceci affecte seulement les studios avec un résolveur
structure.) Toute heuristique à court d'une résolution impatiente serait inexacte, car le câblage manuelS.document().documentId(...).schemaType(...)est supporté. Dans une structure personnalisée, associez toujours une entrée de registre à une entrée de structure.
Afficher les singletons dans Structure Tool
Avec la structure par défaut il n'y a rien à faire — les singletons enregistrés sont ajoutés automatiquement à la liste de contenu de haut niveau.
Dans une structure personnalisée (n'importe quel studio passant un résolveur structure à structureTool()), ajoutez-les explicitement. Tous les assistants sont indexés par id de définition et jettent une SerializeError pour les ids inconnus.
Attention à ce piège : S.documentTypeListItems(), la façon habituelle de reproduire les listes par défaut dans une structure personnalisée, exclut les types de schéma singleton et n'ajoute pas les éléments singleton. S.list().items([...S.documentTypeListItems()]) laisse donc tomber tous les singletons. Utilisez .singletons() à côté :
S.list().id('__root__').title('Content').items(S.documentTypeListItems()).singletons()
// 1. Highest level: a list of singletons — a given set, or all of them.
S.list().id('singletons').title('Singletons').singletons(['siteSettings', 'springCampaign'])
S.list().id('singletons').title('Singletons').singletons()
// 2. A single list item (title/icon default from the definition, then the schema type).
S.listItem().singleton('siteSettings')
// 3. Low level: just the document pane.
S.listItem().title('Settings').id('settings').child(S.document().singleton('siteSettings'))
Règles de composition :
- Chaque défaut est remplaçable par chaînage normal :
S.listItem().singleton('siteSettings').title('Global settings'),S.document().singleton('siteSettings').documentId('override'). - Les titres ne sont pas désambiguïsés pour vous. L'id de l'élément de liste reprend par défaut l'id de définition (unique par construction), mais le titre reprend celui du type de schéma, donc les singletons partageant un type de schéma partagent un label. Donnez à chaque définition un
title— il s'applique partout où le singleton est rendu, y comprisS.list().singletons(), qui n'a pas de surcharges par élément. S.document().singleton()épingle le template de valeur initiale étiqueté du singleton pour que sa valeur initiale s'applique même quand le type a plusieurs templates ; surcharge avec.initialValueTemplate(...).S.list().singletons()retourne un builder sans.items(). Appeler.items()après remplacerait tout le tableau et silencieusement déposeraient les éléments singleton, donc le type l'interdit. Appelez.items([...])avant.singletons([...]), ou mettez les entréesS.listItem().singleton()dans un tableau.items()normal. Les appels répétés.singletons()ajoutent.singletons()sans argument signifie chaque définition enregistrée (ordre d'enregistrement, y compris celles enregistrées par plugin) ;singletons([])signifie aucune. Le combiner avec unS.listItem().singleton(id)écrit à la main dans la même liste ajoute ce singleton deux fois et échoue avec uneSerializeErrord'id en doublon — passez un tableau explicite quand mélanger.S.documentListItem().singleton(id)affiche un aperçu de document en direct au lieu d'un titre statique, et reprend par défaut son id pardocumentIdde la définition (pas l'id de définition), car l'id d'un élément de liste de documents est le document qu'il prévisualise. Préférez leS.listItem()simple quand le document singleton peut ne pas exister encore.
Types de schéma partagés et l'« échappatoire créer nouveau »
Déclarer un singleton sur le type de schéma T remplace le template simple par type de T par le template étiqueté du singleton, donc T disparaît des menus de création. Si les documents T ordinaires devraient rester créables, définissez un template explicite non étiqueté — n'importe quel id fonctionne, y compris T lui-même, puisque le template simple n'existe plus :
schema: {
templates: (prev) => [
...prev,
{id: 'campaign-non-singleton', title: 'Campaign', schemaType: 'campaign', value: {}},
],
},
C'est le seul endroit où la structure par défaut n'est pas suffisante en elle-même : elle affiche les singletons sur le type partagé, mais le type reste filtré, donc les documents ordinaires de celui-ci restent non listés alors que la structure semble complète. Un type de schéma partagé a toujours besoin d'un S.documentTypeListItem(typeName) explicite quelque part dans la structure. (Un futur drapeau allowNonSingletonDocuments sur la définition remplacera à la fois le template d'échappatoire et cette entrée de structure.)
La balise singleton porte la sémantique : les templates avec elle ne sont jamais des options de création ; les templates sans elle le sont toujours. La balise vit sur ResolvedTemplate — la forme résolue dans source.templates — pas sur le type d'authoring Template, donc les développeurs ne la réglez jamais directement. Pour personnaliser la valeur initiale d'un singleton via schema.templates, trouvez le template du singleton par id (égal à l'id de définition) et déployez-le ({...template, value}) — le déploiement préserve la balise au runtime, la gardant hors des menus de création. (Réglez initialValue sur la définition elle-même est habituellement plus simple.)
Lire les singletons depuis les plugins
- Le registre résolu est disponible à
source.document.singletons(useSource().document.singletons). - Les résolveurs scoped au document reçoivent
context.singleton(l'id de définition ;documentIdetschemaTypesont déjà sur le contexte).
Migrer une configuration userland de singleton
Les implémentations userland typiques combinent trois pièces ; chacune est subsumée par le registre :
| Technique userland | Remplacement de première classe |
|---|---|
Filtrage de document.newDocumentOptions du type |
Automatique (les templates singleton étiquetés ne sont jamais des options de création) |
Filtrage de duplicate dans document.actions |
Automatique (terminal, non contournable) |
Structure personnalisée avec S.document().schemaType(x).documentId(y) |
Garder, ou simplifier à S.listItem().singleton(id) |
Étapes de migration :
- Ajoutez le singleton à
document.singletons(raccourci chaîne quand id, id de document, et nom de type sont tous identiques). - Supprimez le filtrage manuel de
newDocumentOptionsetduplicatepour ce type. - Remplacez le câblage de structure manuel par
S.listItem().singleton(id)(ou gardez le câblage manuel — il reste pleinement supporté ; assurez-vous que la pairedocumentId/schemaTypecorrespond à la définition pour quecontext.singletonse résolve). Si la seule raison de l'existence d'une structure personnalisée était d'afficher les singletons, supprimez entièrement le résolveur et laissez la structure par défaut faire. - Si le type de schéma est seulement utilisé par le singleton, rien d'autre ne change. S'il est partagé avec des documents ordinaires, ajoutez un template explicite non étiqueté pour les garder créables, et
S.documentTypeList(typeName)pour les garder listés.
Migrer depuis sanity-plugin-singleton-management
sanity-plugin-singleton-management (un fork maintenu de sanity-plugin-singleton-tools, avec une API identique) marque les types de schéma avec options: {singleton: true}, enregistre un plugin singletonTools() qui filtre les options de nouveau document et les actions de document, et fournit trois assistants de structure. Chaque pièce s'applique directement à l'API de première classe :
| Technique du plugin | Remplacement de première classe |
|---|---|
options: {singleton: true} sur le type de schéma |
Une entrée document.singletons (supprimez l'option de schéma — elle n'a pas de sens de première classe) |
Plugin singletonTools() dans plugins: [...] |
Supprimez (la prévention de création et de duplication est automatique) |
singletonDocumentListItem({S, context, type, title, id, icon}) |
S.listItem().singleton(<definition id>) (les valeurs par défaut title/icon viennent de la définition, puis du type de schéma) |
singletonDocumentListItems({S, context}) |
S.list().singletons() (ou rien du tout, si vous utilisez la structure par défaut) |
filteredDocumentListItems({S, context}) |
...S.documentTypeListItems() — les types de schéma singleton sont exclus automatiquement |
Compatibilité des ids de document (le détail critique) : l'id de document du plugin reprend par défaut le nom du type de schéma (S.document().schemaType(type).id(id ?? type)), donc un singleton de plugin mySingleton édite le document mySingleton. Le raccourci chaîne (singletons: ['mySingleton']) produit exactement cet id de document — le contenu existant reste fonctionnel sans migration de données. Si l'appel du plugin a passé un id explicite, reportez-le comme documentId de la définition.
Étapes de migration :
-
Pour chaque type de schéma avec
options: {singleton: true}: supprimez l'option, et enregistrez le type dansdocument.singletons— le raccourci chaîne quand le plugin utilisait l'id par défaut, sinon{documentId: <the explicit id>, schemaType: <type>}. -
Supprimez
singletonTools()depluginset désinstallez le package. -
Remplacez les assistants de structure selon le tableau ci-dessus. Le plugin passait
{S, context}autour ; les assistants de première classe n'en ont besoin d'aucun — ils lisent le registre du propre contexte du builder. -
Notez une différence de comportement délibérée : le plugin liste blanche les actions (
publish,discardChanges,restore), cachant delete et unpublish. L'API de première classe supprime seulementduplicate— supprimer un singleton est une « réinitialisation » légitime. Pour préserver le comportement plus strict du plugin, ajoutez :document: { actions: (prev, context) => context.singleton ? prev.filter(({action}) => ['publish', 'discardChanges', 'restore'].includes(action as string), ) : prev, },Ajoutez ceci seulement si le comportement plus strict est genuinely voulu — sinon préférez la valeur par défaut de première classe.
-
Si le
singletonDocumentListItemdu plugin était utilisé pour afficher plusieurs singletons du même type de schéma (ses surcharges detitle/id), enregistrez une définition par id de document —{documentId, schemaType, title}— ce qui est le modèle de première classe pour exactement ce cas.
Référence
La source vit dans le monorepo sanity-io/sanity :
- Types de config & validation :
core/config/types.ts,core/config/prepareConfig.tsx - Assistants de structure :
Document.ts,ListItem.ts,List.ts - Exemple fonctionnel :
dev/test-studio/schema/singletons.ts,dev/test-studio/structure/resolveSingletons.ts,dev/test-studio/structure/resolveStructure.ts - Migration travaillée de singletons userland sur types de schéma partagés (templates d'échappatoire dans
dev/test-studio/initialValueTemplates/index.ts, opt-back-ins explicitesS.documentTypeListItem()) : les définitionscircular,grrm, etjrr-tolkiendansresolveSingletons.ts