api-cms-sanity-bb8117 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-cms-sanity-bb8117 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Use Sanity for structured content management with GROQ queries, typed schemas viadefineType/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:
<philosophy>
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:
</philosophy>
<patterns>
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 doneFor 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 resultInline query strings without defineQuery() produce untyped (any) results. For full TypeGen configuration and workflow, see examples/core.md.
</patterns>
<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: falseDo 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] sliceHow 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 uses legacy API behavior that may change without notice. Always pin to a date string.$param parameters.useCdn: false.drafts.* documents) require an API token and perspective: 'previewDrafts'. Without a token, drafts are invisible.Medium Priority Issues:
client.patch(id).set({...}) without .commit() does nothing — the mutation is never sent.defineQuery() or assigned to named variables.@portabletext/react. The old package is unmaintained.Common Mistakes:
_key field. Mutations without _key will fail.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 if no documents match, not an empty object.Gotchas & Edge Cases:
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.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.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>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.