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/uibutton 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",
}),
],
});