Getting started

Example repository: See the example repository with Sanity schemas, block registry, and UI components already wired up. Clone it to explore a full setup or use it as reference while you adapt the steps below to your project.

You can also import the data exported from the example repository to your project. Check the README.md for more details.

Sanity is a powerful CMS, and each project can have its own specifics (folder structure, conventions, plugins, etc.). This documentation shows the most minimal approach — only what’s needed to use the blocks. You should adapt the examples to your setup, rules, and context.

Sanity Typegen: I've used Sanity TypeGen to generate TypeScript types from the schema. The registry and components import types from the generated file (e.g. Blocks, Home in sanity.types.ts). Configure and run typegen in your project per the official documentation.

Shared objects (optional)

Blocks that use CTA, links, etc. reuse shared objects. Example: the cta object. See Sanity › Shared for the list.

You need to export and include these types in your schema according to your project’s context (e.g. an objects index that re-exports cta and others).

Example: schema-types/objects/cta.ts
import { defineField, defineType } from 'sanity'

// Shared object. Used by blocks that have CTA (e.g. grid-two-columns).
export const cta = defineType({
  name: 'cta',
  title: 'CTA',
  type: 'object',
  fields: [
    defineField({
      name: 'ctaEnabled',
      title: 'Enable CTA',
      type: 'boolean',
      description: 'Enable or disable the call to action button',
      initialValue: false,
    }),
    defineField({
      name: 'text',
      title: 'Text',
      type: 'string',
      hidden: ({ parent }) => parent?.ctaEnabled === false,
    }),
    defineField({
      name: 'link',
      title: 'Link',
      type: 'string',
      hidden: ({ parent }) => parent?.ctaEnabled === false,
    }),
    defineField({
      name: 'variant',
      title: 'Variant',
      type: 'string',
      options: {
        list: [
          { title: 'Default', value: 'default' },
          { title: 'Secondary', value: 'secondary' },
          { title: 'Outline', value: 'outline' },
          { title: 'Ghost', value: 'ghost' },
          { title: 'Link', value: 'link' },
        ],
        layout: 'radio',
      },
      initialValue: 'default',
      hidden: ({ parent }) => parent?.ctaEnabled === false,
    }),
  ],
})

Define a block (e.g. Grid Two Columns)

schema-types/blocks/content/grid-two-columns.ts
import {defineField, defineType} from 'sanity'

// Example content block. Blocks that use shared objects (e.g. CTA)
// import and use defineField with type: 'cta' in fields.
export const contentGridTwoColumnsBlock = defineType({
  name: 'contentGridTwoColumns',
  title: 'Grid Two Columns',
  type: 'object',
  groups: [
    {
      name: 'content',
      title: 'Content',
    },
  ],
  fieldsets: [
    {
      name: 'content',
      title: 'Content',
      options: {
        collapsible: true,
        collapsed: false,
      },
    },
  ],
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      group: 'content',
      fieldset: 'content',
    }),
    defineField({
      name: 'content',
      title: 'Content',
      type: 'text',
      group: 'content',
      fieldset: 'content',
    }),
    defineField({
      name: 'media',
      title: 'Media',
      type: 'image',
      group: 'content',
    }),
  ],
  preview: {
    select: {
      title: 'title',
      content: 'content',
    },
    prepare(selection) {
      const {title, content} = selection
      return {
        title: title ? `Grid Two Columns - ${title}` : 'Grid Two Columns',
        subtitle: content ? content.substring(0, 50) + '...' : '',
      }
    },
  },
})

Define the blocks array (blocks.ts)

This is how I use blocks on a page: an array per category (e.g. content) referenced on the document type. When you add blocks or categories, update this file. The values in of refer to the schema type — if the type doesn’t exist in the project, the Studio will break.

schema-types/blocks/blocks.ts
import {defineType} from 'sanity'

// As you add more blocks or categories, update this file.
// The values in 'of' reference each block's schema name — if the type
// does not exist in the project (not defined and exported in the flow), the Studio breaks.
export const blocks = defineType({
  name: 'blocks',
  title: 'UI Blocks',
  type: 'array',
  options: {
    insertMenu: {
      groups: [
        {
          name: 'content',
          title: 'Content',
          of: ['contentGridTwoColumns'],
        },
        // Add more groups (hero, showcase, etc.) and their blocks here.
      ],
      views: [
        {
          name: 'grid',
          previewImageUrl: (schemaTypeName: string) => `/preview/blocks/${schemaTypeName}.png`,
        },
        {name: 'list'},
      ],
    },
  },
  of: [
    {type: 'contentGridTwoColumns', title: 'Content • Grid Two Columns'},
    // List all blocks you defined here (one entry per type).
  ],
})

Define a page type (e.g. home)

A document type like home uses the blocks array so editors can add blocks in the Studio. The field type: 'blocks' references the blocks schema defined in blocks.ts.

schema-types/pages/home-type.ts
import {defineField, defineType} from 'sanity'

export const homeType = defineType({
  name: 'home',
  title: 'Home',
  type: 'document',
  preview: {
    prepare() {
      return {
        title: 'Home',
      }
    },
  },
  fields: [
    defineField({
      name: 'blocks',
      type: 'blocks',
    }),
  ],
})

Block registry (UI)

When adding a new block, register it here. Import types from the generated file (e.g. Blocks from sanity.types.ts).

components/flx/sanity/blocks/registry.ts
import { Blocks } from "../../sanity.types";

type BlockTypeMap = {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [K in Blocks[number]["_type"]]: React.ComponentType<any>;
};

export const blockRegistry: BlockTypeMap = {
  // Add all blocks here

  // Example:
  // 'contentBadgeList': Content.BadgeList, 
  // 'contentCarouselMedia': Content.CarouselMedia, ...
};

export type BlockType = keyof typeof blockRegistry;

Render block

import { Blocks } from "../../sanity.types";

import { blockRegistry } from "./registry";

interface RenderBlockProps {
  block: Blocks[number];
}

export function RenderBlock({ block }: Readonly<RenderBlockProps>) {
  if (typeof block === "string" || !block) {
    return null;
  }

  const Component = blockRegistry[block._type];

  if (Component) {
    return <Component block={block} />;
  }

  return null;
}

Blocks component

import { Blocks as BlocksType } from "../../sanity.types";

import { blockRegistry } from "./registry";
import { RenderBlock } from "./render-block";

interface BlocksProps {
  blocks: BlocksType;
}

export function Blocks({ blocks }: Readonly<BlocksProps>) {
  if (!blocks || blocks.length === 0) {
    return null;
  }

  return blocks?.map((block) => {
    if (!blockRegistry[block._type]) {
      return null;
    }

    if (blocks) {
      return (
        <section key={block._key}>
          <RenderBlock key={block._key} block={block} />
        </section>
      );
    }
    return null;
  });
}

Example page

app/(pages)/page.tsx
import { client } from "@/sanity/client";
import { Home } from "@/sanity.types";
import { Blocks } from "@/components/blocks/blocks";
import { cache } from "react";

const getHome = cache(async () => {
  const query = `*[_type == "home"][0]{
    _id,
    seo,
    blocks[]
  }`;

  const data = await client.fetch<Home>(query);

  if (!data) return null;

  return data;
});

export default async function HomePage() {
  const homeData = await getHome();

  return (
    <main className="container mx-auto min-h-screen max-w-6xl p-8 gap-16 flex flex-col">
      <Blocks blocks={homeData?.blocks ?? []} />
    </main>
  );
}