Sanity Patterns は何をしますか?
クイックガイド: Sanity を構造化コンテンツ管理に使用する場合、GROQ クエリ、
defineType/defineFieldによる型付きスキーマ、@sanity/clientによるデータ取得を利用します。常にapiVersionを日付文字列に設定し、パブリック読み取りにはuseCdn: trueを使用し、下書きドキュメントを明示的に処理し、画像変換には@sanity/image-urlを、リッチテキストレンダリングには@portabletext/reactを使用します。TypeScript の型はsanity typegen generateで生成します。
<critical_requirements>
クリティカル: このスキルを使用する前に
すべてのコードは CLAUDE.md のプロジェクト規約に従う必要があります(ケバブケース、名前付きエクスポート、インポート順序、
import type、名前付き定数)
(createClient には必ず apiVersion を '2025-02-19' のような日付文字列で設定してください。省略するとレガシー API が使用され、壊れる可能性があります)
(パブリック読み取りクエリには useCdn: true を、トークンを使用する場合や新鮮なデータが必要な場合は useCdn: false を必ず使用してください)
(動的な値には必ずパラメーター化された GROQ クエリ ($param) を使用してください。ユーザー入力を GROQ 文字列に直接挿入しないでください)
(下書きは明示的に処理する必要があります。下書きドキュメントの _id には drafts. プレフィックスが付き、perspective: 'published' ではデフォルトで返されません)
(TypeGen の型生成のため、groq から defineQuery() を使用し、クエリを名前付き変数に割り当ててください)
</critical_requirements>
自動検出: Sanity, sanity, @sanity/client, createClient, GROQ, groq, defineType, defineField, defineArrayMember, @sanity/image-url, urlFor, @portabletext/react, PortableText, portable text, block content, sanity.config, sanity.cli, typegen, sanity studio, content lake
使用するタイミング:
@sanity/clientをcreateClientでセットアップしてデータを取得する場合- GROQ クエリ(フィルター、プロジェクション、結合、並べ替え、スライス)を作成する場合
defineType、defineField、defineArrayMemberでコンテンツスキーマを定義する場合@portabletext/reactで Portable Text(ブロックコンテンツ)をレンダリングする場合@sanity/image-urlで画像 URL を生成する場合(レスポンシブ画像、クロップ、ホットスポット)- ミューテーション(作成、部分更新、削除、トランザクション)を実行する場合
client.listen()でリアルタイムリスナーをセットアップする場合- Sanity TypeGen で TypeScript の型を生成する場合
カバーする主要パターン:
createClientとapiVersion設定によるクライアントセットアップ- GROQ クエリ言語: フィルター、プロジェクション、並べ替え、スライス、結合、参照
- スキーマ定義: ドキュメントタイプ、オブジェクトタイプ、配列、参照、画像
- カスタムコンポーネントによる Portable Text レンダリング
- レスポンシブ画像と変換を備えた画像 URL ビルダー
- ミューテーション: 作成、作成または置換、部分更新、削除、トランザクション
client.listen()によるリアルタイムリスナーdefineQuery()を使用した型安全な GROQ クエリのための TypeGen
使用すべきでないタイミング:
- GraphQL のみの API(Sanity は主に GROQ をサポートします。必要に応じて GraphQL スキルを使用してください)
- データベースへの直接アクセス(Sanity はホスト型コンテンツレイクであり、データベースではありません)
- Sanity 以外の CMS プラットフォーム(使用する CMS に専用のスキルを使用してください)
詳細リソース:
- 意思決定フレームワークとクイックリファレンステーブルについては、reference.md を参照してください
クライアント & GROQ:
- examples/core.md — クライアントセットアップ、GROQ クエリ、エラーハンドリング、TypeGen
スキーマ:
- examples/schemas.md — defineType、defineField、ドキュメントタイプ、オブジェクトタイプ、参照、画像
リッチコンテンツ:
- examples/rich-content.md — Portable Text レンダリング、画像 URL ビルダー、レスポンシブ画像
ミューテーション & リアルタイム:
- examples/mutations.md — 作成、部分更新、削除、トランザクション、リアルタイムリスナー
哲学
Sanity は、リアルタイムコンテンツレイク、クエリ言語としての GROQ(Graph-Relational Object Queries)、そしてカスタマイズ可能な編集環境としての Sanity Studio を中心に構築された構造化コンテンツプラットフォームです。
コア原則:
- 構造化コンテンツ — コンテンツは、形状、検証、編集 UI を記述するスキーマ(
defineType、defineField)によって定義されます。スキーマはコードであり、設定ファイルではありません。 - GROQ ファーストのクエリ — GROQ を使用すると、1 つのクエリでデータのフィルタリング、プロジェクション、結合、再形成ができます。REST や GraphQL とは異なり、GROQ クエリはプロジェクションで定義した通りの形状を正確に返します。
- データとしてのコンテンツ — リッチテキストは Portable Text(JSON ベースの仕様)として保存されるため、ベンダーロックインなく任意のフロントエンドフレームワークでレンダリングできます。
- API バージョニング — すべてのクライアントは
apiVersion日付文字列を指定する必要があります。これにより、コードは特定の API 動作に固定され、破壊的な変更が本番環境に影響を与えるのを防ぎます。 - CDN キャッシング — パブリック読み取りクエリは、エッジキャッシュされた応答のために
useCdn: trueを使用します。ミューテーションと認証された読み取りは、新鮮なデータのためにuseCdn: falseを使用します。 - 型生成 — Sanity TypeGen は、スキーマと GROQ クエリの両方から TypeScript の型を生成し、コンテンツモデルからフロントエンドまでエンドツーエンドの型安全性を実現します。
Sanity を使用するタイミング:
- コンテンツ駆動型のウェブサイトやアプリケーション(ブログ、マーケティングサイト、ドキュメント)
- カスタマイズ可能なスタジオでリアルタイムの共同編集が必要なプロジェクト
- 1 つのコンテンツソースからのマルチチャネルコンテンツ配信(ウェブ、モバイル、IoT)
- GROQ と TypeGen を使用した型安全なコンテンツクエリを求めるチーム
使用すべきでないタイミング:
- ACID 保証が必要なトランザクションデータ(データベースを使用してください)
- 大規模なユーザー生成コンテンツ(Sanity は編集コンテンツ向けに最適化されています)
- セルフホスト型 CMS が必要なプロジェクト(Sanity のコンテンツレイクはホスト型ですが、Studio はオープンソースです)
コアパターン
パターン 1: createClient によるクライアントセットアップ
プロジェクト ID、データセット、API バージョン、CDN 設定を使用して @sanity/client を設定します。常に apiVersion を日付文字列に設定し、useCdn を明示的に指定します。
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: "2025-02-19", // 特定の API バージョン日付に固定
useCdn: true, // パブリック読み取りは true、認証/新鮮なデータには false
});
デュアルクライアントセットアップ(パブリック + トークンを使用したプレビュー)については、examples/core.md を参照してください。
パターン 2: フィルターとプロジェクションを使用した GROQ クエリ
GROQ クエリは、フィルター、プロジェクション、並べ替え、スライスを組み合わせます。TypeGen には常に defineQuery() を、動的な値には $param を使用してください。
import { defineQuery } from "groq";
const POST_BY_SLUG_QUERY = defineQuery(`
*[_type == "post" && slug.current == $slug][0]{
_id, title, body, "author": author->{name, image}
}
`);
const post = await client.fetch(POST_BY_SLUG_QUERY, { slug: "my-post" });
ユーザー入力を GROQ 文字列に直接挿入しないでください。GROQ インジェクションを防ぐために、常に $param パラメーターを使用してください。高度なクエリ(複合クエリ、条件付きプロジェクション)については、examples/core.md を参照してください。
パターン 3: defineType と defineField を使用したスキーマ定義
型安全性と Studio UI のために、"sanity" から defineType、defineField、defineArrayMember を使用してコンテンツ構造を定義します。
import { defineType, defineField } from "sanity";
export const postType = defineType({
name: "post",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: (r) => r.required(),
}),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
],
});
完全なスキーマ(ホットスポット付き画像、配列、参照、プレビュー、オブジェクトタイプ)については、examples/schemas.md を参照してください。
パターン 4: Portable Text レンダリング
@portabletext/react を使用してブロックコンテンツをレンダリングします。非標準のブロック(画像、コード)やマーク(リンク、ハイライト)については、カスタム PortableTextComponents を定義します。
import { PortableText } from "@portabletext/react";
<PortableText value={body} components={components} />;
非推奨の @sanity/block-content-to-react パッケージは使用しないでください。完全なコンポーネント例については、examples/rich-content.md を参照してください。
パターン 5: 画像 URL ビルダー
@sanity/image-url を使用して、クロップとホットスポットをサポートする最適化されたレスポンシブ画像 URL を生成します。
import { createImageUrlBuilder } from "@sanity/image-url";
const builder = createImageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}
// 使用法: urlFor(image).width(800).auto("format").url()
レスポンシブな srcSet パターンと画像変換については、examples/rich-content.md を参照してください。
パターン 6: ミューテーション(作成、部分更新、削除)
ドキュメントミューテーションには @sanity/client のメソッドを使用します。パッチとトランザクションでは常に .commit() を呼び出してください。
await client.create({ _type: "post", title: "New Post" });
await client.patch("post-123").set({ title: "Updated" }).commit();
await client.delete("post-123");
createOrReplace、createIfNotExists、トランザクション、配列への挿入、可視性オプションについては、examples/mutations.md を参照してください。
パターン 7: リアルタイムリスナー
client.listen() でドキュメントの変更を購読します。リスナーは GROQ のフィルター部分のみを使用します。プロジェクションと並べ替えは無視されます。
const subscription = client.listen(`*[_type == "post"]`).subscribe({
next: (update) => {
/* update.transition: 'update' | 'appear' | 'disappear' */
},
error: (err) => console.error(err),
});
subscription.unsubscribe(); // 完了したらクリーンアップ
本番用フロントエンドでは、よりシンプルな代替手段として新しい Live Content API を評価してください。リスナーオプションと注意事項については、examples/mutations.md を参照してください。
パターン 8: 型安全な GROQ のための TypeGen
sanity.cli.ts で TypeGen を overloadClientMethods: true で設定し、型付きの client.fetch 結果を取得します。"groq" から defineQuery() を使用して、TypeGen がクエリを検出できるようにします。
// sanity.cli.ts — typegen.overloadClientMethods: true を設定
// queries/post-queries.ts — クエリを defineQuery() でラップ
// sanity.types.ts — 自動生成された結果の型
const posts = await client.fetch(allPostsQuery); // 型付き結果
defineQuery() を使用しないインラインクエリ文字列は、型付けされていない(any)結果を生成します。完全な TypeGen 設定とワークフローについては、examples/core.md を参照してください。
<decision_framework>
意思決定フレームワーク
useCdn: true vs false
データはパブリックでパーソナライズされていないか?
├─ YES → useCdn: true(エッジキャッシュ、高速)
└─ NO →
├─ 認証読み取りにトークンを使用? → useCdn: false
├─ リアルタイムの新鮮なデータ(プレビュー)が必要? → useCdn: false
└─ ミューテーションを実行? → useCdn: false
下書き処理
下書きドキュメントが必要か?
├─ YES → perspective: 'previewDrafts' を使用(トークンが必要)
├─ NO → perspective: 'published' を使用(API v2025-02-19 以降のデフォルト)
└─ 混合(プレビューモードの切り替え)?
└─ 2 つのクライアントを作成: 1 つはパブリック(useCdn: true)、もう 1 つはプレビュー用(トークン + useCdn: false)
GROQ クエリの戻り値の形状
期待するドキュメント数は?
├─ 1 件(ID、スラッグ、シングルトンによる) → 末尾に [0] を追加(オブジェクトまたは null を返す)
├─ 複数件(リスト、フィード) → スライス接尾辞なし(配列を返す)
└─ ページネーション → [start...end] スライスを追加
画像処理
画像はどのように配信されるべきか?
├─ 固定サイズ(サムネイル、アバター) → urlFor(img).width(W).height(H).url()
├─ レスポンシブ(記事画像) → 複数の幅で srcSet
├─ フォーマット最適化 → .auto('format') で WebP/AVIF
└─ アスペクト比にクロップ → .width(W).height(H).fit('crop')
ミューテーションメソッドの選択
必要な操作は?
├─ 新しいドキュメントを作成 → client.create()
├─ 作成または完全に置換 → client.createOrReplace()(シングルトン用)
├─ 存在しない場合のみ作成 → client.createIfNotExists()
├─ 特定のフィールドを更新 → client.patch(id).set({...}).commit()
├─ フィールドを削除 → client.patch(id).unset([...]).commit()
├─ 複数の関連変更 → client.transaction()...commit()
└─ ドキュメントを削除 → client.delete(id)
</decision_framework>
<red_flags>
RED FLAGS(警告フラグ)
高優先度の問題:
- クライアントの
apiVersion欠落 —apiVersionを省略すると、予告なく変更される可能性のあるレガシー API 動作が使用されます。常に日付文字列で固定してください。 - GROQ クエリへの文字列補間 — ユーザー入力を GROQ 文字列に補間すると、GROQ インジェクションが可能になります。常に
$paramパラメーターを使用してください。 - トークンでの
useCdn: trueの使用 — CDN キャッシュされた応答は認証トークンを無視します。認証されたクエリはuseCdn: falseを使用する必要があります。 - トークンなしでの下書きドキュメントへのアクセス — 下書き(
drafts.*ドキュメント)には API トークンとperspective: 'previewDrafts'が必要です。トークンがないと下書きは見えません。
中優先度の問題:
- 必要なフィールドが少ないのに
{...}ですべてのフィールドを取得 — 過剰なフェッチは帯域幅と CDN キャッシュ効率を浪費します。必要なフィールドのみをプロジェクションしてください。 - パッチでの
.commit()の欠落 —client.patch(id).set({...})は.commit()なしでは何も行いません。ミューテーションは送信されません。 - GROQ クエリに
defineQuery()を使用していない — TypeGen はdefineQuery()でラップされるか名前付き変数に割り当てられていないクエリの型を生成できません。 - ハードコードされたプロジェクト ID またはデータセット — 環境変数を使用してください。ハードコードされた値は環境の切り替えを妨げ、プロジェクトの詳細を漏洩します。
- 非推奨の
@sanity/block-content-to-reactの使用 —@portabletext/reactに置き換えられました。古いパッケージはメンテナンスされていません。
よくある間違い:
- ミューテーションでの配列アイテムの
_key忘れ — Sanity 配列のすべてのアイテムには一意の_keyフィールドが必要です。_keyがないミューテーションは失敗します。 client.create()での_idの使用 —create()はランダムな_idを生成します。_idを指定してドキュメントが存在するとエラーになります。冪等な操作にはcreateOrReplace()またはcreateIfNotExists()を使用してください。[0]がnullを返すケースの処理漏れ — 末尾に[0]を付けても結果が null になる可能性があります。後続のプロパティアクセスを行う前に必ず null チェックを行うか、??を使用してください。fetchで@sanity/preview-kitを使用する際のperspectiveの欠落 — プレビューモードではperspective: 'previewDrafts'が設定されたプレビュークライアントを使用する必要があります。そうしないと、下書きはクエリ結果に表示されません。
</red_flags>