关于 sanity-singletons
sanity-singletons 是面向 Sanity Studio 开发者的技能,介绍 6.17.0 及更高版本中用于注册和呈现固定 ID 文档的单例 API。内容涵盖默认结构、自定义 Structure Tool 配置及迁移方式。
主要功能
- 01注册单例定义并绑定文档 ID 与模式类型
- 02使用 Structure Tool 辅助方法呈现单例
- 03管理单例初始值模板与创建选项
- 04迁移用户自定义或第三方单例实现
适用场景
- 01为站点设置、导航或页脚内容配置单例
- 02在自定义 Studio 结构中加入单例条目
- 03将旧单例方案迁移到内置 API
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sanity-io/ agent-toolkit --skill sanity-singletons安装后在 Claude Code 中输入/sanity-singletons确认已加载。
详细介绍
Sanity 单例
“单例”是 ID 固定、只能创建一次的文档。常见示例包括:站点设置、导航和页脚内容。Studio 通过 document.singletons 配置注册表和一组 Structure Tool 辅助函数,将单例作为一等基本功能提供支持。
需要 sanity 6.17.0 或更高版本。 使用此 API 前,请检查锁定文件中的已安装版本。对于较旧的 Studio,请先升级,或使用 sanity-best-practices → studio-structure 中描述的用户自定义模式(在自定义结构中固定 documentId,并手动过滤 newDocumentOptions 和 duplicate)。
何时使用此技能
- 开发者询问如何创建“设置文档”或“单例”。
- Studio 配置需要添加
document.singletons注册表条目。 - Structure Tool 需要将单例呈现为列表项或文档窗格。
- 你正在将现有的用户自定义单例实现迁移到一等 API。
- 你正在从第三方单例插件(例如
sanity-plugin-singleton-management或其前身sanity-plugin-singleton-tools)迁移。
注册单例
单例在配置中注册,而非在架构类型上注册。单例定义将一个_定义 ID_绑定到一个_文档 ID_和一个_架构类型_:
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'},
}),
],
},
})
要点:
defineSingleton生成的是单例_定义_,而非文档架构。 它引用的架构类型通过defineType单独定义,无需任何单例专用配置。- 多个单例可以共用一个架构类型(如上面的两个营销活动),每个单例都可以拥有自己的
title、icon和initialValue(缺省时使用架构类型的值);同一个架构类型也可以用于普通文档。 - 定义 ID 是单例的稳定标识,并且是可选的——省略时会继承
documentId。Structure 代码会引用它(S.listItem().singleton('springCampaign'))。仅在需要明确表达、避免未来发生冲突,或需要在不同工作区或环境将单例映射到不同文档 ID 时,以统一方式引用单例的情况下,才显式设置此 ID。 - 也接受解析器函数:
singletons: (prev, context) => [...prev, ...]。解析器接收的始终是完全解析后的定义(字符串简写会先展开)。
配置解析时的验证(所有失败会合并为一个 ConfigResolutionError):
id:必须非空,且在所有定义中唯一(继承的 ID 也计入——显式 ID 不得与其他定义继承的 ID 冲突)。该值会原样用作 Structure Tool 节点 ID,因此仅限[a-zA-Z0-9._-],且不得以__edit__开头。此外,它不得与任何没有被单例认领的文档类型名称相同,因为默认内容列表会同时使用 ID 作为键(字符串简写不会触发此限制——定义本身会认领其所命名的类型)。documentId:必须非空、唯一,并符合已发布 ID 的格式(不得带有drafts./versions.前缀,且仅限[a-zA-Z0-9._-])。schemaType:必须是现有的文档类型。无需唯一。- 模板:未标记的模板不得重用定义 ID(该 ID 已被生成的单例模板认领);每个
singleton标签最多只能由一个模板使用;标签必须引用已注册的定义。
Studio 会自动执行的操作
- 为每个单例生成专用的初始值模板——通过
ResolvedTemplate.singleton标记定义 ID,值取自definition.initialValue ?? schemaType.initialValue;它不是普通的按类型生成的模板。此外,从所有“新建”入口中移除带标记的模板(全局创建菜单、结构窗格、引用输入框)。带标记的模板仍保留在source.templates中,因此在单例尚不存在时打开它,仍会应用其初始值。 - 将
context.singleton(定义 ID)注入文档级配置上下文——包括document.actions、badges、inspectors、字段操作、comments.enabled、askToEdit.enabled;按 已发布 的文档 ID 查找,因此无论通过何种方式打开文档(结构、意图、深层链接、搜索),单例的草稿和发布版本都能匹配。 - 过滤
duplicate文档操作,在所有用户解析器运行后执行,确保它无法通过document.actions重新添加。 - 在 Structure Tool 的隐式默认内容列表中隐藏该 schema 类型,并将单例本身追加到该列表中——每个定义对应一个条目,位于文档类型列表之后,并按注册顺序排列;每个条目都等同于
S.listItem().singleton(id)。未配置 Structure Tool 的 Studio 完全不需要编写结构代码。此功能仅适用于默认结构;structure解析器会整体替换默认结构。
Studio 特意不会执行的操作:
- 删除、取消发布和丢弃仍然可用。 删除单例是一种合理的“重置”方式:结构仍指向固定 ID,编辑时会重新创建文档。若要同时隐藏删除操作:
document: {actions: (prev, context) => (context.singleton ? prev.filter((action) => action.action !== 'delete') : prev)}。 - 永远不会过滤显式结构。
S.documentTypeList(typeName)适用于单例 schema 类型,并且会有意将单例文档与普通文档一起列出。 - 不会对“悬空”单例发出警告。 结构是惰性解析的,因此 Studio 无法检测已注册但从未被_自定义_结构添加的单例。(默认结构会添加所有单例,所以这只影响配置了
structure解析器的 Studio。)除非进行提前解析,否则任何启发式检测都会误判,因为系统支持手动配置S.document().documentId(...).schemaType(...)。在自定义结构中,始终要为每个注册表条目配套添加一个结构条目。
在 Structure Tool 中呈现单例
使用默认结构时,无需进行任何操作 —— 已注册的单例会自动追加到顶层内容列表。
在自定义结构中(任何向 structureTool() 传入 structure resolver 的 studio),都要显式添加它们。所有辅助方法都以定义 ID 为键,遇到未知 ID 时会抛出 SerializeError。
请注意这个陷阱:S.documentTypeListItems() 是在自定义结构中复现默认列表的常用方法,但它会排除单例 schema 类型,也不会添加单例项。因此,S.list().items([...S.documentTypeListItems()]) 会丢弃所有单例。请同时使用 .singletons():
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'))
组合规则:
- 所有默认值都可以通过常规链式调用覆盖:
S.listItem().singleton('siteSettings').title('Global settings')、S.document().singleton('siteSettings').documentId('override')。 - 标题不会自动消除歧义。 列表项 ID 默认为定义 ID(按设计具有唯一性),但标题会回退为 schema 类型的标题,因此共享同一 schema 类型的单例会使用相同标签。请为每个定义指定
title—— 无论单例在哪里呈现,都会应用此标题,包括没有逐项覆盖选项的S.list().singletons()。 S.document().singleton()会固定单例标记的初始值模板,因此即使该类型有多个模板,也会应用其初始值;可使用.initialValueTemplate(...)覆盖。S.list().singletons()返回的构建器没有.items()。 之后调用.items()会替换整个数组并静默丢弃单例项,因此类型定义禁止这样做。请在.singletons([...])之前调用.items([...]),或者将S.listItem().singleton()项放入常规.items()数组中。重复调用.singletons()会追加内容。- 不带参数的
singletons()表示所有已注册定义(按注册顺序,包括由插件注册的定义);singletons([])表示不包含任何定义。在同一个列表中将它与手写的S.listItem().singleton(id)组合,会导致同一单例被添加两次,并因重复 ID 而抛出SerializeError—— 混用时请传入显式数组。 S.documentListItem().singleton(id)会呈现实时文档预览,而不是静态标题;其 ID 默认为定义的documentId(而不是定义 ID),因为文档列表项的 ID 就是它预览的文档 ID。如果单例文档可能尚不存在,请优先使用普通的S.listItem()。
共享 schema 类型与“创建新文档”后备方案
在 schema 类型 T 上声明单例,会用单例的标记模板替换 T 的普通类型模板,因此 T 会从创建菜单中消失。如果普通的 T 文档仍应可创建,请定义一个显式的未标记模板 —— ID 可以是任意值,包括 T 自身,因为普通模板已不存在:
schema: {
templates: (prev) => [
...prev,
{id: 'campaign-non-singleton', title: 'Campaign', schemaType: 'campaign', value: {}},
],
},
这是默认结构无法独立完成的唯一情况:它会呈现共享类型上的单例,但该类型仍会被过滤,因此普通文档不会列出,尽管结构看起来完整。共享 schema 类型始终需要在结构中的某处显式添加 S.documentTypeListItem(typeName)。(未来,定义上的 allowNonSingletonDocuments 标记将同时取代后备模板和此结构项。)
singleton 标记承载了这些语义:带有此标记的模板永远不会作为创建选项;不带此标记的模板始终会作为创建选项。该标记位于 ResolvedTemplate(即 source.templates 中的解析后形态)上,而不是用于编写模板的 Template 类型上,因此开发者不会直接设置它。若要通过 schema.templates 自定义单例的初始值,请按 ID 查找单例模板(其 ID 与定义 ID 相同),然后展开它({...template, value})—— 展开操作会在运行时保留该标记,使模板不会出现在创建菜单中。(通常直接在定义上设置 initialValue 更简单。)
从插件读取单例
- 解析后的注册表可通过
source.document.singletons(useSource().document.singletons)获取。 - 文档级解析器会收到
context.singleton(定义 ID;documentId和schemaType已包含在上下文中)。
迁移用户自定义的单例配置
典型的用户自定义实现由三个部分组成;注册表可分别取代它们:
| 用户自定义方式 | 一等替代方案 |
|---|---|
使用 document.newDocumentOptions 筛除该类型 | 自动处理(带标签的单例模板不会成为创建选项) |
使用 document.actions 筛除 duplicate | 自动处理(终止性操作,不可绕过) |
使用自定义结构 S.document().schemaType(x).documentId(y) | 保留,或简化为 S.listItem().singleton(id) |
迁移步骤:
- 将单例添加到
document.singletons(当 ID、文档 ID 和类型名称完全相同时,可使用字符串简写)。 - 删除针对该类型手动筛除
newDocumentOptions和duplicate的逻辑。 - 将手动配置结构的代码替换为
S.listItem().singleton(id)(或保留手动配置——仍完全受支持;确保documentId/schemaType对与定义一致,以便解析context.singleton)。如果创建自定义结构的唯一原因是展示单例,请删除整个解析器,让默认结构负责展示。 - 如果该 schema 类型_仅_供单例使用,则无需进行其他更改。如果它与普通文档共用,请添加一个显式的无标签模板,以便仍可创建普通文档,并添加
S.documentTypeList(typeName),以便仍能列出它们。
从 sanity-plugin-singleton-management 迁移
sanity-plugin-singleton-management(sanity-plugin-singleton-tools 的一个持续维护的分支,API 完全相同)会使用 options: {singleton: true} 标记 schema 类型,注册一个 singletonTools() 插件来筛选新建文档选项和文档操作,并提供三个结构辅助函数。所有功能都可直接对应到一等 API:
| 插件实现方式 | 一等 API 替代方案 |
|---|---|
在 schema 类型上设置 options: {singleton: true} | 添加 document.singletons 条目(删除该 schema 选项——它没有一等 API 含义) |
在 plugins: [...] 中使用 singletonTools() 插件 | 删除(创建和防止重复的功能会自动处理) |
singletonDocumentListItem({S, context, type, title, id, icon}) | S.listItem().singleton(<definition id>)(标题/图标默认取自定义,然后取自 schema 类型) |
singletonDocumentListItems({S, context}) | S.list().singletons()(如果使用默认结构,也可以完全不添加) |
filteredDocumentListItems({S, context}) | ...S.documentTypeListItems()——singleton schema 类型会自动排除 |
**文档 ID 兼容性(关键细节):**该插件的文档 ID 默认为 schema 类型名称(S.document().schemaType(type).id(id ?? type)),因此插件中的 singleton mySingleton 会编辑文档 mySingleton。字符串简写形式(singletons: ['mySingleton'])会生成完全相同的文档 ID——现有内容无需迁移数据即可继续使用。如果插件调用传入了显式的 id,请将其转为定义中的 documentId:
迁移步骤:
-
对于每个带有
options: {singleton: true}的 schema 类型:移除该选项,并在document.singletons中注册该类型——如果插件使用的是默认 ID,则使用字符串简写;否则使用{documentId: <the explicit id>, schemaType: <type>}。 -
从
plugins中移除singletonTools(),并卸载该软件包。 -
按照上表替换结构辅助函数。插件会传递
{S, context};一等辅助函数不需要这些参数——它们会从 builder 自身的上下文中读取注册表。 -
请注意一个有意为之的行为差异:该插件会通过允许列表限制操作(
publish、discardChanges、restore),并隐藏删除和取消发布操作。一等 API 只会移除duplicate——删除 singleton 是一种合理的“重置”方式。若要保留插件更严格的行为,请添加:document: { actions: (prev, context) => context.singleton ? prev.filter(({action}) => ['publish', 'discardChanges', 'restore'].includes(action as string), ) : prev, },只有确实需要更严格的行为时才添加此项——否则建议使用一等默认行为。
-
如果插件的
singletonDocumentListItem曾用于渲染同一 schema 类型的多个 singleton(通过title/id覆盖默认值),请为每个文档 ID 注册一个定义——{documentId, schemaType, title}——这正是一等模型专门支持的用例。
参考
源代码位于 sanity-io/sanity 单体仓库中:
- 配置类型与验证:
core/config/types.ts、core/config/prepareConfig.tsx - 结构辅助工具:
Document.ts、ListItem.ts、List.ts - 示例:
dev/test-studio/schema/singletons.ts、dev/test-studio/structure/resolveSingletons.ts、dev/test-studio/structure/resolveStructure.ts - 使用 共享 schema 类型迁移用户自定义单例的完整示例(
dev/test-studio/initialValueTemplates/index.ts中的逃生舱模板,以及显式重新启用的S.documentTypeListItem()):resolveSingletons.ts中的circular、grrm和jrr-tolkien定义