Sanity.io Headless CMS Tutorial: El CMS que Construyes Tú Mismo (No al Revés)

Sanity.io Headless CMS Tutorial: El CMS que Construyes Tú Mismo (No al Revés)

Programación· 8 min de lectura

Sanity.io No Es un CMS. Es un Backend que Parece un CMS, y Eso Cambia Todo

Si piensas que Sanity.io es "un WordPress headless pero en React", te has tragado el marketing sin masticar.

*El 90% de los tutoriales te enseñan a configurar schemas, conectar un frontend y olvidarte. Y con eso pierdes lo único que hace a Sanity especial. *

Sanity no almacena contenido en tablas relacionales con foreign keys y joins. No te obliga a definir relaciones many-to-many antes de escribir una línea de contenido. No te da un panel admin fijo que aceptas o rompes.

Sanity es un content lake: una base de datos de documentos JSON almacenados en un único dataset, donde cada entrada es un blob con un _type y un _id. Y puedes consultarlos con GROQ, un lenguaje de consultas que es mitad GraphQL, mitad SQL, y enteramente diseñado para este modelo.

La trampa del "CMS headless": la etiqueta te hace pensar en gestión de contenido cuando deberías pensar en arquitectura de datos. Y esa diferencia define si tu proyecto escala o se convierte en un embudo de trabajo manual.

--

Por Qué "Headless CMS" es una Etiqueta que Vende Corto

La mayoría de plataformas headless (Contentful, Strapi, Prismic) funcionan igual: defines modelos de contenido, la API REST te devuelve JSON, y renderizas en el frontend.

Sanity hace lo mismo en la superficie. Pero bajo el capó el modelo es radicalmente distinto.

El Content Lake vs. Bases de Datos Relacionales

En WordPress o Drupal, un post tiene un autor (foreign key a una tabla de usuarios), categorías (tabla many-to-many con join), y extraer datos anidados requiere múltiples queries SQL o joins complejos.

En Sanity, un post es esto:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Un solo JSON. Sin tablas. Sin joins predefinidos. Las referencias se resuelven en tiempo de consulta con GROQ.

Ventaja: puedes añadir campos nuevos sin migraciones. Quieres un array de "featured products" en tu landing page? Lo añades al schema y ya está. No necesitas alter table, no necesitas migración, no necesitas downtime.

Trade-off: pierdes integridad relacional. No hay constraints de foreign key. Si borras un autor referenciado desde 20 posts, esos _ref se quedan colgando. La consistencia la gestionas tú en la capa de aplicación.

El Problema Real: la Gente Trata GROQ Como un REST API

Cuando empiezas con Sanity, lo natural es hacer peticiones REST a https://{project}.api.sanity.io/v202X/data/query/production?query=....

Y eso funciona. Pero es como conducir un Ferrari en primera.

GROQ no es un wrapper de SQL. Es un lenguaje declarativo que entiende la estructura de documentos y referencias de Sanity. Y permite hacer en una sola query lo que con REST requeriría 5 llamadas y un bucle en el frontend.

---

La Evidencia: GROQ y Portable Text Son lo que Justifica Sanity

He desplegado proyectos con Sanity para gestorías, directorios de profesionales y plataformas legales. En todos los casos, el factor diferencial no fue "tener un CMS bonito". Fueron GROQ y Portable Text.

GROQ: El Query Language que el 90% Ignora

GROQ soporta más de 20 tipos de filtros, proyecciones computadas, slice operators, y puede recorrer referencias a profundidad arbitraria.

Mira esta query:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Lo que hace esta query en una sola petición:

  1. Encuentra el post por slug
  2. Resuelve el autor (nombre, imagen, bio) mediante la referencia author->
  3. Resuelve todas las categorías (título, slug) mediante categories[]->
  4. Busca posts relacionados usando references(^._id) (posts que referencian al mismo autor o categorías)
  5. Extrae un excerpt de 200 caracteres del Portable Text usando pt::text()
  6. Limita a 5 resultados con [0..5]

Para conseguir lo mismo con REST, necesitarías:

  • ✅ Una petición para el post
  • ❌ Otra para el autor
  • ❌ Otra para las categorías
  • ❌ Una cuarta para los posts relacionados
  • ❌ Procesar el rich text en el frontend para extraer el excerpt
  • ❌ Lógica de filtrado para excluir el post actual

GROQ reduce 6 operaciones a 1. Y cada operación REST es latencia de red, carga en el servidor, y complejidad en el frontend.

Portable Text: El Formato que el Resto del Mundo No Tiene

Aquí está el truco que pocos explican.

La mayoría de headless CMS almacenan rich text como HTML en un string o Markdown. Esto funciona para web. Pero cuando necesitas renderizar en iOS nativo, Android, o una interfaz de voz, te tienes que tragar un parser HTML en cada plataforma. Y el HTML semántico se pierde.

Portable Text es diferente: almacena el contenido como un array de bloques tipados.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Cada bloque sabe si es un heading, un párrafo, una lista, un bloque de código. Los marks (negrita, cursiva, enlaces) son arrays de referencias a definiciones de marcado.

