Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add Mindrally/skills --skill sanity -a claude-code
Open on GitHub

What 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-code

2Manually: put the files in place

~/.claude/skills/sanity/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/sanity/

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

Similar skills

All skills for Backend and APIs →

From the same set214

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor