mcpField
S
sanity-singletons

sanity-singletons

github.com/sanity-io开发194162 次安装1 天前更新
代码仓库

关于 sanity-singletons

sanity-singletons 是面向 Sanity Studio 开发者的技能,介绍 6.17.0 及更高版本中用于注册和呈现固定 ID 文档的单例 API。内容涵盖默认结构、自定义 Structure Tool 配置及迁移方式。

主要功能

  1. 01注册单例定义并绑定文档 ID 与模式类型
  2. 02使用 Structure Tool 辅助方法呈现单例
  3. 03管理单例初始值模板与创建选项
  4. 04迁移用户自定义或第三方单例实现

适用场景

  1. 01为站点设置、导航或页脚内容配置单例
  2. 02在自定义 Studio 结构中加入单例条目
  3. 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 会自动执行的操作

  1. 为每个单例生成专用的初始值模板——通过 ResolvedTemplate.singleton 标记定义 ID,值取自 definition.initialValue ?? schemaType.initialValue;它不是普通的按类型生成的模板。此外,从所有“新建”入口中移除带标记的模板(全局创建菜单、结构窗格、引用输入框)。带标记的模板仍保留在 source.templates 中,因此在单例尚不存在时打开它,仍会应用其初始值。
  2. 将 context.singleton(定义 ID)注入文档级配置上下文——包括 document.actions、badges、inspectors、字段操作、comments.enabled、askToEdit.enabled;按 已发布 的文档 ID 查找,因此无论通过何种方式打开文档(结构、意图、深层链接、搜索),单例的草稿和发布版本都能匹配。
  3. 过滤 duplicate 文档操作,在所有用户解析器运行后执行,确保它无法通过 document.actions 重新添加。
  4. 在 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)

迁移步骤:

  1. 将单例添加到 document.singletons(当 ID、文档 ID 和类型名称完全相同时,可使用字符串简写)。
  2. 删除针对该类型手动筛除 newDocumentOptions 和 duplicate 的逻辑。
  3. 将手动配置结构的代码替换为 S.listItem().singleton(id)(或保留手动配置——仍完全受支持;确保 documentId/schemaType 对与定义一致,以便解析 context.singleton)。如果创建自定义结构的唯一原因是展示单例,请删除整个解析器,让默认结构负责展示。
  4. 如果该 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:

迁移步骤:

  1. 对于每个带有 options: {singleton: true} 的 schema 类型:移除该选项,并在 document.singletons 中注册该类型——如果插件使用的是默认 ID,则使用字符串简写;否则使用 {documentId: <the explicit id>, schemaType: <type>}。

  2. 从 plugins 中移除 singletonTools(),并卸载该软件包。

  3. 按照上表替换结构辅助函数。插件会传递 {S, context};一等辅助函数不需要这些参数——它们会从 builder 自身的上下文中读取注册表。

  4. 请注意一个有意为之的行为差异:该插件会通过允许列表限制操作(publish、discardChanges、restore),并隐藏删除和取消发布操作。一等 API 只会移除 duplicate——删除 singleton 是一种合理的“重置”方式。若要保留插件更严格的行为,请添加:

    document: {
      actions: (prev, context) =>
        context.singleton
          ? prev.filter(({action}) =>
              ['publish', 'discardChanges', 'restore'].includes(action as string),
            )
          : prev,
    },
    

    只有确实需要更严格的行为时才添加此项——否则建议使用一等默认行为。

  5. 如果插件的 singletonDocumentListItem 曾用于渲染同一 schema 类型的多个 singleton(通过 title/id 覆盖默认值),请为每个文档 ID 注册一个定义——{documentId, schemaType, title}——这正是一等模型专门支持的用例。

参考

源代码位于 sanity-io/sanity 单体仓库中:

评论

支持 Markdown