Sanity Presets
@sanity/presets provides ready-made helpers for creating schema types for common content patterns in Sanity Studio. Instead of modelling pages, links, images, and metadata from scratch, call a `define<Type>` function and get a working schema type with sensible defaults.
By Jordan Lawrence & Ash
Install command
npm i @sanity/presets@sanity/presets
Overview
@sanity/presets provides ready-made helpers for creating schema types for common content patterns in Sanity Studio. Instead of modelling pages, links, images, and metadata from scratch, call a define<Type> function and get a working schema type with sensible defaults.
Included presets:
definePage— document type for page building (content blocks, slug, SEO metadata)defineLink— internal and external links with conditional fieldsdefineCta— call-to-action with an inline link and semantic importance leveldefineSeo— search engine metadata (title, description, Open Graph image)defineImage— image with optional alt text, caption, and hotspotdefineRichText— Portable Text with link annotations, image blocks, and CTA inline objects; embedded objects default on, opt out withobjects: falseorobjects: {link: false}
When to use presets:
- You want opinionated defaults to get started quickly.
- You don't yet have a need for highly custom content modelling.
Presets are designed to be extended — add fields, groups, and map hooks as your needs evolve. When a preset no longer fits, replace it with your own schema type using defineType directly.
Installation
Prerequisites: A Sanity Studio project with sanity installed. See the getting started guide if you're starting from scratch.
Presets give you schema types to add to a Studio you've already created — they don't scaffold a project or generate a schema from nothing.
npm install @sanity/presetspnpm add @sanity/presetsyarn add @sanity/presetsGetting started
A working page-building schema, from scratch. The recommended layout keeps the registry in a module of its own, separate from your schema types and your Studio config:
sanity.config.ts # Studio config — imports the assembled schema types
schemaTypes/
├── presets.ts # creates the registry, exports the define* functions
├── hero.ts # a custom type, modelled by hand
└── index.ts # assembles the schema types array1. Create the registry
Call createPresetsRegistry once, in a module of its own. It returns the define<Type> functions that produce schema types. Export them for your schema files to import:
// schemaTypes/presets.ts
import {createPresetsRegistry} from '@sanity/presets'
export const {definePage, defineLink, defineCta, defineImage, defineRichText} =
createPresetsRegistry({
link: {
// Document types an internal link can point to. This cascades to
// every link — standalone, inside CTAs, inside rich text. See "Registry".
to: ['page'],
},
})Keep this in its own module rather than in sanity.config.ts or your schema index — creating the registry where your schema files import it back from leads to import cycles.
2. Define your schema types
Import the define<Type> functions and use them to build your types. definePage produces a document type; the others produce object types you compose into it. Presets and hand-modelled types mix freely:
// schemaTypes/index.ts
import {definePage, defineImage, defineCta, defineRichText} from './presets'
import {hero} from './hero'
export const schemaTypes = [
definePage({
name: 'page',
title: 'Page',
// Reference types by name, or inline a preset instance directly.
// See "Inline vs named types".
pageBuilderBlocks: ['hero', 'imageBlock', 'cta', 'richText'],
}),
hero,
defineImage({name: 'imageBlock', title: 'Image'}),
defineCta({name: 'cta', title: 'Call to action'}),
defineRichText({name: 'richText', title: 'Rich text'}),
]The custom hero type is modelled by hand with defineType, the same as any non-preset type:
// schemaTypes/hero.ts
import {defineField, defineType} from 'sanity'
export const hero = defineType({
name: 'hero',
title: 'Hero',
type: 'object',
fields: [
defineField({name: 'heading', title: 'Heading', type: 'string'}),
defineField({name: 'body', title: 'Body', type: 'text', rows: 3}),
],
})3. Wire the schema into your config
Import the assembled array and pass it to schema.types:
// sanity.config.ts
import {defineConfig} from 'sanity'
import {schemaTypes} from './schemaTypes'
export default defineConfig({
projectId: 'your-project-id',
dataset: 'production',
schema: {
types: schemaTypes,
},
})That's a complete setup. From here, read Concepts to understand the registry and composition, or Usage for the options each preset accepts.
Concepts
Registry
The presets registry is the entry point to @sanity/presets. Call createPresetsRegistry() to get a set of define<Type> functions that produce schema types.
The registry serves two purposes:
Global configuration. Presets can be configured at the registry level, providing defaults that apply everywhere a preset is used. For example, configuring
link.toonce means every link — whether standalone, inside a CTA, or inside rich text — knows which document types are available for internal links.Composition. Some presets compose other presets internally. The CTA preset includes a link field; the page preset includes SEO fields. The registry ensures these composed presets share the same global configuration.
const {defineLink, defineCta, definePage} = createPresetsRegistry({
link: {
// Every link in this registry — standalone, inside CTAs,
// inside rich text — will offer these types for internal links.
to: ['marketingPage', 'blogPost'],
},
})Global configuration can be overridden at the call site. If a specific link instance needs different internal types, pass them directly:
defineLink({
name: 'specialLink',
// Overrides the registry-level to for this instance only.
to: ['product'],
})Inline vs named types
Only definePage produces a document type. The other presets (defineLink, defineCta, defineSeo, defineImage, defineRichText) produce object and array types — building blocks meant to live inside other types, not standalone documents. There are two ways to place them:
Inline — call the preset directly where the type is used: inside a custom type's
fields, or in a page'spageBuilderBlocks. This is the default; reach for it unless you have a reason not to.defineType({ name: 'blockquote', type: 'object', fields: [ defineField({name: 'quote', type: 'text'}), // An inline link, defined right where it's used. defineLink({name: 'source', title: 'Source'}), ], })Named — register the preset once in
schema.typeswith aname, then reference it elsewhere by that name string. Reach for this when you want one standardized definition reused in several places: define it once, and every reference stays in sync.// In schema.types, register a reusable CTA: defineCta({name: 'cta', title: 'Call to action'}) // Elsewhere, refer to it by name: pageBuilderBlocks: ['cta'] fields: [defineField({name: 'cta', type: 'cta'})]
Default to inline; promote a preset to a named type only when reuse calls for it.
Composition
Presets can compose other presets. This means configuring one preset can affect others that depend on it.
The link preset is the clearest example. When you configure link.to at the registry level, that configuration cascades to:
- CTA (call to action) — the CTA preset includes an inline link field. The link field automatically uses the registry-level
toconfiguration. - Rich text — the rich text preset includes link annotations. Those annotations also use the registry-level
toconfiguration.
This means you configure link behaviour once, and every preset that uses links inherits that configuration automatically.
Map hooks
Every preset accepts a map option containing map hooks — functions that receive the produced schema type and return a modified version.
Map hooks exist as an escape hatch. They give you full control over the schema type a preset produces, including the ability to reorder, rename, or remove fields.
import {defineField} from 'sanity'
definePage({
name: 'marketingPage',
title: 'Marketing Page',
map: {
// Prepend a "Subtitle" field before all other fields.
fields: (fields = []) => [
defineField({
name: 'subtitle',
title: 'Subtitle',
type: 'string',
group: 'main',
}),
...fields,
],
},
})Each hook receives the value from the produced schema type (after any fields, groups, or other options have been applied) and must return a compatible value.
Use map hooks carefully. They have the final say in the produced schema type, which means they can unintentionally break a preset's intended functionality. A few guidelines:
- If you find yourself heavily rewriting the produced schema type with map hooks, it may be a sign that you should model the content type yourself using
defineTypeanddefineFielddirectly. See the schema type documentation for more. - If you use map hooks to rename fields, existing documents may need to be migrated. See the schema and content migrations documentation.
Usage
Page
The page preset produces a document type designed for page building. It includes fields for a page name, slug, content (an array of page-builder blocks), and SEO metadata.
definePage({
name: 'marketingPage',
title: 'Marketing Page',
pageBuilderBlocks: [
defineImage({name: 'imageBlock', title: 'Image'}),
defineRichText({name: 'richText', title: 'Rich text'}),
],
})Each entry in pageBuilderBlocks is either an inline schema type definition — typically a preset instance, as above — or a string referencing a type defined elsewhere in your schema (see Use presets alongside custom types). Mix both freely.
Rich text presets work in pageBuilderBlocks, both inline (defineRichText({...})) and by name ('richText'). Documents store each rich text block under content[].content.
Fields:
| Field | Type | Group | Description |
|---|---|---|---|
name | string | Main | The page's display name. Required. |
slug | slug | Main | URL-friendly slug, sourced from name. |
content | array | Main | Page builder blocks. Types are specified via pageBuilderBlocks. |
seo | object | Metadata | SEO fields (title, description, Open Graph image). Composed from the SEO preset. |
Groups: Main (default), Metadata.
Options:
| Option | Type | Description |
|---|---|---|
pageBuilderBlocks | PageBuilderBlock[] | Type names or inline schema type definitions to include in the content array. |
fields | FieldDefinition[] | Additional fields to append. |
groups | FieldGroupDefinition[] | Additional groups to append after the defaults. |
The page preset is not the only way to create page documents. It provides opinionated defaults to get started quickly. For specialised page types, use defineType directly. See the document type documentation.
Link
The link preset produces an object type for internal and external links. It includes fields for the link type (internal or external), a reference field for internal links, a URL field for external links, and an "open in new tab" option.
defineLink({
name: 'primaryLink',
title: 'Primary Link',
// Document types available for internal links. Falls back to
// the registry-level link.to if not provided here.
to: ['page', 'post'],
})Fields:
| Field | Type | Description |
|---|---|---|
linkType | string | "Internal" or "External". Defaults to "Internal". |
reference | reference | Internal link. Hidden when link type is external. Targets configured via to. |
url | url | External URL. Hidden when link type is internal. Validates http, https, mailto, and tel schemes. |
openInNewTab | boolean | Whether to open in a new tab. Hidden for internal links. |
Options:
| Option | Type | Description |
|---|---|---|
to | (string | {type: string})[] | Document types available for internal links. Accepts string shorthand (['page']) or object form ([{type: 'page'}]). Falls back to the registry-level link.to. |
CTA (call to action)
The CTA preset produces an object type for call-to-action elements. It includes an inline link field (composed from the link preset) and a level selector for semantic importance.
defineCta({
name: 'heroCta',
title: 'Hero CTA',
})Fields:
| Field | Type | Description |
|---|---|---|
link | object | An inline link, composed from the link preset. Inherits to from the registry. |
level | number | Semantic importance level (1, 2, or 3). |
SEO (search engine optimization)
The SEO preset produces an object type for search engine metadata. It includes fields for a title, description, and Open Graph image with a recommended size of 1200×630.
defineSeo({
name: 'metadata',
title: 'Metadata',
group: 'metadata',
})Fields:
| Field | Type | Description |
|---|---|---|
title | string | Page title for search engines. Shows an info note past 70 characters, where search engines may truncate it. |
description | text | Meta description. Shows an info note past 150 characters, where search engines may truncate it. |
ogImage | image | Open Graph image. Shows a warning when dimensions aren't exactly 1200×630; recommended, not enforced. |
The SEO preset is also composed into the page preset, where it appears as an inline object field in the Metadata group.
Image
The image preset produces an image type with optional alt text and caption fields. Hotspot is an option on the type itself; alt text and caption are added as fields. It includes built-in preview configuration.
defineImage({
name: 'heroImage',
title: 'Hero Image',
altText: true,
caption: false,
hotspot: true,
})Fields:
| Field | Type | Description |
|---|---|---|
altText | string | Alt text for accessibility. Enabled by default. Shows a warning encouraging alt text. |
caption | text | Image caption. Enabled by default. |
Options:
| Option | Type | Default | Description |
|---|---|---|---|
altText | boolean | true | Include the alt text field. |
caption | boolean | true | Include the caption field. |
hotspot | boolean | true | Enable image hotspot. |
Rich text
The rich text preset produces a Portable Text array type with link annotations, image blocks, and inline call-to-action (CTA) objects. All three embedded objects are on by default.
defineRichText({
name: 'body',
title: 'Body',
})Embedded objects:
| Object | Type | Description |
|---|---|---|
link | Portable Text annotation | Adds the link preset as an inline annotation on blocks. |
image | Array member | Adds the image preset as a block-level member of the array. |
cta | Inline object | Adds the CTA preset as an inline object within blocks. |
Options:
| Option | Type | Default | Description |
|---|---|---|---|
objects | boolean | {link?: boolean, image?: boolean, cta?: boolean} | true | Toggles embedded objects on or off. Pass false to disable all, or an object per entry. |
To disable all embedded objects for a plain Portable Text field:
defineRichText({
name: 'plainBody',
objects: false,
})To disable individual embedded objects:
defineRichText({
name: 'body',
objects: {cta: false},
})Configuring the embedded presets
Configure each object's options (link to, image altText, and so on) at the registry level. Those options cascade into every rich text field:
const {defineRichText} = createPresetsRegistry({
link: {
to: ['page', 'post'],
},
image: {
altText: true,
caption: false,
},
})The objects option only toggles embedded objects on or off. To reshape the array members on a specific rich text field, use the map hooks escape hatch.
Recommended patterns
Configure links globally
The link preset is used by multiple other presets (CTA, rich text). Configure to at the registry level so all links share the same set of linkable document types:
const {defineLink, defineCta, definePage} = createPresetsRegistry({
link: {
to: ['page', 'post', 'product'],
},
})This single configuration flows into every defineLink, defineCta, and defineRichText call from this registry. Override at the call site only when a specific instance needs different behaviour.
Use presets alongside custom types
Presets are not intended to replace all content modelling. They provide opinionated defaults for common patterns — pages, links, images, metadata — but your schema will likely include custom types that are specific to your project.
Custom types and presets work well together. You can use presets inside custom types, and reference custom types from presets:
import {defineType, defineField} from 'sanity'
// A custom "blockquote" type that includes a link preset.
// This type must be added to schema.types alongside your presets.
defineType({
name: 'blockquote',
title: 'Blockquote',
type: 'object',
fields: [
defineField({
name: 'quote',
title: 'Quote',
type: 'text',
}),
defineField({
name: 'author',
title: 'Author',
type: 'string',
}),
defineLink({
name: 'source',
title: 'Source',
}),
],
})When a preset references a custom type — for example, passing pageBuilderBlocks: ["blockquote"] to definePage — that type must be defined in your schema. Presets don't create these types for you.
Extend presets with fields and groups
Rather than reaching for map hooks, use the fields and groups options to extend a preset. Fields and groups added this way are appended after the preset's own fields and groups:
definePage({
name: 'blogPost',
title: 'Blog Post',
pageBuilderBlocks: [defineRichText({name: 'richText'}), defineImage({name: 'imageBlock'})],
groups: [{name: 'settings', title: 'Settings'}],
fields: [
defineField({
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
group: 'metadata',
}),
defineField({
name: 'featured',
title: 'Featured',
type: 'boolean',
group: 'settings',
}),
],
})Reserve map hooks for when you need full control
The fields option is sufficient for adding new fields to a preset. Use map hooks when you need full control over the produced schema type — for example, reordering or wrapping existing fields:
import {defineField} from 'sanity'
definePage({
name: 'marketingPage',
title: 'Marketing Page',
map: {
// Prepend a "Subtitle" field before all other fields.
fields: (fields = []) => [
defineField({
name: 'subtitle',
title: 'Subtitle',
type: 'string',
group: 'main',
}),
...fields,
],
},
})A few guidelines for using map hooks:
- If you find yourself heavily rewriting the produced schema type with map hooks, it may be a sign that you should model the content type yourself. See the schema type documentation for more.
- If you use map hooks to rename fields, existing documents may need to be migrated to reflect the new field names. See the schema and content migrations documentation.