dist/plugins/api-cms-sanity/skills/api-cms-sanity/SKILL.md
Structured content platform — GROQ queries, schema definitions, @sanity/client, Portable Text, image handling, real-time listeners, mutations, TypeGen
npx skillsauth add agents-inc/skills api-cms-sanityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Sanity for structured content management with GROQ queries, typed schemas via
defineType/defineField, and@sanity/clientfor data fetching. Always setapiVersionto a dated string, useuseCdn: truefor public reads, handle draft documents explicitly, use@sanity/image-urlfor image transformations, and render rich text with@portabletext/react. Generate TypeScript types withsanity typegen generate.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST always set apiVersion on createClient to a dated string like '2025-02-19' — omitting it uses a legacy API that may break)
(You MUST use useCdn: true for public read queries and useCdn: false when using a token or needing fresh data)
(You MUST use parameterized GROQ queries ($param) for any dynamic values — never interpolate user input into GROQ strings)
(You MUST handle drafts explicitly — draft documents have _id prefixed with drafts. and are not returned by default with perspective: 'published')
(You MUST use defineQuery() from groq and assign queries to named variables for TypeGen type generation)
</critical_requirements>
Auto-detection: 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
When to use:
@sanity/client with createClient for data fetchingdefineType, defineField, defineArrayMember@portabletext/react@sanity/image-url (responsive images, crops, hotspots)client.listen()Key patterns covered:
createClient and apiVersion configurationclient.listen()defineQuery()When NOT to use:
Detailed Resources:
Client & GROQ:
Schemas:
Rich Content:
Mutations & Real-time:
Sanity is a structured content platform built around a real-time content lake, GROQ (Graph-Relational Object Queries) as its query language, and Sanity Studio as a customizable editing environment.
Core principles:
defineType, defineField) that describe shape, validation, and editorial UI. Schemas are code, not configuration files.apiVersion date string. This pins your code to a specific API behavior, preventing breaking changes from affecting production.useCdn: true for edge-cached responses. Mutations and authenticated reads use useCdn: false for fresh data.When to use Sanity:
When NOT to use:
Configure @sanity/client with project ID, dataset, API version, and CDN preference. Always set apiVersion to a dated string and useCdn explicitly.
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: "2025-02-19", // Pin to a specific API version date
useCdn: true, // true for public reads, false for authenticated/fresh data
});
For dual client setup (public + preview with token), see examples/core.md.
GROQ queries combine filters, projections, ordering, and slicing. Always use defineQuery() for TypeGen and $param for dynamic values.
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" });
Never interpolate user input into GROQ strings -- always use $param parameters to prevent GROQ injection. For advanced queries (combined queries, conditional projections), see examples/core.md.
Define content structure with defineType, defineField, and defineArrayMember from "sanity" for type safety and Studio UI.
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" } }),
],
});
For complete schemas (images with hotspot, arrays, references, previews, object types), see examples/schemas.md.
Render block content with @portabletext/react. Define custom PortableTextComponents for non-standard blocks (images, code) and marks (links, highlights).
import { PortableText } from "@portabletext/react";
<PortableText value={body} components={components} />;
Do not use the deprecated @sanity/block-content-to-react package. For full component examples, see examples/rich-content.md.
Use @sanity/image-url to generate optimized, responsive image URLs with crop and hotspot support.
import { createImageUrlBuilder } from "@sanity/image-url";
const builder = createImageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}
// Usage: urlFor(image).width(800).auto("format").url()
For responsive srcSet patterns and image transformations, see examples/rich-content.md.
Use @sanity/client methods for document mutations. Always call .commit() on patches and transactions.
await client.create({ _type: "post", title: "New Post" });
await client.patch("post-123").set({ title: "Updated" }).commit();
await client.delete("post-123");
For createOrReplace, createIfNotExists, transactions, array inserts, and visibility options, see examples/mutations.md.
Subscribe to document changes with client.listen(). The listener only uses the filter portion of GROQ -- projections and ordering are ignored.
const subscription = client.listen(`*[_type == "post"]`).subscribe({
next: (update) => {
/* update.transition: 'update' | 'appear' | 'disappear' */
},
error: (err) => console.error(err),
});
subscription.unsubscribe(); // Cleanup when done
For production frontends, evaluate the newer Live Content API as a simpler alternative. For listener options and caveats, see examples/mutations.md.
Configure TypeGen in sanity.cli.ts with overloadClientMethods: true for typed client.fetch results. Use defineQuery() from "groq" to make queries discoverable by TypeGen.
// sanity.cli.ts — set typegen.overloadClientMethods: true
// queries/post-queries.ts — wrap queries with defineQuery()
// sanity.types.ts — auto-generated result types
const posts = await client.fetch(allPostsQuery); // Typed result
Inline query strings without defineQuery() produce untyped (any) results. For full TypeGen configuration and workflow, see examples/core.md.
<decision_framework>
Is the data public and non-personalized?
├─ YES → useCdn: true (edge-cached, fast)
└─ NO →
├─ Using a token for authenticated reads? → useCdn: false
├─ Need real-time fresh data (preview)? → useCdn: false
└─ Performing mutations? → useCdn: false
Do you need draft documents?
├─ YES → Use perspective: 'previewDrafts' (requires token)
├─ NO → Use perspective: 'published' (default since API v2025-02-19)
└─ Mixed (preview mode toggle)?
└─ Create two clients: one public (useCdn: true), one preview (token + useCdn: false)
How many documents do you expect?
├─ One (by ID, slug, singleton) → Add [0] at end (returns object or null)
├─ Many (list, feed) → No slice suffix (returns array)
└─ Paginated → Add [start...end] slice
How should images be delivered?
├─ Fixed size (thumbnails, avatars) → urlFor(img).width(W).height(H).url()
├─ Responsive (article images) → srcSet with multiple widths
├─ Format optimization → .auto('format') for WebP/AVIF
└─ Cropped to aspect ratio → .width(W).height(H).fit('crop')
What operation do you need?
├─ Create new document → client.create()
├─ Create or fully replace → client.createOrReplace() (for singletons)
├─ Create only if missing → client.createIfNotExists()
├─ Update specific fields → client.patch(id).set({...}).commit()
├─ Remove fields → client.patch(id).unset([...]).commit()
├─ Multiple related changes → client.transaction()...commit()
└─ Delete document → client.delete(id)
</decision_framework>
<red_flags>
High Priority Issues:
apiVersion on client — Omitting apiVersion uses legacy API behavior that may change without notice. Always pin to a date string.$param parameters.useCdn: true with a token — CDN-cached responses ignore authentication tokens. Authenticated queries must use useCdn: false.drafts.* documents) require an API token and perspective: 'previewDrafts'. Without a token, drafts are invisible.Medium Priority Issues:
{...} when only a few are needed — Over-fetching wastes bandwidth and CDN cache efficiency. Project only the fields you need..commit() on patches — client.patch(id).set({...}) without .commit() does nothing — the mutation is never sent.defineQuery() for GROQ queries — TypeGen cannot generate types for queries that aren't wrapped in defineQuery() or assigned to named variables.@sanity/block-content-to-react — Replaced by @portabletext/react. The old package is unmaintained.Common Mistakes:
_key on array items in mutations — Every item in a Sanity array must have a unique _key field. Mutations without _key will fail._id with client.create() — create() generates a random _id. If you specify _id and the document exists, it errors. Use createOrReplace() or createIfNotExists() for idempotent operations.[0] returns null — A GROQ query ending in [0] returns null if no documents match, not an empty object.client.listen() to work with projections — The listener only uses the filter portion of a GROQ query. Projections, ordering, and slicing are ignored.Gotchas & Edge Cases:
2025-02-19 changed default perspective — Before this version, the default perspective was raw (includes drafts). After, it defaults to published. Existing code may break if you update apiVersion without accounting for this.useCdn: true) may return stale data for a few seconds. Use useCdn: false or add a small delay for consistency-critical reads after writes..current — Query slug.current, not slug directly. *[slug == "my-slug"] will never match.asset._ref to urlFor() works for basic URLs but loses crop and hotspot metadata. Pass the entire image field object._key on every block — When creating Portable Text content programmatically, every block and inline object needs a unique _key.client.listen() reconnects automatically — But there's no built-in guarantee against missed events during reconnection. For critical use cases, combine with periodic re-fetching.schema.json extraction first — Run npx sanity schema extract before npx sanity typegen generate. The extract step reads your Studio schemas and outputs a JSON representation.</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST always set apiVersion on createClient to a dated string like '2025-02-19' — omitting it uses a legacy API that may break)
(You MUST use useCdn: true for public read queries and useCdn: false when using a token or needing fresh data)
(You MUST use parameterized GROQ queries ($param) for any dynamic values — never interpolate user input into GROQ strings)
(You MUST handle drafts explicitly — draft documents have _id prefixed with drafts. and are not returned by default with perspective: 'published')
(You MUST use defineQuery() from groq and assign queries to named variables for TypeGen type generation)
Failure to follow these rules will cause unpredictable API behavior, GROQ injection vulnerabilities, and untyped query results.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety