Schema Design Patterns en Sanity.io: Cómo Construir Schemas que Sobreviven a Tus Migraciones
Aprende los schema design patterns en Sanity.io para crear estructuras reutilizables, escalables y que minimizan el dolor de migraciones en tu headless CMS.
El 80% de los Schemas en Sanity Son Técnico Debt desde el Día Uno
Copias el schema de tu último proyecto. Pegas. Modificas cuatro campos. Funciona.
Seis meses después, el cliente quiere un tipo de contenido nuevo. Empiezas a copiar código. Los campos duplicados se multiplican. Cuando necesitas cambiar algo global, editas en cinco lugares distintos.
*El problema real no es que Sanity sea flexible. Es que nadie te enseñó a diseñar schemas como un sistema, no como documentos aislados.*
Este es el patrón de diseño que separa proyectos que escalan de los que se convierten en legacy antes de llegar a producción.
Por Qué Tus Schemas Se Convierten en Caos
La mayoría de developers trata cada schema como un documento independiente. Defines `post.ts`, defines `author.ts`, defines `page.ts`. Cada uno con sus campos, sus validaciones, sus referencias.
Funciona para proyectos pequeños. Falla en tres escenarios:
1. Contenido que comparte estructura pero no tipo
Un blog post y un artículo del manual tienen título, fecha, imagen principal, cuerpo y autor. Pero son documentos distintos. La solución naive es duplicar campos en ambos schemas.
2. Cambios que deben propagarse a múltiples tipos
Necesitas añadir un campo `featured` a doce tipos de contenido. Editas doce archivos. Si te olvidas de uno, tienes inconsistencia en producción.
3. Referencias circulares que rompen validación
Un `product` referencia `category`, que referencia `collection`, que referencing `product`. Sanity no tiene problema con esto a nivel de datos. Pero tu código de frontend se convierte en un mapa de referencias que nadie entiende.
La arquitectura de schemas en Sanity tiene herramientas para resolver esto. Casi nadie las usa.
El Patrón de Objetos Reutilizables
Sanity distingue entre `document` y `object`. Un document es un tipo de contenido raíz. Un object es una estructura reutilizable que puedes embeber en múltiples documentos.
Este es el cambio mental más importante: no todo necesita ser un document.
Step 1: Extrae campos comunes a object types
Crea un object type para todo campo que repitas en más de un documento:
```javascript
// schemas/objects/seo-meta.js
export default {
name: 'seoMeta',
title: 'SEO Meta',
type: 'object',
fields: [
{
name: 'title',
title: 'Meta Title',
type: 'string',
validation: Rule => Rule.max(60).warning('Más de 60 caracteres puede truncarse en SERPs')
},
{
name: 'description',
title: 'Meta Description',
type: 'text',
rows: 3,
validation: Rule => Rule.max(155).warning('Más de 155 caracteres puede truncarse')
},
{
name: 'ogImage',
title: 'Open Graph Image',
type: 'image',
options: { hotspot: true }
}
]
}
```
Ahora cualquier documento puede incluir SEO sin duplicar lógica:
```javascript
// schemas/documents/blog-post.js
export default {
name: 'blogPost',
title: 'Blog Post',
type: 'document',
fields: [
{ name: 'title', type: 'string', validation: Rule => Rule.required() },
{ name: 'slug', type: 'slug', options: { source: 'title' } },
{ name: 'seo', type: 'seoMeta' }, // ← reutilizable
{ name: 'body', type: 'blockContent' }
]
}
```
Step 2: Usa referencias planas, no anidadas
❌ MAL — Anidado profundo
```javascript
fields: [
{
name: 'author',
type: 'object',
fields: [
{ name: 'name', type: 'string' },
{ name: 'avatar', type: 'image' }
]
}
]
```
✅ BIEN — Referencia a documento
```javascript
fields: [
{
name: 'author',
type: 'reference',
to: [{ type: 'author' }]
}
]
```
Las referencias planas permiten editar el autor una vez y ver el cambio en todos los posts. Los objetos anidados duplican datos y rompen la consistencia.
El Patrón de Composición con Fieldsets
Cuando un schema crece, la interfaz del Studio se vuelve difícil de navegar. Sanity tiene `fieldsets` para agrupar campos visualmente.
```javascript
// schemas/documents/product.js
export default {
name: 'product',
title: 'Producto',
type: 'document',
fieldsets: [
{ name: 'main', title: 'Información Principal', options: { collapsed: false } },
{ name: 'media', title: 'Imágenes y Multimedia' },
{ name: 'inventory', title: 'Inventario' }
],
fields: [
{
name: 'name',
type: 'string',
fieldset: 'main'
},
{
name: 'slug',
type: 'slug',
fieldset: 'main'
},
{
name: 'mainImage',
type: 'image',
fieldset: 'media',
options: { hotspot: true }
},
{
name: 'gallery',
type: 'array',
of: [{ type: 'image' }],
fieldset: 'media'
},
{
name: 'stock',
type: 'number',
fieldset: 'inventory'
}
]
}
```
Esto organiza la edición en el Studio sin cambiar la estructura de datos. Un editor ve secciones colapsables lógicas en lugar de una lista infinita de campos.
El Patrón de Validation Centralizada
Cada campo puede tener validation rules. Pero cuando necesitas cambiar una regla globalmente (por ejemplo, "todos los slugs deben tener máximo 50 caracteres"), terminas editando múltiples archivos.
La solución es crear validation helpers reutilizables:
```javascript
// lib/validation-rules.js
export const slugValidation = {
max: 50,
min: 3,
warning: 'Un slug óptimo tiene entre 3 y 50 caracteres'
}
export const requiredString = {
validation: Rule => Rule.required()
}
export const urlValidation = {
validation: Rule => Rule.uri({ allowRelative: true })
}
```
Aplícalos en tus schemas:
```javascript
import { slugValidation, requiredString } from '../../lib/validation-rules'
export default {
name: 'page',
title: 'Página',
type: 'document',
fields: [
{
name: 'title',
title: 'Título',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title', maxLength: slugValidation.max },
validation: Rule => Rule.required().max(slugValidation.max)
}
]
}
```
Cambiar la regla globalmente significa editar un archivo.
El Patrón de Migración Sin Dolor
El mayor miedo al cambiar schemas es romper contenido existente. Sanity tiene una estrategia: nunca borres campos, solo añádelos y désactivalos.
Estrategia de deprecación segura
❌ RIESGO ALTO — Eliminar campo
```javascript
fields: [
// Se fue. Todo el contenido en ese campo se pierde.
// Si tenías 500 posts con datos, los pierdes.
]
```
✅ ENFOQUE SEGURO — Deprecation workflow
```javascript
fields: [
{
name: 'oldCategory',
title: 'Categoría (deprecated)',
type: 'string',
hidden: ({ document }) => true, // Oculto en Studio
description: 'Este campo será removido en la versión 2.0. Usa "category" en su lugar.'
},
{
name: 'category',
title: 'Categoría',
type: 'reference',
to: [{ type: 'category' }]
}
]
```
`hidden` mantiene el campo activo en la base de datos para queries legacy, pero no lo muestra a los editores. Puedes hacer migrate data con un script antes de eliminarlo completamente.
Script de migración con Sanity CLI
```bash
Ejecuta un script de migración sobre tu dataset
sanity exec migrations/migrate-category.js --with-user-token
```
```javascript
// migrations/migrate-category.js
import { getCliClient } from 'sanity/cli'
const client = getCliClient()
export async function migrateCategory() {
const query = '*[_type == "blogPost" && defined(oldCategory)][0...100]'
const posts = await client.fetch(query)
for (const post of posts) {
// Busca o crea la categoría
const categoryQuery = '*[_type == "category" && title == $title][0]'
let category = await client.fetch(categoryQuery, { title: post.oldCategory })
if (!category) {
category = await client.create({
_type: 'category',
title: post.oldCategory
})
}
// Actualiza el post con la referencia
await client.patch(post._id).set({
category: { _type: 'reference', _ref: category._id },
oldCategory: undefined
}).commit()
console.log(`Migrated: ${post._id}`)
}
}
```
Este script migra datos de `oldCategory` (string) a `category` (reference). Ejecutas, verificas, y luego puedes eliminar el campo sin miedo.
El Patrón de Schemas Modular
La mejor práctica para proyectos que escalan: organza tus schemas en carpetas lógicas y exporta desde un index central.
```
schemas/
├── index.js
├── documents/
│ ├── post.js
│ ├── page.js
│ ├── product.js
│ └── author.js
├── objects/
│ ├── seo-meta.js
│ ├── portable-text.js
│ ├── link.js
│ └── image-with-caption.js
└── components/
└── inputs/
└── custom-slug-input.js
```
```javascript
// schemas/index.js
import post from './documents/post'
import page from './documents/page'
import product from './documents/product'
import author from './documents/author'
import seoMeta from './objects/seo-meta'
import portableText from './objects/portable-text'
import link from './objects/link'
export const schemaTypes = [
// Documents
post,
page,
product,
author,
// Objects
seoMeta,
portableText,
link
]
```
Añadir un nuevo tipo de contenido significa crear un archivo y añadirlo al array. Sin magia, sin configuración oculta.
Conclusión
La diferencia entre un proyecto Sanity que escala y uno que se convierte en legacy no es el presupuesto ni la complejidad. Es si diseñaste tus schemas como un sistema o como documentos independientes.
Los patterns que funcionan:
→ Object types para campos reutilizables
→ Referencias planas sobre objetos anidados
→ Fieldsets para organizar la interfaz del Studio
→ Validation helpers centralizados
→ Deprecación gradual sobre borrado agresivo
→ Migraciones con scripts, no a mano
Tu próximo schema debería tomar 15 minutos más en diseñarse. Te ahorrará 15 horas cuando el proyecto crezca.
*La pregunta no es si necesitas un schema reutilizable. Es si quieres migrar o si quieres escalar.*
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

