Content Modeling Best Practices for Headless CMS
How to structure your content for maximum flexibility and reusability—lessons learned from building content architectures at scale.

Content Modeling Best Practices for Headless CMS
The difference between a good headless CMS implementation and a great one often comes down to content modeling. Get it right, and your content scales beautifully. Get it wrong, and you're refactoring for months.
Think in Blocks, Not Pages
The biggest mindset shift in headless CMS: content is not pages.
Instead of creating a "Homepage" content type with every field hardcoded, create reusable blocks:
// ❌ Bad: Page-centric model
const Homepage = {
heroTitle: "string",
heroSubtitle: "string",
heroImage: "image",
feature1Title: "string",
feature1Description: "string",
feature2Title: "string",
// ... endless fields
};
// ✅ Good: Block-based model
const HeroBlock = {
title: "string",
subtitle: "string",
image: "image",
cta: "reference(CTA)",
};
const FeatureBlock = {
title: "string",
description: "richText",
icon: "string",
};
const Page = {
title: "string",
slug: "slug",
blocks: "array(HeroBlock | FeatureBlock | ...)",
};
Normalize Your References
Don't duplicate data—reference it:
// ❌ Bad: Duplicated author data
const Post = {
title: "string",
authorName: "string",
authorBio: "string",
authorImage: "image",
};
// ✅ Good: Referenced author
const Author = {
name: "string",
bio: "text",
image: "image",
social: "object",
};
const Post = {
title: "string",
author: "reference(Author)",
};
Plan for Localization Early
Even if you're not multi-language now, structure for it:
// Sanity example with localization
const Post = {
title: {
type: "localeString", // Custom type
// Expands to: { en: 'string', es: 'string', fr: 'string' }
},
content: {
type: "localePortableText",
},
};
SEO as a Reusable Object
Create a shared SEO object used across content types:
const SEO = {
metaTitle: "string",
metaDescription: "text",
ogImage: "image",
noIndex: "boolean",
};
const Page = {
// ... other fields
seo: "object(SEO)",
};
const Post = {
// ... other fields
seo: "object(SEO)",
};
Content Relationships Matrix
Before building, map out your relationships:
| Content Type | References | Referenced By |
|---|---|---|
| Author | - | Post, Page |
| Category | - | Post |
| Post | Author, Category | Page (featured) |
| Page | Post, Author | - |
| CTA | - | HeroBlock, Page |
Validation Rules
Set constraints at the model level:
// Sanity validation example
const Post = defineType({
name: "post",
fields: [
{
name: "title",
type: "string",
validation: (Rule) => Rule.required().max(60),
},
{
name: "excerpt",
type: "text",
validation: (Rule) => Rule.required().max(160),
},
{
name: "slug",
type: "slug",
options: { source: "title" },
validation: (Rule) => Rule.required(),
},
],
});
The Migration Trap
Plan your content model thoroughly before importing content. Migrating a bad model with thousands of documents is painful.
My process:
- Sketch content types on paper
- Build in CMS with sample content
- Test all query patterns
- Import real content only when confident
Key Takeaways
- Blocks over pages: Maximum reusability
- Normalize references: Single source of truth
- Plan for i18n: Even if not needed now
- Validate early: Catch issues at input, not output
- Document everything: Future you will thank you










