Shared Schemas

Reusable Sanity object types in the Studio package

Skip this section if you're using our latest Next.js Sanity Starter. The shared schemas are already included under studio/schemas/ in the monorepo (Sanity Studio lives in studio/, not inside the Next.js frontend/ app).

Paths below are relative to the repository root.

  • Portable Text

studio/schemas/blocks/shared/block-content.ts:

import { defineType, defineArrayMember } from "sanity";
import { YouTubePreview } from "../../previews/youtube-preview";
import { GradientTextIcon, YouTubeIcon } from "../../previews/icons";

export default defineType({
  title: "Block Content",
  name: "block-content",
  type: "array",
  of: [
    defineArrayMember({
      title: "Block",
      type: "block",
      styles: [
        { title: "Normal", value: "normal" },
        { title: "H1", value: "h1" },
        { title: "H2", value: "h2" },
        { title: "H3", value: "h3" },
        { title: "H4", value: "h4" },
        { title: "Quote", value: "blockquote" },
      ],
      lists: [
        { title: "Bullet", value: "bullet" },
        { title: "Number", value: "number" },
      ],
      marks: {
        decorators: [
          { title: "Strong", value: "strong" },
          { title: "Emphasis", value: "em" },
          {
            title: "Gradient Text",
            value: "gradient-text",
            icon: GradientTextIcon,
          },
        ],
        annotations: [
          {
            name: "link",
            type: "object",
            title: "Link",
            fields: [
              {
                name: "isExternal",
                type: "boolean",
                title: "Is External",
                initialValue: false,
              },
              {
                name: "internalLink",
                type: "reference",
                title: "Internal Link",
                to: [{ type: "page" }, { type: "post" }],
                hidden: ({ parent }) => parent?.isExternal,
              },
              {
                name: "href",
                title: "href",
                type: "url",
                hidden: ({ parent }) => !parent?.isExternal,
                validation: (Rule) =>
                  Rule.uri({
                    allowRelative: true,
                    scheme: ["http", "https", "mailto", "tel"],
                  }),
              },
              {
                name: "target",
                type: "boolean",
                title: "Open in new tab",
                initialValue: false,
                hidden: ({ parent }) => !parent?.isExternal,
              },
            ],
          },
        ],
      },
    }),
    defineArrayMember({
      type: "image",
      options: { hotspot: true },
      fields: [
        {
          name: "alt",
          type: "string",
          title: "Alternative Text",
        },
      ],
    }),
    defineArrayMember({
      name: "youtube",
      type: "object",
      title: "YouTube",
      icon: YouTubeIcon,
      fields: [
        {
          name: "videoId",
          title: "Video ID",
          type: "string",
          description: "YouTube Video ID",
        },
      ],
      preview: {
        select: {
          title: "videoId",
        },
      },
      components: {
        preview: YouTubePreview,
      },
    }),
    defineArrayMember({
      name: "code",
      type: "code",
      options: {
        withFilename: true,
        language: "typescript",
        languageAlternatives: [
          { title: "TypeScript", value: "typescript" },
          { title: "JavaScript", value: "javascript" },
          { title: "JSX", value: "jsx" },
          { title: "TSX", value: "tsx" },
          { title: "HTML", value: "html" },
          { title: "CSS", value: "css" },
          { title: "SCSS", value: "scss" },
          { title: "JSON", value: "json" },
          { title: "Python", value: "python" },
          { title: "PHP", value: "php" },
          { title: "Ruby", value: "ruby" },
          { title: "Shell", value: "shell" },
          { title: "Markdown", value: "markdown" },
          { title: "YAML", value: "yaml" },
          { title: "GraphQL", value: "graphql" },
          { title: "SQL", value: "sql" },
        ],
      },
    }),
  ],
});
  • YouTube preview

studio/schemas/previews/youtube-preview.tsx:

import type { PreviewProps } from "sanity";
import { Play } from "lucide-react";
import { Box, Card, Flex, Text } from "@sanity/ui";

export function YouTubePreview(props: PreviewProps) {
  const { title: videoId } = props;

  if (typeof videoId !== "string" || !videoId) {
    return (
      <Card padding={3} radius={2} tone="transparent" border>
        <Flex align="center" justify="center" gap={2}>
          <Play size={20} />
          <Text size={1} muted>
            Add a YouTube Video ID
          </Text>
        </Flex>
      </Card>
    );
  }

  const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;

  return (
    <Card padding={3} radius={2} tone="default">
      <Box
        style={{
          width: "100%",
          maxWidth: "560px",
          aspectRatio: "16/9",
          position: "relative",
          margin: "0 auto",
          overflow: "hidden",
          borderRadius: "4px",
        }}
      >
        <img
          src={thumbnailUrl}
          alt="YouTube video thumbnail"
          style={{
            width: "100%",
            height: "100%",
            objectFit: "cover",
            display: "block",
          }}
          loading="lazy"
        />
        <Box
          style={{
            position: "absolute",
            inset: 0,
            pointerEvents: "none",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          <span
            style={{
              display: "inline-flex",
              alignItems: "center",
              justifyContent: "center",
              width: 74,
              height: 74,
              borderRadius: "50%",
              background: "rgba(0,0,0,0.45)",
              transition: "background 0.2s, box-shadow 0.2s",
              boxShadow: "0 2px 8px 0 rgba(0,0,0,0.12)",
              pointerEvents: "auto",
              border: "2px solid rgba(255,255,255,0.15)",
              cursor: "pointer",
            }}
          >
            <Play size={40} strokeWidth={1.8} color="#fff" />
          </span>
        </Box>
      </Box>
    </Card>
  );
}
  • Link with shadcn/ui button variant

studio/schemas/blocks/shared/link.ts:

import { defineField, defineType } from "sanity";

export default defineType({
  name: "link",
  type: "object",
  title: "Link",
  fields: [
    defineField({
      name: "isExternal",
      type: "boolean",
      title: "Is External",
      initialValue: false,
    }),
    defineField({
      name: "internalLink",
      type: "reference",
      title: "Internal Link",
      to: [{ type: "page" }, { type: "post" }],
      hidden: ({ parent }) => parent?.isExternal,
    }),
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "href",
      title: "href",
      type: "url",
      hidden: ({ parent }) => !parent?.isExternal,
      validation: (Rule) =>
        Rule.uri({
          allowRelative: true,
          scheme: ["http", "https", "mailto", "tel"],
        }),
    }),
    defineField({
      name: "target",
      type: "boolean",
      title: "Open in new tab",
      initialValue: false,
      hidden: ({ parent }) => !parent?.isExternal,
    }),
    defineField({
      name: "buttonVariant",
      type: "button-variant",
      title: "Button Variant",
    }),
  ],
});
  • Layout constants (used by block schemas)

studio/schemas/blocks/shared/layout-variants.ts:

export const STACK_ALIGN = [
  { title: "Left", value: "left" },
  { title: "Center", value: "center" },
];

export const SECTION_WIDTH = [
  { title: "Default", value: "default" },
  { title: "Narrow", value: "narrow" },
];

export const COLS_VARIANTS = [
  { title: "2 Columns", value: "grid-cols-2" },
  { title: "3 Columns", value: "grid-cols-3" },
  { title: "4 Columns", value: "grid-cols-4" },
];
  • Color variants from shadcn/ui

studio/schemas/blocks/shared/color-variant.ts:

import { defineType } from "sanity";

export const COLOR_VARIANTS = [
  { title: "Background", value: "background" },
  { title: "Primary", value: "primary" },
  { title: "Secondary", value: "secondary" },
  { title: "Card", value: "card" },
  { title: "Accent", value: "accent" },
  { title: "Destructive", value: "destructive" },
  { title: "Muted", value: "muted" },
];

export const colorVariant = defineType({
  name: "color-variant",
  title: "Color Variant",
  type: "string",
  options: {
    list: COLOR_VARIANTS.map(({ title, value }) => ({ title, value })),
  },
  initialValue: "background",
});
  • Button variants from shadcn/ui

studio/schemas/blocks/shared/button-variant.ts:

import { defineType } from "sanity";

export const BUTTON_VARIANTS = [
  { title: "Default", value: "default" },
  { title: "Destructive", value: "destructive" },
  { title: "Outline", value: "outline" },
  { title: "Secondary", value: "secondary" },
  { title: "Ghost", value: "ghost" },
  { title: "Link", value: "link" },
];

export const buttonVariant = defineType({
  name: "button-variant",
  title: "Button Variant",
  type: "string",
  options: {
    list: BUTTON_VARIANTS.map(({ title, value }) => ({ title, value })),
    layout: "radio",
  },
  initialValue: "default",
});
  • Section padding

studio/schemas/blocks/shared/section-padding.ts:

import { defineField, defineType } from "sanity";

export default defineType({
  name: "section-padding",
  type: "object",
  title: "Padding",
  description: "Add padding to the section. Based on design system spacing",
  fields: [
    defineField({
      name: "top",
      type: "boolean",
      title: "Top Padding",
    }),
    defineField({
      name: "bottom",
      type: "boolean",
      title: "Bottom Padding",
    }),
  ],
});