¿Por qué importa? Porque en React renderizas con @portabletext/react y un mapa de serializers. En Android renderizas mapeando cada _type block a un componente nativo. En una interfaz de voz, recorres los bloques, ignoras los code blocks, y lees los headings con énfasis. No parseas HTML. No limpias strings. Lees estructura.

Y Portable Text es open source (MIT). Puedes usarlo sin Sanity.

---

El Marco: Los 5 Movimientos para Usar Sanity Correctamente

He probado suficientes configuraciones para saber que el 90% de los proyectos fallan en los mismos sitios: schemas acoplados, queries enormes sin proyección, Portable Text tratado como HTML.

Aquí está el patrón de los 5 movimientos que uso en cada proyecto que despliego:

1. Schemas Atómicos, No Monolíticos

La mayoría define un schema "Post" con 40 campos: título, cuerpo, autor, categorías, SEO, metadatos, relatedPosts, featuredImage...

Error. Cada campo que añades a un schema aumenta la complejidad de las queries y la fragilidad del frontend.

Enfoque correcto: schemas pequeños y referencias.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Cada pieza de contenido es su propio tipo de documento. Las relaciones son referencias. Así GROQ puede proyectar exactamente lo que necesitas sin arrastrar campos muertos.

2. GROQ con Proyecciones, No con Select Estrella

Nunca hagas *[_type == "post"]. Siempre proyecta.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Cada campo extra que pides es ancho de banda y tiempo de serialización. En un proyecto con 4.800+ documentos, la diferencia entre una query con proyección y una sin ella es de cientos de milisegundos por petición.

3. Portable Text con Serializers Propios

El error más común: renderizar Portable Text como si fuera HTML, usando el serializador por defecto y ya está.

El poder real está en los serializers custom:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esto te permite:

  • Renderizar bloques de código con syntax highlighting
  • Incrustar YouTube embeds directamente desde el contenido
  • Añadir lazy loading a imágenes automáticamente
  • Personalizar estilos de headings por proyecto

4. Image API, No Carpeta de Assets

No subas imágenes optimizadas manualmente. Sanity tiene una Image API que transforma las imágenes sobre la marcha:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Puedes redimensionar, recortar, aplicar blur, cambiar formato (webp, avif), ajustar calidad... todo con parámetros URL.

En el frontend usas @sanity/image-url:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Sin pipeline de imágenes. Sin tareas de build. Sin ocupar espacio en el repo.

5. GROQ Query Inversa: El Frontend Pregunta, No Espera

Este es el patrón que marca la diferencia entre un proyecto ágil y uno lento.

En lugar de tener endpoints que devuelven datos preformateados, deja que el frontend haga queries GROQ directamente (desde el lado servidor, no expongas el token público si no quieres).

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Cada página pide exactamente lo que necesita. Sin over-fetching. Sin under-fetching. Sin capas de abstracción que añaden latencia.

---

Sí, Pero... (Objeciones Reales)

"GROQ es propietario. Me da miedo el lock-in."

GROQ es el lenguaje nativo de Sanity, pero Sanity también expone REST API y GraphQL API. GROQ es una herramienta de potencia, no una cadena. Y Portable Text es open source (MIT) — puedes exportar tu contenido como JSON y llevártelo a donde quieras.

"Sin constraints de foreign key, los datos se ensucian."

Es cierto. Sanity prioriza flexibilidad sobre integridad relacional. La solución: validación en los schemas. Usa validation: Rule => Rule.required().reference(), y si necesitas consistencia fuerte, escribe hooks de validación custom o un cron job que detecte referencias huérfanas.

El trade-off es aceptable para proyectos content-heavy donde la velocidad de iteración importa más que la rigidez de un schema SQL.

---

Lo Que Te Llevas

Sanity no es un CMS. Es un backend para contenido estructurado que resulta tener un panel de admin construido en React.

  • GROQ te permite hacer queries que reemplazan 5-6 peticiones REST con una sola llamada
  • Portable Text resuelve el problema de renderizado multi-plataforma sin parsear HTML
  • Schemas atómicos + proyecciones GROQ mantienen tu frontend rápido y tu backend flexible
  • Image API elimina la necesidad de pipelines de imágenes

El error no es elegir Sanity. Es tratarlo como un CMS headless más y perderte lo que realmente lo hace diferente.

La pregunta no es "¿Qué CMS uso?" Es "¿Quiero un gestor de contenido o una base de datos editorial que me deje consultar como quiera?"

GROQ es el superpoder que nadie usa. Y Portable Text es la arquitectura que no sabías que necesitabas hasta que tuviste que renderizar en una app nativa.

Ahora ve y escribe tu primer schema. Pero esta vez, piensa en documentos, no en tablas.

Artículos relacionados

---

¿Quieres recibir contenido como este cada semana? Suscríbete a mi newsletter

Brian Mena

Brian Mena

Ingeniero informatico construyendo productos digitales rentables: SaaS, directorios y agentes de IA. Todo desde cero, todo en produccion.

LinkedIn