Sanity.io Headless CMS Tutorial: No es un CMS. Es una Infraestructura de Contenido en Tiempo Real
Tutorial completo de Sanity.io headless CMS. Descubre por qué no es un CMS tradicional y aprende el framework de 5 pasos para usarlo como infraestructura de contenido con GROQ y Portable Text.
Sanity.io No es un CMS. Es una Infraestructura de Contenido Mal Etiquetada
El 90% de los que empezáis con Sanity lo tratáis como "otro headless CMS".
Evaluáis precios. Comparáis con Contentful. Migráis desde WordPress. Y luego modeláis el contenido como si fuera SQL.
*Y os perdéis exactamente lo que hace a Sanity revolucionario. *
El error no es técnico. Es conceptual. Sanity no es un panel de administración con API. Es una plataforma de infraestructura de contenido en tiempo real — un Content Lake consultable con GROQ, un formato de texto estructurado (Portable Text) que ni HTML ni Markdown pueden igualar, y un Studio React que tú controlas.
La mayoría lo configura como una base de datos relacional disfrazada de CMS. Y luego se preguntan por qué no notan la diferencia.
Voy a mostraros el framework de 5 pasos para usarlo como toca.
Por Qué el Modelo Mental de "Base de Datos" Te Está Frenando
Si vienes de WordPress o Strapi, piensas en contenido como páginas. Un post tiene un título, un cuerpo HTML, una imagen destacada. Fin.
Sanity no funciona así. Sanity almacena contenido como documentos JSON estructurados en un Content Lake vivo. No hay tablas. No hay filas. No hay JOINs como los conoces.
*El error más común: normalizar los esquemas como si fueras a meterlos en PostgreSQL. *
En SQL, desnormalizar es pecado. En Sanity, desnormalizar es el patrón esperado. Porque GROQ —el lenguaje de consultas de Sanity— te permite atravesar referencias, proyectar arrays y filtrar por tipo de documento en una sola petición.
Lo que en REST serían 3 o 4 endpoints separados, en GROQ es una query.
❌ Enfoque WordPress: Creas un post type "blog", otro "author", y otro "comment". Luego haces 3 peticiones API para montar la página.
✅ Enfoque Sanity: Una query GROQ que en un solo viaje te da el post, el autor (resuelto desde la referencia), y los primeros 3 comentarios. Un viaje de ida y vuelta al servidor.
El Framework de 5 Pasos para Usar Sanity Como Toca
He visto decenas de proyectos con Sanity. Los que funcionan siguen este patrón. Los que fracasan lo ignoran.
Llamadlo El Método del Content Lake: cinco pasos para que Sanity no sea un CMS caro, sino la espina dorsal de tu contenido.
1. Modela como Datos Estructurados, No como Páginas
Tu esquema no debería reflejar cómo se ve una página. Debería reflejar qué es cada cosa.
```javascript
// ❌ Mal: modelas una página de blog como un blob
export default {
name: 'blogPost',
type: 'document',
fields: [
{ name: 'title', type: 'string' },
{ name: 'body', type: 'blockContent' }, // Portable Text genérico
{ name: 'authorName', type: 'string' }, // Texto plano, no referencia
]
}
// ✅ Bien: modelas el contenido como objetos reutilizables
export default {
name: 'blogPost',
type: 'document',
fields: [
{ name: 'title', type: 'string' },
{ name: 'body', type: 'blockContent' },
{ name: 'author', type: 'reference', to: [{ type: 'person' }] },
{ name: 'relatedProducts', type: 'array', of: [{ type: 'reference', to: [{ type: 'product' }] }] },
{ name: 'seo', type: 'seoMetadata' }, // Tipo reutilizable
]
}
```
Cada tipo de documento debe tener un propósito. Persona, producto, categoría — no "página de inicio" como un cajón desastre.
2. Domina GROQ — Es Tu Arma Secreta
GROQ no es GraphQL. No es REST. Es un lenguaje de consultas diseñado para documentos JSON con referencias.
Esto es lo que no puedes hacer con REST tradicional:
```groq
// Una sola query: posts, autores, productos relacionados, comentarios
*[_type == 'blogPost' && publishedAt < now()] | order(publishedAt desc) [0...10] {
title,
slug,
"author": author->{name, avatar},
"relatedProducts": relatedProducts[]->{title, price, slug},
"comments": *[_type == 'comment' && references(^._id)] | order(_createdAt desc) [0...3] {
text,
"user": user->name
},
body
}
```
Esa query hace en un viaje lo que un backend REST necesitaría 4 endpoints para servir:
1. Listar posts
2. Resolver el autor de cada post
3. Resolver productos relacionados
4. Resolver comentarios de cada post
Y todo eso con proyecciones selectivas — no traes campos que no necesitas.
*El truco: el operador `->` resuelve referencias en la misma query. * En REST tendrías que hacer un fetch por cada referencia. En GROQ, es sintaxis nativa.
3. Portable Text No es un Editor Rico. Es un Formato Estructural
La mayoría trata Portable Text como "el editor WYSIWYG de Sanity".
Error. Portable Text es un formato JSON de bloques que representa texto enriquecido como un array de objetos tipados. Cada bloque puede ser un párrafo, un encabezado, una lista, o —y aquí está la potencia— un tipo personalizado.
```javascript
// Portable Text renderer en React con tipos personalizados
import { PortableText } from '@portabletext/react'
const components = {
types: {
tweetEmbed: ({ value }) => (
<TweetEmbed tweetId={value.id} theme={value.theme} />
),
mathEquation: ({ value }) => (
<MathJaxRenderer latex={value.latex} />
),
productLink: ({ value }) => (
<ProductCard productId={value.productId} />
),
},
marks: {
link: ({ children, value }) => {
// El mismo bloque se renderiza como:
// - <a> en web
// - Navigation en React Native
// - Enlace de atribución en email
return <a href={value.href} target={value.blank ? '_blank' : '_self'}>{children}</a>
}
}
}
function BlogPost({ content }) {
return <PortableText value={content} components={components} />
}
```
¿Por qué esto es mejor que Markdown o HTML?
Porque el mismo contenido se renderiza de forma distinta en cada plataforma sin parsear HTML con regex. Un tweet embebido en web es un `<TweetEmbed />`. En mobile, es un `TweetView` nativo de SwiftUI. En email, es un enlace estático. Los datos son los mismos. El renderizado cambia.
4. Construye un Studio a Tu Medida
El Studio de Sanity no es un admin genérico. Es una aplicación React que despliegas tú y que puedes extender por completo.
Añade componentes de previsualización en vivo. Valida reglas entre documentos. Construye selectores visuales que tu equipo editorial necesita y que ningún CMS de serie ofrece.
```javascript
// Componente de input personalizado: un selector de color visual
import { defineField } from 'sanity'
export const colorPickerInput = defineField({
name: 'brandColor',
type: 'string',
components: {
input: ({ value, onChange }) => (
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<input
type="color"
value={value || '#000000'}
onChange={e => onChange(e.target.value)}
style={{ width: '48px', height: '48px', padding: 0, border: 'none' }}
/>
<input
type="text"
value={value || ''}
onChange={e => onChange(e.target.value)}
placeholder="#000000"
/>
</div>
)
}
})
```
Cada customización que haces en el Studio es código React que controlas. El coste: mantenimiento. Cada versión mayor de Sanity puede romper tu Studio. Pero el beneficio —un equipo editorial que no te pide cambios porque ya tiene lo que necesita— lo compensa.
5. Usa el Listener API para Preview en Vivo (y CDN para Producción)
El Content Lake no es una base de datos dormida. Es un sistema en tiempo real con suscripciones mediante Operational Transform (OT) — el mismo mecanismo que usa Google Docs para colaboración simultánea.
```javascript
// Conexión en tiempo real con Sanity listener
import { createClient } from '@sanity/client'
const client = createClient({
projectId: 'tu-project-id',
dataset: 'production',
useCdn: true, // CDN para usuarios finales
apiVersion: '2026-07-19'
})
// Cliente de preview (sin CDN, con listener)
const previewClient = client.withConfig({ useCdn: false })
// En Next.js con App Router
export default async function Page({ params }) {
const query = `*[_type == 'page' && slug.current == $slug][0]{
title, body, "author": author->name
}`
// Para SSG/ISR: consulta con CDN
const data = await client.fetch(query, { slug: params.slug })
// Para preview en vivo: suscripción al listener
// (se ejecuta solo cuando el editor está en preview mode)
const subscription = previewClient.listen(
query,
{ slug: params.slug }
).subscribe((update) => {
// Actualiza el estado en tiempo real
console.log('Documento actualizado:', update.result)
})
return <PageRenderer data={data} />
}
```
*El patrón correcto: * en producción, CDN con cache. En preview, listener con suscripción en tiempo real. Nunca al revés.
Las 3 Objeciones Que Vas a Tener (y Por Qué no Son Mortales)
"GROQ es propietario — vendor lock-in"
GROQ es open source (MIT). Tiene implementaciones en Python, Go, Rust. Y Sanity también soporta GraphQL si prefieres el estándar.
*El riesgo real no es GROQ. Es Portable Text y las customizaciones del Studio. * Migrar desde Sanity requiere exportar documentos JSON y escribir un script que los convierta al formato de destino. No es trivial, pero tampoco es imposible.
"Sanity es overkill para un blog pequeño"
Es cierto. Para una web de 5 páginas con un editor, Sanity es excesivo. Usa flat-file CMS, Markdown, o incluso WordPress.
*Pero si tu proyecto va a crecer —múltiples plataformas, varios editores, contenido reutilizable— la inversión inicial en Sanity se amortiza. * El coste no es económico. Es de aprendizaje. Y ese coste lo pagas una vez.
"Las queries GROQ complejas son lentas"
GROQ con proyecciones profundas puede ser impredecible con miles de documentos. La solución: proyecta solo lo que necesitas. No hagas `{ ... }` que trae todo el documento. Y usa el export de dataset para ETL pesado.
La Línea de Meta
Sanity no es un CMS. Es infrastructure-as-code para contenido.
Si lo tratas como un WordPress con APIs, vas a tener un WordPress caro. Si lo tratas como un backend de contenido con un lenguaje de consultas propio, un formato de texto que se renderiza en cualquier plataforma, y un panel de administración que controlas al 100%, tienes algo que ningún otro CMS te da.
El contenido no son páginas. Son datos estructurados que esperan ser consultados, transformados y renderizados donde haga falta.
Modela así. Consulta con GROQ. Renderiza con Portable Text.
Y deja de llamar "CMS" a lo que es una infraestructura de contenido en tiempo real.
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

