Skip to content



Raw SKILL.md · MIT · sha256:ad788c864e54e69cdfbb9bf997daedbd56dd33b876404fbbdfe4344c85c8897a

Sanity schema design

Schema conventions for Sanity studios that editors actually enjoy using. The recurring theme: every field teaches the editor what it does, every type carries an icon, and the folder structure scales past the tenth page builder block without turning into a junk drawer.

Basic structure

For TypeScript files, always import the Sanity helpers and use them everywhere: defineType around the whole type, defineField on every field, defineArrayMember only when needed.

import { defineArrayMember, defineField, defineType } from "sanity";

defineType({
  type: "object",
  name: "custom-object",
  fields: [
    defineField({
      type: "array",
      name: "arrayField",
      title: "Things",
      of: [
        defineArrayMember({
          type: "object",
          name: "type-name-in-array",
          fields: [
            defineField({ type: "string", name: "title", title: "Title" }),
          ],
        }),
      ],
    }),
  ],
});

Always use named exports for schema types.

Icons

Every document and block type gets an icon. Prefer @sanity/icons first; fall back to whichever icon set the project already has installed (lucide-react is common). An iconless type in a page builder insert menu is unfindable once the menu passes ten entries.

Studio folder structure

Rough shape, with an index at each level so types compose into arrays near where they live:

studio/
├── sanity.config.ts
├── structure.ts
├── components/
│   └── slug-field-component.tsx
├── plugins/
│   └── presentation-url.ts
├── schemaTypes/
│   ├── index.ts
│   ├── common.ts
│   ├── blocks/
│   │   ├── index.ts
│   │   ├── hero.ts
│   │   ├── cta.ts
│   │   ├── faq-accordion.ts
│   │   ├── feature-cards.ts
│   │   └── image-link-cards.ts
│   ├── definitions/
│   │   ├── index.ts
│   │   ├── button.ts
│   │   ├── custom-url.ts
│   │   ├── pagebuilder.ts
│   │   └── rich-text.ts
│   └── documents/
│       ├── author.ts
│       ├── blog.ts
│       ├── faq.ts
│       └── page.ts
└── utils/
    ├── og-fields.ts
    ├── seo-fields.ts
    └── slug.ts

Do not copy these names literally; the point is the layering. documents/ for queryable document types, blocks/ for page builder sections, definitions/ for reusable field-level types.

Block index pattern

Each folder's index.ts exports an array, so registering a new block is one import plus one array entry, and the root index stays small:

import { callToAction } from "./call-to-action";
import { faqList } from "./faq-list";
import { featureCards } from "./feature-cards";
import { hero } from "./hero";
import { logoCloud } from "./logo-cloud";
import { pricingTable } from "./pricing-table";
import { richTextBlock } from "./rich-text-block";
import { statsCard } from "./stats-card";
import { testimonialQuote } from "./testimonial-quote";

export const pagebuilderBlocks = [
  hero,
  featureCards,
  faqList,
  callToAction,
  logoCloud,
  pricingTable,
  statsCard,
  testimonialQuote,
  richTextBlock,
];

export const blocks = [...pagebuilderBlocks];

Common field templates

Every field gets a name, title, description, and type, with the description written for a non-technical editor and placed above type. These templates cover the fields almost every block needs:

Eyebrow

defineField({
  name: "eyebrow",
  title: "Eyebrow",
  description: "The smaller text that sits above the title to provide context",
  type: "string",
});

Title

defineField({
  name: "title",
  title: "Title",
  description: "The large text that is the primary focus of the block",
  type: "string",
});

Heading level toggle

defineField({
  name: "isHeadingOne",
  title: "Is it a <h1>?",
  type: "boolean",
  description:
    "By default the title is a <h2> tag. If you use this as the top block on the page, you can toggle this on to make it a <h1> instead",
  initialValue: false,
});

Rich text

defineField({
  name: "richText",
  title: "Rich Text",
  description:
    "Large body of text that has links, ordered/unordered lists and headings.",
  type: "richText",
});

Buttons

defineField({
  name: "buttons",
  title: "Buttons",
  description: "Add buttons here, the website will handle the styling",
  type: "array",
  of: [{ type: "button" }],
});

Image

defineField({
  name: "image",
  title: "Image",
  type: "image",
  fields: [
    defineField({
      name: "alt",
      type: "string",
      description:
        "Remember to use alt text for people to be able to read what is happening in the image if they are using a screen reader, it's also important for SEO",
      title: "Alt Text",
    }),
  ],
});

Type generation

After adding or changing schema, regenerate TypeScript definitions:

sanity schema extract && sanity typegen generate --enforce-required-fields

Run this in the same change as the schema edit so the frontend types never drift.

Building schema from screenshots

When asked to produce schema from a design screenshot, describe the types using the conventions above and lean on these visual cues:

  • Tiny text above a title is likely an eyebrow
  • Large unformatted text that reads as a header is a title or subtitle
  • Text with formatting (bold, italics, lists) needs richText
  • Every image gets an alt text field
  • Repeated button patterns use the reusable button array
  • If the project already defines richTextField or buttonsField helpers, use them

Always include editor-facing descriptions based on what the element does in the design.

About this skill

Maintained by Roboto Studio, a UK agency that has shipped Sanity studios for startups through enterprise. It distills the schema conventions we hold every project to. If you would rather have it done for you: robotostudio.com/services/sanity.

Licensed MIT. Wow, I can't believe people are actually using these. Tell me if it worked: yo@robotostudio.com