Sanity.io Headless CMS Tutorial: No es un CMS. Es una Infraestructura de Contenido en Tiempo Real
Tutorial completo de Sanity.io headless CMS para 2026. Aprende GROQ, Portable Text y el patrón Content Lake con ejemplos reales de código React.
Si crees que Sanity.io es "otro headless CMS", lo estás evaluando en la categoría equivocada
El 90% de los tutoriales sobre Sanity.io empiezan igual: "alternativa a Contentful" o "Strapi pero con Studio personalizable".
*Es un error de categoría. *
Sanity no es un CMS que te dan hecho. Es una plataforma de infraestructura de contenido donde tú construyes el CMS. Donde el contenido vive en una base de datos en tiempo real con versionado, eventos y un lenguaje de consultas propio llamado GROQ.
Contentful te da un editor bonito y una API REST. Strapi te da un panel admin con SQL debajo.
Sanity te da un Content Lake — un almacén de datos versionado y event-driven — y te dice: "construye tu propio Studio, modela tu propio esquema, escribe tus propias queries".
La diferencia no es sutil. Es arquitectónica.
---
El problema que Sanity resuelve y que nadie más ha solucionado
Los headless CMS tradicionales tienen tres problemas estructurales:
1. Los datos relacionales en contenido apestan
En Contentful, si un artículo referencia a un autor, haces dos llamadas API. O metes Embed, y pierdes la integridad referencial.
En Strapi, las relaciones existen en SQL, pero la API REST te devuelve populates que o son demasiado profundos o demasiado planos. Nunca el punto justo.
Sanity modela el contenido como documentos con referencias. Cada tipo de contenido es un documento en el Content Lake. Una referencia (`_ref`) es un puntero a otro documento.
```js
// Esquema de artículo con referencias a autor y categoría
export default {
name: 'post',
type: 'document',
fields: [
{ name: 'title', type: 'string' },
{ name: 'slug', type: 'slug' },
{
name: 'author',
type: 'reference',
to: [{ type: 'author' }]
},
{
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }]
},
{
name: 'body',
type: 'blockContent' // Portable Text
}
]
}
```
Esto no es JSON anidado. Es una base de datos documental con relaciones.
2. El rich text te ata a un proveedor
Contentful tiene su propio renderizador de rich text. Strapi usa Markdown o HTML plano.
Si mañana quieres migrar, pierdes todo el formato.
Sanity creó Portable Text: una especificación abierta que almacena el contenido rich text como un array de bloques tipados. Cada bloque tiene un `_type`. Cada marca (negrita, cursiva, enlace) es un objeto en el array `marks`.
El contenido es portable. Puedes renderizarlo en React, Vue, Swift, o un cliente de terminal. Solo necesitas un serializer.
```js
// Renderizado de Portable Text en React con componentes custom
import { PortableText } from '@portabletext/react'
const components = {
types: {
image: ({ value }) => <img src={value.asset.url} alt={value.alt} />,
code: ({ value }) => (
<pre><code className={`language-${value.language}`}>{value.code}</code></pre>
),
callToAction: ({ value }) => (
<a href={value.url} className="cta">{value.text}</a>
)
},
marks: {
link: ({ children, value }) => (
<a href={value.href} target="_blank">{children}</a>
)
}
}
export function PostBody({ content }) {
return <PortableText value={content} components={components} />
}
```
Cada tipo de bloque se serializa con un componente React. Añades un bloque de tipo `callToAction` en el Studio, y el frontend lo renderiza como un enlace con clase `.cta`.
Sin HTML en strings. Sin Markdown parseado. Sin lock-in.
3. Las queries N+1 te matan
Quieres los últimos 10 artículos con su autor, categorías, y la imagen del autor.
En REST: 1 llamada para artículos + 10 llamadas para autores + 10 para categorías = 21 requests.
En GraphQL con Contentful: una query con nested resolvers que igual hace 21 llamadas internas por detrás.
En GROQ: una query.
```groq
*[_type == "post" && publishedAt < now()] | order(publishedAt desc) [0...10] {
title,
slug,
"author": author->{name, image, bio},
"categories": categories[]->{title, slug},
excerpt
}
```
El operador `->` es la magia: resuelve la referencia en la misma query. Un solo viaje de ida y vuelta al Content Lake. Sin N+1. Sin over-fetching. Sin under-fetching.
GROQ no es GraphQL. Es más cercano a MongoDB Aggregation Pipeline. Está diseñado específicamente para consultar documentos con referencias, no para exponer un grafo genérico.
---
❌ Sanity es overkill para un blog simple — el argumento en contra (y por qué falla)
Es la objeción más común: "solo tengo 5 páginas y un editor WYSIWYG. ¿Para qué me sirve Sanity?"
*Es una objeción justa... si no piensas escalar. *
El problema es que casi todo el mundo escala. Añades un segundo frontend (app móvil). O una versión AMP. O un asistente de voz. O necesitas reutilizar contenido en newsletters.
Con un CMS tradicional, ese día tienes que reescribir todo el modelo de contenido.
Con Sanity, el día 1 ya tienes contenido estructurado con referencias, tipos, y un esquema portable. Añadir un frontend nuevo es escribir un serializer de Portable Text. Nada más.
La pregunta no es si Sanity es overkill hoy. Es si quieres reescribir todo dentro de 6 meses.
---
El Patrón de 5 Capas para el Primer Proyecto en Sanity
Si vas a montar tu primer proyecto con Sanity, sigue este orden. No lo saltes. Cada capa depende de la anterior.
Capa 1: Modela con referencias, no con jerarquías
Diseña el esquema como si fuera una base de datos normalizada. Cada entidad es un tipo de documento separado. Usa `_ref` para las relaciones.
❌ Mal: Meter el autor como un campo de texto dentro del artículo.
✅ Bien: Crear un tipo `author` y referenciarlo desde `post`.
El beneficio llega cuando cambias el nombre del autor o su bio. Cambias en un sitio, se actualiza en todos los artículos. Con datos embebidos, tendrías que hacer un update masivo.
Capa 2: Consulta con GROQ, no con REST/GraphQL por defecto
Sanity expone REST y GraphQL. Pero GROQ es más expresivo para contenido.
```groq
// Productos con categoría, stock filtrado y precio formateado
*[_type == "product" && stock > 0] {
_id,
title,
"category": category->{title, slug},
price,
stock,
"primaryImage": images[0]->{url, alt}
}
```
Una query. Un viaje. Todos los datos que necesitas.
Capa 3: Convierte tu HTML/Markdown a Portable Text
Si vienes de WordPress o Strapi, tienes contenido en HTML o Markdown. Migrar a Portable Text no es trivial, pero vale la pena.
Usa `@sanity/block-tools` para convertir HTML a bloques Portable Text. O simplifica: escribe el contenido nuevo directamente como bloques tipados en el Studio.
El payoff: tu contenido rich text deja de ser una string opaca y pasa a ser datos consultables. Puedes hacer queries como "dame todos los párrafos que contengan la palabra 'GROQ'" porque cada bloque es un objeto en el array.
Capa 4: Customiza el Studio con desk structure y previews
El Studio de Sanity es una React app. Puedes reordenar el menú lateral, agrupar tipos, y añadir previews en vivo.
```js
// Desk structure personalizada
export const structure = (S) =>
S.list()
.title('Contenido')
.items([
S.listItem()
.title('Artículos')
.icon(PostsIcon)
.child(
S.documentList()
.title('Artículos')
.filter('_type == "post"')
),
S.divider(),
...S.documentTypeListItems().filter(
listItem => !['post'].includes(listItem.getId())
)
])
```
Añade previews que muestren cómo se verá el contenido en el frontend. Los editores dejan de adivinar cómo queda un artículo.
Capa 5: Conecta webhooks para invalidación en tiempo real
Cada cambio en el Content Lake emite un evento. Puedes escucharlo con un webhook para:
Regenerar ISR en Next.js.
Reindexar Algolia o Typesense.
Invalidar caché de CDN.
Disparar builds en Vercel.
```js
// Ejemplo: webhook handler para invalidar Next.js ISR
export async function handler(req) {
const body = JSON.parse(req.body)
if (body._type === 'post' && body.operation === 'publish') {
await fetch(`https://tu-dominio.com/api/revalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret: process.env.REVALIDATE_SECRET })
})
}
return { statusCode: 200 }
}
```
El editor pulsa "Publicar". En segundos, el frontend muestra el contenido actualizado. Sin polling. Sin refresco manual.
---
GROQ vs GraphQL: el debate que nadie tiene
La industria ha decidido que GraphQL es la respuesta para todo. Pero GraphQL fue diseñado para APIs de propósito general, no para contenido.
GROQ está diseñado para documentos. Para referencias. Para filtros sobre arrays.
❌ GraphQL en Sanity: Tienes que aprender a escribir queries GraphQL que Sanity traduce internamente a algo parecido a GROQ. Más abstracción, más latencia, más complejidad.
✅ GROQ: Aprendes una sintaxis específica para el dominio del contenido. Las queries son más cortas. Más legibles. Más rápidas.
La objeción: "GROQ es lock-in".
Realidad: GROQ es open source. La implementación está en GitHub. Sanity también expone REST y GraphQL. No empiezas con GROQ si no quieres. Lo adoptas cuando entiendes por qué es mejor.
El verdadero lock-in no son las queries. Es el contenido. Y Portable Text es una especificación abierta que migras a cualquier sistema que la soporte.
---
La arquitectura Content Lake no es una base de datos. Es un sistema de eventos.
Sanity llama "Content Lake" a su backend por una razón. No es PostgreSQL con una API encima.
Es un almacén de documentos:
Versionado: cada cambio guarda una revisión. Puedes hacer rollback a cualquier punto.
En tiempo real: abres un socket y recibes cambios al instante. Ideal para live preview.
Event-driven: cada `publish`, `unpublish`, `delete` emite un evento que encadenas a webhooks, CI/CD, o reindexados.
Esto cambia cómo piensas el flujo editorial. No es "edito, guardo, despliego". Es "edito, guardo, el sistema reacciona".
---
Lo que nadie te cuenta sobre el Studio
El Studio de Sanity es una SPA de React que se despliega donde quieras. No es un panel alojado por Sanity. Es tu app.
Puedes:
Añadir componentes de entrada custom (un color picker, un validador de URLs, un selector de iconos).
Cambiar el theme con CSS.
Añadir dashboards con métricas en tiempo real.
Ejecutarlo en local durante desarrollo y desplegarlo en Vercel para producción.
```js
// Componente de input custom para URLs con preview
import { FormField } from '@sanity/base/components'
import { TextInput, Stack, Card, Text } from '@sanity/ui'
export const UrlPreviewInput = React.forwardRef((props, ref) => {
const { value, onChange } = props
return (
<Stack space={2}>
<FormField {...props}>
<TextInput
ref={ref}
value={value}
onChange={event => onChange(event.target.value)}
placeholder="https://..."
/>
</FormField>
{value && (
<Card padding={2} tone="positive">
<Text size={1}>
Vista previa: <a href={value} target="_blank">{value}</a>
</Text>
</Card>
)}
</Stack>
)
})
```
El Studio es extensible porque es React. No necesitas aprender un DSL de plugins. Necesitas saber JSX.
---
Conclusión: el CMS que construyes tú mismo (no al revés)
El error más común es evaluar Sanity como "otro headless CMS" y compararlo con Contentful o Strapi en una tabla de features.
*La categoría correcta no es CMS. Es infraestructura de contenido. *
Sanity te da:
Un almacén de documentos versionado y event-driven (Content Lake).
Un lenguaje de consultas diseñado para contenido (GROQ).
Una especificación abierta para rich text portable (Portable Text).
Un Studio extensible que construyes con React.
Un sistema de eventos para orquestar despliegues y reindexados.
No es el CMS más fácil de configurar. No es el que menos código requiere.
Es el que te da control total sobre tu contenido sin sacrificar la experiencia del editor.
El resto de headless CMS te preguntan: "¿qué template quieres usar?"
Sanity te pregunta: "¿qué problema quieres resolver?"
Elige sabiamente.
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

