sanity-singletons について
sanity-singletonsは、固定IDのドキュメントを扱うSanity Studio開発者向けのスキルです。Sanity 6.17.0以降のシングルトンAPIについて、標準・カスタムのStructure Tool構成や移行方法を説明します。
主な機能
- 01ドキュメントIDとスキーマ型を結ぶ定義の登録
- 02Structure 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 を持ち、ちょうど 1 回だけ作成できるドキュメントです。一般的な例として、サイト設定、ナビゲーション、フッターのコンテンツがあります。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で別途定義し、シングルトン固有の設定は必要ありません。- 複数のシングルトンで同じスキーマ型を共有できます(上記の 2 つのキャンペーンなど)。それぞれに独自の
title、icon、initialValue(スキーマ型の値にフォールバック)があり、共有スキーマ型は通常のドキュメントにも使用できます。 - 定義 ID はシングルトンの安定した識別子で、省略可能です。省略すると
documentIdを引き継ぎます。Structure のコードではこの ID を参照します(S.listItem().singleton('springCampaign'))。明示的に設定するのは、記述を詳しくする場合、将来的な競合を防ぐ場合、またはワークスペースや環境によって異なるドキュメント ID に割り当てられるシングルトンを共通に指定する場合に限ります。 - リゾルバー関数も使用できます:
singletons: (prev, context) => [...prev, ...]。リゾルバーに渡されるのは、常に完全に解決済みの定義です(文字列の省略形は先に展開されます)。
設定解決時の検証(すべての失敗は 1 つの 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タグを持てるテンプレートは最大 1 つです。タグは登録済みの定義を参照する必要があります。
Studio で自動的に行われること
- シングルトンごとに専用の初期値テンプレートを生成します。
ResolvedTemplate.singletonを使って定義 ID をタグ付けし、値にはdefinition.initialValue ?? schemaType.initialValueを使用します。通常の型ごとのテンプレートの代わりにこのテンプレートを使用し、さらにタグ付きテンプレートをすべての「新規作成」画面(グローバル作成メニュー、構造ペイン、参照入力)から除外します。タグ付きテンプレートはsource.templatesに残るため、シングルトンがまだ存在しない状態で開いても初期値が適用されます。 - ドキュメントスコープの設定コンテキスト(
document.actions、badges、inspectors、フィールドアクション、comments.enabled、askToEdit.enabled)にcontext.singleton(定義 ID)を注入します。公開済みのドキュメント ID を使って照合するため、シングルトンのドラフトやリリースバージョンも、ドキュメントを開いた方法(構造、インテント、ディープリンク、検索)にかかわらず一致します。 - ユーザー定義のリゾルバーがすべて実行された後、
duplicateドキュメントアクションを除外し、document.actions経由で再追加できないようにします。 - Structure Tool の暗黙的なデフォルトコンテンツリストからスキーマ型を非表示にし、そのリストにシングルトン自体を追加します。定義ごとに 1 項目を登録順にドキュメント型のリストの後に追加し、それぞれ
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)はシングルトンのスキーマ型でも動作し、通常のドキュメントと並べてシングルトンのドキュメントを意図的に一覧表示します。 - 「宙に浮いた」シングルトンに対する警告はありません。 構造は遅延的に解決されるため、カスタム構造に追加されていない登録済みシングルトンを Studio が検出することはできません。(デフォルトの構造ではすべて追加されるため、これは
structureリゾルバーを指定した Studio にのみ影響します。)先行して解決しない限り、どのようなヒューリスティックも誤検出につながります。これはS.document().documentId(...).schemaType(...)による手動の接続がサポートされているためです。カスタム構造では、レジストリの各エントリーに対応する構造エントリーを必ず追加してください。
Structure Tool でシングルトンを表示する
デフォルトのストラクチャーでは、何もする必要はありません — 登録済みのシングルトンは、トップレベルのコンテンツリストに自動で追加されます。
カスタムストラクチャー(structureTool() に structure リゾルバーを渡すスタジオ)では、明示的に追加します。すべてのヘルパーは定義 IDをキーとしており、不明な ID を指定すると SerializeError がスローされます。
次の落とし穴に注意してください。カスタムストラクチャーでデフォルトのリストを再現する際によく使われる S.documentTypeListItems() は、シングルトンのスキーマタイプを除外し、シングルトンのアイテムも追加しません。そのため、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 になります(構造上、一意です)が、タイトルはスキーマタイプのタイトルにフォールバックするため、同じスキーマタイプを共有するシングルトンには同じラベルが付けられます。各定義に
titleを指定してください。これはシングルトンが表示されるすべての場所に適用されます。アイテムごとの上書きを設定できないS.list().singletons()も対象です。 S.document().singleton()は、シングルトンのタグ付き初期値テンプレートを固定するため、そのタイプに複数のテンプレートがある場合も初期値が適用されます。.initialValueTemplate(...)で上書きできます。S.list().singletons()が返すビルダーには.items()がありません。 その後に.items()を呼び出すと配列全体が置き換えられ、シングルトンのアイテムが気づかないうちに消えてしまうため、型で禁止されています。.items([...])を.singletons([...])より先に呼び出すか、通常の.items()配列にS.listItem().singleton()のエントリーを含めてください。.singletons()を繰り返し呼び出すと追加されます。- 引数なしの
singletons()は、登録順に、プラグインで登録されたものも含め、登録済みのすべての定義を意味します。singletons([])は何も含めません。同じリスト内でS.listItem().singleton(id)を手書きで追加すると、そのシングルトンが二重に追加され、重複 ID のSerializeErrorが発生します。併用する場合は、明示的な配列を渡してください。 S.documentListItem().singleton(id)は静的なタイトルではなく、ライブのドキュメントプレビューを表示します。また、ドキュメントリストアイテムの ID はプレビュー対象のドキュメントそのものなので、定義 ID ではなく定義のdocumentIdがデフォルトで使用されます。シングルトンドキュメントがまだ存在しない場合は、通常のS.listItem()を使ってください。
共有スキーマタイプと「新規作成」の回避策
スキーマタイプ T にシングルトンを宣言すると、T の通常のタイプごとのテンプレートはシングルトンのタグ付きテンプレートに置き換わるため、作成メニューから T が表示されなくなります。通常の T ドキュメントも作成可能にするには、タグなしのテンプレートを明示的に定義してください。プレーンなテンプレートは存在しなくなるため、T 自体を含め、どの ID でも使えます。
schema: {
templates: (prev) => [
...prev,
{id: 'campaign-non-singleton', title: 'Campaign', schemaType: 'campaign', value: {}},
],
},
デフォルトのストラクチャーだけでは不十分なのはここだけです。共有タイプに対するシングルトンは表示されますが、そのタイプは引き続きフィルターされるため、ストラクチャーが完成しているように見えても、通常のドキュメントはリストに表示されません。共有スキーマタイプは、必ずストラクチャー内のどこかに明示的な S.documentTypeListItem(typeName) が必要です。(将来的には、定義に追加される allowNonSingletonDocuments フラグによって、この回避策用のテンプレートとストラクチャーエントリーの両方が不要になります。)
singleton タグがセマンティクスを担います。タグ付きのテンプレートは作成オプションにならず、タグなしのテンプレートは常に作成オプションになります。このタグは ResolvedTemplate(source.templates 内の解決済みの形)に存在し、作成時の Template 型には存在しないため、開発者が直接設定することはありません。schema.templates を使ってシングルトンの初期値をカスタマイズするには、シングルトンのテンプレートを ID(定義 ID と同じ)で検索し、スプレッド構文({...template, value})で展開します。スプレッド構文によって実行時にもタグが保持され、作成メニューに表示されなくなります。(通常は定義自体に initialValue を設定するほうが簡単です。)
プラグインからシングルトンを読み取る
- 解決済みのレジストリは
source.document.singletons(useSource().document.singletons)から利用できます。 - ドキュメントスコープのリゾルバーには
context.singleton(定義 ID)が渡されます(documentIdとschemaTypeはすでにコンテキストに含まれています)。
ユーザーランドのシングルトン設定を移行する
一般的なユーザーランドの実装では、次の 3 つを組み合わせています。それぞれはレジストリによって置き換えられます。
| ユーザーランドの手法 | 第一級の代替手段 |
|---|---|
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)に置き換えます(または、手動の設定をそのまま使用します。引き続き完全にサポートされます。context.singletonが解決されるように、documentIdとschemaTypeの組み合わせが定義と一致していることを確認してください)。カスタム構造を作成した理由がシングルトンを表示することだけであれば、リゾルバー全体を削除し、デフォルトの構造に任せます。 - スキーマタイプをシングルトン専用で使用する場合は、ほかに変更はありません。通常のドキュメントと共有する場合は、作成可能な状態を維持するために明示的なタグなしテンプレートを追加し、一覧表示を維持するために
S.documentTypeList(typeName)を追加します。
sanity-plugin-singleton-management からの移行
sanity-plugin-singleton-management(sanity-plugin-singleton-tools のメンテナンスされているフォークで、API は同一です)は、options: {singleton: true} でスキーマ型をマークし、新規ドキュメントのオプションとドキュメントアクションをフィルタリングする singletonTools() プラグインを登録し、3 つのストラクチャーヘルパーを提供します。各要素は、ファーストクラス API に直接対応しています。
| プラグインでの手法 | ファーストクラスの代替手段 |
|---|---|
スキーマ型の options: {singleton: true} | document.singletons エントリ(スキーマオプションは削除してください。ファーストクラス API では意味を持ちません) |
plugins: [...] 内の singletonTools() プラグイン | 削除(作成と重複防止は自動で行われます) |
singletonDocumentListItem({S, context, type, title, id, icon}) | S.listItem().singleton(<definition id>)(タイトル/アイコンのデフォルト値は定義から、その後スキーマ型から取得されます) |
singletonDocumentListItems({S, context}) | S.list().singletons()(または、デフォルトのストラクチャーを使用している場合は何も指定しません) |
filteredDocumentListItems({S, context}) | ...S.documentTypeListItems() — シングルトンのスキーマ型は自動的に除外されます |
ドキュメント ID の互換性(重要なポイント): プラグインでは、ドキュメント ID はデフォルトでスキーマ型名になります(S.document().schemaType(type).id(id ?? type))。そのため、プラグインのシングルトン mySingleton はドキュメント mySingleton を編集します。文字列の省略記法(singletons: ['mySingleton'])は、まさにそのドキュメント ID を生成するため、データ移行なしで既存のコンテンツを引き続き使用できます。プラグイン呼び出しで明示的な id を渡していた場合は、定義の documentId に引き継いでください。
移行手順:
-
options: {singleton: true}が設定されている各スキーマ型について、オプションを削除し、その型をdocument.singletonsに登録します。プラグインでデフォルト ID を使用していた場合は文字列の省略記法を、それ以外の場合は{documentId: <明示的な id>, schemaType: <type>}を使用します。 -
pluginsからsingletonTools()を削除し、パッケージをアンインストールします。 -
上の表に従ってストラクチャーヘルパーを置き換えます。プラグインでは
{S, context}を渡していましたが、ファーストクラスのヘルパーには不要です。ビルダー自身のコンテキストからレジストリを読み取ります。 -
意図的な動作の違いに注意してください。プラグインではアクションを許可リスト(
publish、discardChanges、restore)で制限し、削除と非公開化を非表示にします。ファーストクラス API が削除するのはduplicateのみです。シングルトンの削除は、正当な「リセット」操作です。プラグインのより厳格な動作を維持するには、次を追加します。document: { actions: (prev, context) => context.singleton ? prev.filter(({action}) => ['publish', 'discardChanges', 'restore'].includes(action as string), ) : prev, },より厳格な動作が本当に必要な場合にのみ追加してください。そうでなければ、ファーストクラス API のデフォルトを使用してください。
-
プラグインの
singletonDocumentListItemを使って、同じスキーマ型の複数のシングルトン(title/idの上書き)を表示していた場合は、ドキュメント ID ごとに 1 つの定義({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 - _共有_スキーマ型を使ったユーザーランドのシングルトンの移行例(
dev/test-studio/initialValueTemplates/index.tsにあるエスケープハッチ用テンプレートと、明示的なS.documentTypeListItem()による再追加):resolveSingletons.tsのcircular、grrm、jrr-tolkienの定義