
Content Modeling Best Practices in Sanity
The core content modelling best practice in Sanity is to model what your content is, not how a particular page looks. Create document types for things that exist on their own and get reused (products, people, categories), use object types for groups of fields that only make sense inside something else (an address, an SEO block), connect documents with references instead of copying data, and name fields by meaning rather than presentation. Back that up with validation, clear descriptions and useful previews, and you get a model that serves your website today and any other channel tomorrow.
A good content model is the difference between a Studio editors enjoy and one they work around. It also decides how simple your GROQ queries are and how painful future redesigns will be. This guide covers the principles that hold up across real projects: thinking in content rather than pages, choosing between documents, objects and references, naming, page builders, taxonomies, editor experience, and how to change a model safely once content exists.
Start With Content, Not Pages
The most common mistake is to look at a design and create one document type per page, with fields for each box on the screen: heroTitle, leftColumnText, blueBanner. It works until the design changes, and then the model is wrong.
Instead, start by listing the things your organisation talks about:
- Write down the nouns: event, speaker, venue, ticket type, sponsor.
- For each one, list its attributes: an event has a title, dates, a venue and speakers.
- Note the relationships: events have many speakers, a speaker can appear at many events.
- Only then think about pages, which become views over that content.
A conference site modelled this way can show a speaker on the speaker page, the event page and the schedule without duplicating a single field. Your first schema can be small. How to create your first schema shows the mechanics.
Choose Documents, Objects and References Deliberately
| Use a… | When the content… | Example |
|---|---|---|
| Document | Exists on its own, is reused, or needs its own URL or lifecycle | author, product, category |
| Object | Only makes sense inside its parent and isn't shared | address, seo, openingHours |
| Reference | Points to a document that lives elsewhere | post.author, product.category |
| Array of objects | Is an ordered list owned by the parent | faqItems, pageSections |
A useful test: if two documents need the same data, it's a document and they reference it. If an author's bio lives inside each post as an object, updating the bio means editing every post. As a separate document, you edit it once. How to use references in Sanity schemas covers the details.
The opposite mistake is turning everything into a document. A post's SEO settings aren't reused and don't have a lifecycle of their own, so an inline object is simpler for editors and for queries.
Name Fields by Meaning
Field names become keys in your JSON and your frontend code. Choose names that describe the content:
| Avoid | Prefer | Why |
|---|---|---|
bigRedText | tagline | Survives a redesign |
leftImage | portrait | Describes the content, not the position |
text1, text2 | summary, body | Readable in queries |
isBlue | variant with options.list | Semantic, extensible |
postTitle on post | title | The type already gives context |
Some conventions that help across a project:
- camelCase for field and type names, matching JavaScript.
- Consistent common fields: always
title,slug,publishedAt, so queries and components can be shared. - Singular type names:
post, notposts, since each document is one item. - Never rename casually: a new name hides existing data, as the old key is still stored.
Keep Presentation Out of the Model, Mostly
Editors sometimes need a say in presentation, such as choosing a layout variant for a section. Give them semantic choices rather than raw styling:
defineField({
name: 'tone',
title: 'Tone',
type: 'string',
options: {
list: [
{ title: 'Default', value: 'default' },
{ title: 'Highlight', value: 'highlight' },
{ title: 'Muted', value: 'muted' },
],
layout: 'radio',
},
initialValue: 'default',
}),
Your frontend maps highlight to whatever the current design uses. Compare that with a free-text colour field, where editors type hex codes that no redesign can safely change.
Structure Over Blobs
Put meaningful data in fields, not inside rich text. If an event's date is typed into a paragraph, you can't sort events by date, show them in a calendar or hide past ones. Structured fields unlock all of that:
// schemaTypes/event.ts
import { defineField, defineType } from 'sanity';
export const event = defineType({
name: 'event',
title: 'Event',
type: 'document',
fields: [
defineField({ name: 'title', type: 'string', validation: (rule) => rule.required() }),
defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
defineField({ name: 'startsAt', type: 'datetime', validation: (rule) => rule.required() }),
defineField({ name: 'endsAt', type: 'datetime' }),
defineField({ name: 'venue', type: 'reference', to: [{ type: 'venue' }] }),
defineField({
name: 'speakers',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'person' }] }],
}),
defineField({ name: 'description', type: 'array', of: [{ type: 'block' }] }),
],
});
Now a single GROQ query can list upcoming events:
*[_type == "event" && startsAt > now()] | order(startsAt asc){
title,
"slug": slug.current,
startsAt,
"venue": venue->name,
"speakers": speakers[]->name
}
Rich text is still the right place for narrative content. Just don't hide data in it.
Build Reusable Pieces
Define shared structures once as named object types and reuse them:
// schemaTypes/objects/seo.ts
import { defineField, defineType } from 'sanity';
export const seo = defineType({
name: 'seo',
title: 'SEO',
type: 'object',
fields: [
defineField({ name: 'metaTitle', type: 'string', validation: (rule) => rule.max(60).warning() }),
defineField({ name: 'metaDescription', type: 'text', rows: 2, validation: (rule) => rule.max(160).warning() }),
defineField({ name: 'noIndex', type: 'boolean', initialValue: false }),
],
});
Then any document can use defineField({ name: 'seo', type: 'seo' }), and your frontend can use one component to render the meta tags. Building reusable object types goes deeper into this pattern.
Page Builders: Flexible but Bounded
Arrays of object types let editors compose pages from sections, such as a hero, a feature grid and a call to action. That's powerful, but unlimited flexibility leads to inconsistent pages. Keep page builders in check:
- Limit the menu: only offer section types the design actually supports.
- Keep sections small: each section should have a handful of fields, not dozens.
- Reference shared content: a testimonials section should reference
testimonialdocuments rather than retyping quotes. - Use page builders for landing pages: structured types like
postorproductusually don't need them. - Add good previews: give each section a preview so the array reads like an outline.
Model Taxonomies as Documents
Categories and tags are easier to manage as documents than as string lists:
- Editors can add new categories without a code change.
- Each category can have its own title, slug, description and image.
- Renaming a category updates everywhere, because posts only store a reference.
- You can query posts by category with
references().
*[_type == "category" && slug.current == $slug][0]{
title,
"posts": *[_type == "post" && references(^._id)]{ title, "slug": slug.current }
}
Use string with options.list only for fixed values that developers control, such as a status or layout variant.
Design for Editors
A model can be technically perfect and still frustrating to use. Studio gives you tools to fix that:
- Descriptions: explain what a field is for and how it's used on the site.
- Validation with clear messages: catch mistakes before publishing, and use warnings for advice. See how to add validation rules.
- Groups and fieldsets: split long forms into tabs such as Content, Media and SEO.
- Conditional fields: hide fields that don't apply, such as a sale price when a product isn't on sale.
- Initial values: pre-fill sensible defaults.
- Previews: show a meaningful title, subtitle and thumbnail in every list.
- Structure: organise the sidebar with
structureToolso editors find things quickly.
Sit with an editor while they create their first few documents. You'll learn more in 20 minutes than from any diagram.
Plan Early for Hard-to-Change Decisions
Some decisions are cheap to change later, and some aren't:
| Easy to change later | Hard to change later |
|---|---|
| Field titles and descriptions | Field and type name values |
| Validation rules | Object vs document (embedded vs referenced) |
| Groups, fieldsets and ordering | Localisation strategy (field-level vs document-level) |
| Adding new optional fields | Changing a field's type |
Decide on multilingual content, for example, before you have hundreds of documents. The multilingual content guide compares the approaches.
Change Models Safely
When you do need to restructure:
- Add, don't replace: create the new field alongside the old one.
- Migrate the data with a script, using
npx sanity migration createto scaffold a migration, or with the client. - Update the frontend to read the new field, ideally falling back to the old one during the transition.
- Remove or deprecate the old field with
deprecated: { reason: '...' }once everything has moved. - Test on a copy: run migrations against a staging dataset before production.
FAQ: Content Modelling in Sanity
Make it a document when it's reused across other content, needs its own URL, is managed independently or could be useful in other channels. Keep it as an object when it only makes sense inside its parent, like an address on a venue or SEO settings on a page.
Usually not. Model the underlying content types, such as products, people and articles, and let pages display them. For unique pages like a homepage or settings, a singleton document type is fine, but avoid fields named after positions in the current design.
They are for marketing and landing pages where editors need layout flexibility. Limit the available section types, keep each section focused and reference shared content. For structured content such as articles or products, a fixed set of fields is easier to maintain and query.
Model them as document types and connect them with references. Editors can then manage the list without code changes, each category can carry its own fields, and you can query all content in a category with the references function in GROQ.
Adding optional fields and changing labels, validation or layout is safe at any time. Renaming fields, changing types or switching between embedded and referenced content needs a data migration, so add the new structure first, migrate content, then retire the old field.
Detailed enough that important data lives in its own fields, so you can filter, sort and reuse it, but not so granular that editors fill in dozens of fields for simple content. Start with what the site needs now, and add structure when a real use case appears.
Conclusion
Good content modelling in Sanity is about meaning. Model the things your organisation deals with, choose documents for reusable content and objects for embedded detail, link them with references, and name fields for what they contain rather than where they appear. Put important data in structured fields, keep presentation choices semantic, and build reusable objects for patterns you repeat.
Then design for the people who use it every day: clear descriptions, helpful validation, tidy groups and good previews. Make the hard-to-change decisions early, change the rest freely, and migrate carefully when you need to. The result is a model that outlives your current design and makes every query simpler.


