mcpField
S
sanity

sanity

github.com/mindrallyDevelopment287730 installsUpdated 3w ago
Repository

About sanity

A development skill for building with Sanity CMS, GROQ, and TypeScript. It covers content schemas, queries, client setup, validation, and custom components for headless CMS projects.

Key features

  1. 01Schema and content modeling
  2. 02GROQ query patterns
  3. 03TypeScript client integration
  4. 04Field validation and previews
  5. 05Custom input components

Use cases

  1. 01Defining document and object schemas
  2. 02Fetching typed content with GROQ
  3. 03Adding validation to CMS fields

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/mindrally/skills --skill sanity

After installing, type/sanityin Claude Code to confirm it loaded.

Details

From the project README

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
// 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
// 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
// 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
// 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

// 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

// 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

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

// 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

Comments

Markdown supported