sanity — Claude Code skill: schemas, GROQ, TypeScript
A skill for Sanity CMS work: it guides the agent through document schemas, GROQ queries and TypeScript wiring for content.
npx skills add Mindrally/skills --skill sanity -a claude-codeWhat sanity does
The skill puts the agent in the role of a Sanity CMS specialist. It ships templates built on defineType and defineField for document and object types, a blockContent array with blocks, images and link annotations, and a preview config so the studio list shows titles and authors.
GROQ gets its own section: fetching all posts ordered by publishedAt, a single post by slug.current, pagination with [$start...$end], plus select for conditional fields and coalesce for fallbacks. Client setup uses @sanity/client with projectId, dataset and apiVersion, and @sanity/image-url for image URLs. You supply the credentials yourself — the client will not build without a project ID and dataset.
There is also material on generating types from the schema, custom validation such as checking an email domain, and custom input components. The skill recommends references between documents, portable text for rich content, and setting up CORS and API permissions. It assumes a project already running on Sanity; it will not scaffold one from scratch.
What it helps with
- define a document schema with slug and validation rules
- write a GROQ query with ordering and pagination
- link documents by reference and pull fields through the arrow
- set up the Sanity client with projectId, dataset and apiVersion
- add a custom field check inside the studio
Example prompts for the agent
- Help me define a post document schema with title, slug, author and publishedAt
- Write a GROQ query for all posts ordered by publishedAt with pagination
- Set up the Sanity client and urlFor for images in lib/client.ts
How to install sanity
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill sanity -a claude-code2Manually: put the files in place
~/.claude/skills/sanity/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/sanity/
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill sanity -a codex2Manually: put the files in place
.agents/skills/sanity/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill sanity -a cursor2Manually: put the files in place
.cursor/skills/sanity/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Mindrally/skills sanity2Manually: put the files in place
.github/skills/sanity/SKILL.md1Skills CLI (Vercel)
npx skills add Mindrally/skills --skill sanity -a gemini-cli2Platform CLI
gemini skills install https://github.com/Mindrally/skills --path sanityManually: put the files in place
.devin/skills/sanity/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Mindrally/skills --skill sanity -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about sanity
How do I install sanity in Claude?
Run npx skills add Mindrally/skills --skill sanity -a claude-code. The skill file lands at ~/.claude/skills/sanity/SKILL.md.
What does the sanity skill do?
It gives the agent Sanity CMS development guidelines: document and object schemas, GROQ queries, TypeScript integration, validation and project organization.
Which platforms support the sanity skill?
It installs into Claude, Codex, Cursor, GitHub Copilot, Gemini CLI and OpenCode, each with its own install command.
Source text
SKILL.md · Apache-2.0
---
name: sanity
description: Sanity CMS development guidelines for schema creation, GROQ queries, TypeScript integration, and project organization
metadata:
maintainer: Mindrally
source: https://github.com/Mindrally/skills
---
# Sanity CMS Development
You are an expert in Sanity CMS, GROQ queries, TypeScript integration, and headless CMS architecture.
## Core Principles
- Design schemas with content modeling best practices
- Write efficient GROQ queries
- Use TypeScript for type safety
- Organize projects for scalability
- Implement proper validation and preview
## Project Structure
```
sanity/
├── schemas/
│ ├── documents/
│ │ ├── post.ts
│ │ └── author.ts
│ ├── objects/
│ │ ├── blockContent.ts
│ │ └── image.ts
│ └── index.ts
├── lib/
│ ├── client.ts
│ └── queries.ts
├── components/
│ └── previews/
└── sanity.config.ts
```
## Schema Definition
### Document Types
```typescript
// schemas/documents/post.ts
import { defineType, defineField } from 'sanity';
export default defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required().min(10).max(80),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'author',
title: 'Author',
type: 'reference',
to: [{ type: 'author' }],
}),
defineField({
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent',
}),
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare({ title, author, media }) {
return {
title,
subtitle: author ? `by ${author}` : '',
media,
};
},
},
});
```
### Object Types
```typescript
// schemas/objects/blockContent.ts
import { defineType, defineArrayMember } from 'sanity';
export default defineType({
name: 'blockContent',
title: 'Block Content',
type: 'array',
of: [
defineArrayMember({
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'Quote', value: 'blockquote' },
],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
{ title: 'Code', value: 'code' },
],
annotations: [
{
name: 'link',
type: 'object',
title: 'URL',
fields: [
{
name: 'href',
type: 'url',
title: 'URL',
},
],
},
],
},
}),
defineArrayMember({
type: 'image',
options: { hotspot: true },
}),
],
});
```
## GROQ Queries
### Basic Queries
```typescript
// lib/queries.ts
// Get all posts
export const allPostsQuery = groq`
*[_type == "post"] | order(publishedAt desc) {
_id,
title,
slug,
publishedAt,
"author": author->name,
"imageUrl": mainImage.asset->url
}
`;
// Get single post by slug
export const postBySlugQuery = groq`
*[_type == "post" && slug.current == $slug][0] {
_id,
title,
body,
publishedAt,
"author": author->{name, image},
"categories": categories[]->title
}
`;
// Pagination
export const paginatedPostsQuery = groq`
*[_type == "post"] | order(publishedAt desc) [$start...$end] {
_id,
title,
slug,
excerpt
}
`;
```
### Advanced GROQ
```typescript
// Conditional projections
export const conditionalQuery = groq`
*[_type == "post"] {
title,
"content": select(
defined(body) => body,
"No content available"
)
}
`;
// Coalesce for fallbacks
export const fallbackQuery = groq`
*[_type == "post"] {
"displayTitle": coalesce(seoTitle, title)
}
`;
// References
export const withReferencesQuery = groq`
*[_type == "post" && references($authorId)] {
title,
publishedAt
}
`;
```
## Client Setup
```typescript
// lib/client.ts
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
useCdn: process.env.NODE_ENV === 'production',
});
const builder = imageUrlBuilder(client);
export function urlFor(source: any) {
return builder.image(source);
}
```
## TypeScript Integration
```typescript
// Generate types from schema
import { Post, Author } from '@/sanity/types';
export async function getPosts(): Promise<Post[]> {
return client.fetch(allPostsQuery);
}
export async function getPost(slug: string): Promise<Post | null> {
return client.fetch(postBySlugQuery, { slug });
}
```
## Validation
```typescript
defineField({
name: 'email',
type: 'string',
validation: (Rule) =>
Rule.required()
.email()
.custom((email) => {
if (email && !email.endsWith('@company.com')) {
return 'Must be a company email';
}
return true;
}),
});
```
## Custom Components
```typescript
// Custom input component
import { StringInputProps } from 'sanity';
export function CustomStringInput(props: StringInputProps) {
return (
<div>
<label>{props.schemaType.title}</label>
{props.renderDefault(props)}
<span>{props.value?.length ?? 0} characters</span>
</div>
);
}
```
## Best Practices
- Use references for relationships between documents
- Implement proper validation rules
- Create meaningful preview configurations
- Use portable text for rich content
- Optimize images with Sanity's image pipeline
- Set up proper CORS and API permissions
© Mindrally · Apache-2.0 · Mindrally/skills