Visual Editing en Sanity.io: Live Previews y Content Source Maps para Editores
Aprende a implementar Visual Editing en Sanity.io con live previews y content source maps. Framework de 3 capas para editores eficientes.
El 90% de los Equipos Implementa Visual Editing en Sanity Como si Fuera un Extra
Los editores abren Sanity Studio. Cambian un título. Guardan. Esperan. Revisan el frontend.
Repetir.
*La mayoría trata el Visual Editing como un plugin opcional, no como el flujo de trabajo central que define si un editor trabaja en 5 minutos o en 45.*
Si vuestros editores están pegando URLs de preview en nuevas pestañas, tenéis un problema arquitectónico. No un problema de configuración.
El Visual Editing real no es un toggle. Es una cadena de sistemas que trabajan juntos: live preview, content source maps, y resolve production doc.
Por Qué Vuestro Visual Editing Actual Es una Mentira Funcional
La mayoría de implementaciones de Visual Editing fracasan en un punto específico: separan el contenido del contexto visual.
Cuando un editor pulsa sobre un bloque en el frontend, espera que Sanity abra ese documento exacto en el Studio. No un flujo genérico. No un modal genérico. El documento específico que corresponde a ese componente en esa página.
❌ Lo que hace la mayoría:
Configuran un endpoint `/api/preview` genérico
Crean un component resolver que devuelve el primer documento que coincide
El editor pulsa en un botón de "Open in Studio" que abre la home del CMS
✅ Lo que debería hacer:
Content source maps que vinculan cada nodo en el frontend con su documento en Sanity
Un sistema de navegación bidireccional: Studio → Frontend Y Frontend → Studio
Resolvers que usan contexto de ruta para encontrar el documento exacto
El error más común es confundir "live preview" con "iframe con secret token". Live preview significa que cada cambio se refleja en tiempo real en el viewport del editor. Sin recargas. Sin cache. Sin fricción.
El Patrón de los Tres Nodos para Visual Editing Efectivo
La arquitectura de Visual Editing efectiva en Sanity sigue un patrón de tres nodos interconectados:
Nodo 1: Presentation Layer (El Frontend)
El frontend necesita exponer su estructura de componentes de forma que Sanity pueda entenderla. Esto se hace mediante content source maps, que son esencialmente un mapa que conecta cada elemento renderizado con su referencia en el schema de Sanity.
```typescript
// sanity.config.ts - Configuración del plugin de Visual Editing
import { defineConfig } from 'sanity'
import { visionTool } from '@sanity/vision'
import { visualEditing } from '@sanity/visual-editing'
export default defineConfig({
name: 'your-project',
title: 'Your Project',
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
plugins: [
visionTool(),
visualEditing({
// Habilita el overlay visual en el frontend
previewUrl: '/api/preview',
// Permite editar directamente en el viewport
liveQueryEnabled: true,
}),
],
})
```
```typescript
// _app.tsx - Configuración del proveedor de Visual Editing en Next.js
import { NextStudio } from 'next-sanity/studio'
import { VisualEditing } from 'sanity-plugin-visual-editing'
import config from '../../sanity.config'
export default function StudioPage() {
return (
<>
<NextStudio config={config} />
<VisualEditing
// Token para autenticación con el API de Sanity
token={process.env.SANITY_API_TOKEN}
// Frecuencia de sincronización en ms
refreshInterval={500}
/>
</>
)
}
```
El componente VisualEditing se monta una sola vez y permanece activo mientras el editor navega. Cada vez que se cambia de ruta, el componente sincroniza automáticamente el documento correspondiente.
Nodo 2: Resolution Layer (El API de Preview)
El API de preview es el puente entre el frontend visual y los documentos en Sanity. Su responsabilidad es resolver una URL a un documento específico.
```typescript
// pages/api/preview.ts - Endpoint de resolución de preview
import { definePreview } from 'next-sanity/preview'
import { getClient } from '@/sanity/lib/client'
// El resolver usa la ruta para encontrar el documento correcto
const preview = definePreview({
// Cliente con permisos de lectura en draft
client: getClient(true),
// Función que resuelve una URL a un documento de Sanity
resolveSourceUrl: async ({ requestByHref, href }) => {
if (!href) return null
// Parsear la URL para extraer parámetros
const url = new URL(href, 'http://localhost')
const slug = url.pathname.replace(/^\/|\/$/g, '')
// Buscar el documento por slug
const page = await requestByHref({
query: `*[_type == "page" && slug.current == $slug][0]{
_id,
_type,
title
}`,
params: { slug },
})
return page
},
})
export default preview
```
Este resolver es el corazón del sistema. Sin él, Sanity no puede saber qué documento está viendo el editor cuando pulsa en el overlay.
Nodo 3: Navigation Layer (El Botón de Apertura)
El último nodo es la navegación bidireccional. Necesitáis un mecanismo para que cuando el editor esté en el frontend y pulse sobre un componente, Sanity abra exactamente ese documento.
```typescript
// components/EditableComponent.tsx - Wrapper para edición visual
import { useEditingLink } from '@sanity/visual-editing'
import Link from 'next/link'
interface EditableComponentProps {
documentId: string
documentType: string
children: React.ReactNode
}
export function EditableComponent({
documentId,
documentType,
children
}: EditableComponentProps) {
const { onClick } = useEditingLink({
id: documentId,
type: documentType,
})
return (
<div
data-sanity-edit-link
onClick={onClick}
style={{ cursor: 'pointer' }}
>
{children}
</div>
)
}
```
```typescript
// hooks/useContentSourceMap.ts - Hook para crear content source maps
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export function useContentSourceMap() {
const router = useRouter()
useEffect(() => {
// Cuando el router cambia, actualizar el content source map
// Esto permite que Sanity sepa qué documentos están en pantalla
const currentPath = router.asPath
// Registrar los documentos visibles en esta ruta
// para que el overlay pueda identificarlos
const registerDocuments = async () => {
// El código real dependería de cómo exponéis los documentos
// desde vuestras páginas de Next.js
}
registerDocuments()
}, [router.asPath])
}
```
El Error que el 90% Commite: Confundir Preview con Visual Editing
Vuelvo al punto inicial porque es donde más equipos fracasan.
Preview es una funcionalidad: montáis un iframe, le pasáis un token, el contenido se muestra sin publicar.
Visual Editing es una experiencia: el editor manipula el contenido como si estuviera en el frontend, ve los cambios en tiempo real, y cuando pulsa "Publish", todo está actualizado.
❌ Implementación incorrecta:
```typescript
// ❌ Este código NO es Visual Editing
// Es solo un preview básico
export default function PreviewPage() {
const previewData = usePreviewContext()
return (
<iframe
src={`${process.env.NEXT_PUBLIC_SITE_URL}?preview=true`}
style={{ width: '100%', height: '100vh' }}
/>
)
}
```
✅ Implementación correcta:
```typescript
// ✅ Este código SÍ es Visual Editing real
// El editor puede editar directamente en el viewport
export default function StudioPage() {
return (
<NextStudio config={config}>
<VisualEditing
liveQueryEnabled={true}
zIndex={9999}
refreshInterval={500}
/>
</NextStudio>
)
}
```
La diferencia es que en el segundo caso, el componente VisualEditing se monta en el DOM y se comunica con el overlay de Sanity. Cada vez que el editor modifica un campo en el Studio, la query en el frontend se re-ejecuta automáticamente sin recargar la página.
Content Source Maps: La Pieza que el 90% Ignora
Los content source maps son el mecanismo que permite que Sanity sepa qué documentos está renderizando vuestra página en cada momento.
Cuando un componente se renderiza en el frontend, puede annotarse a sí mismo en el content source map. Esto permite que cuando el editor pulsa sobre cualquier elemento visible, Sanity pueda:
1. Identificar el documento exacto
2. Identificar el campo específico (no solo el documento completo)
3. Abrir ese campo para edición en el Studio
```typescript
// lib/contentSourceMap.ts - Configuración del content source map
import { createClient } from '@sanity/client'
import { defineDocumentId } from '@sanity/visual-editing'
export async function registerContentSourceMap(pageData: any) {
const documents = []
// Recursively extract all Sanity document references
const extractReferences = (obj: any) => {
if (!obj || typeof obj !== 'object') return
if (obj._type && obj._id) {
documents.push({
_id: obj._id,
_type: obj._type,
// Usar el path completo para identificar el campo específico
_path: extractPath(obj),
})
}
// Recursión para objetos anidados
Object.values(obj).forEach(extractReferences)
}
extractReferences(pageData)
// Registrar en el content source map global
if (typeof window !== 'undefined') {
(window as any).__SANITY_CONTENT_SOURCE_MAP__ = documents
}
}
```
Sin content source maps, el editor puede abrir el documento correcto pero no sabe qué campo específico corresponde al elemento que ha pulsado. Es la diferencia entre abrir un libro entero o abrir directamente la página 47, párrafo 3.
Live Query: El Detalle que Hace que Funcione
La parte de "live" en live preview es la más importante y la más ignorada.
El sistema funciona con subscriptions GraphQL o con queries GROQ que se mantienen abiertas. Cuando un documento cambia en Sanity, el servidor envía una actualización y el frontend se re-renderiza sin intervención.
```typescript
// lib/liveQuery.ts - Hook para queries en tiempo real
import { useQuerySubscription } from 'next-sanity'
export function useLiveQuery<T>(query: string, params?: Record<string, any>) {
const subscription = useQuerySubscription<T>({
query,
params: params || {},
enabled: true,
// Listener para actualizaciones en tiempo real
onData: (data) => {
// Actualizar el estado con los nuevos datos
// La UI se re-renderiza automáticamente
},
})
return subscription
}
```
En el frontend, cada componente subscribe a las queries específicas de los documentos que renderiza. Cuando el editor cambia un título, solo ese componente se actualiza. No hay full page refresh. No hay pérdida de estado.
La Métrica que Importa: Tiempo de Iteración del Editor
El objetivo real del Visual Editing no es técnico. Es reducir el tiempo entre que un editor tiene una idea y esa idea aparece en producción.
Un editor sin Visual Editing:
1. Abre Sanity Studio
2. Busca el documento
3. Edita el campo
4. Publica
5. Abre nueva pestaña
6. Navega al contenido
7. Evalúa el resultado
8. Vuelve a Studio si no es correcto
Tiempo medio: 3-5 minutos por iteración.
Un editor con Visual Editing:
1. Pulsa sobre el elemento en el frontend
2. Edita directamente
3. Ve el resultado instantáneamente
4. Publish cuando está satisfecho
Tiempo medio: 30 segundos por iteración.
Eso es una reducción del 85% en tiempo de iteración. Un editor que hace 20 cambios al día pasa de 60-100 minutos de fricción a 10 minutos.
Framework: El Sistema de 3 Capas para Visual Editing en Producción
Basándome en implementaciones que funcionan en producción, os presento el framework que elimina el 90% de los problemas de Visual Editing:
Capa 1: Configuración Central
```typescript
// sanity.config.ts
export default defineConfig({
// ... resto de configuración
plugins: [
visualEditing({
// Indicar que el frontend ya tiene el componente de overlay
overlay: false,
}),
],
})
```
Capa 2: API de Resolución Robusto
```typescript
// lib/resolvePreviewUrl.ts
export async function resolvePreviewUrl(doc: any, requestByHref: any) {
// Si es una página, usar slug para resolver
if (doc._type === 'page' && doc.slug?.current) {
return `/${doc.slug.current}`
}
// Si es una página de inicio
if (doc._type === 'page' && doc.isHomePage) {
return '/'
}
// Si es un post de blog
if (doc._type === 'post' && doc.slug?.current) {
return `/blog/${doc.slug.current}`
}
// Fallback: buscar en el sitemap
const sitemapEntry = await requestByHref({
query: `*[_type == "sitemap" && document._ref == $id][0]{ url }`,
params: { id: doc._id },
})
return sitemapEntry?.url || '/'
}
```
Capa 3: Componentes Editables
```typescript
// components/withVisualEditing.tsx
import { EditableComponent } from '@/components/EditableComponent'
export function withVisualEditing<P extends object>(
Component: React.ComponentType<P>,
documentConfig: { type: string; resolveId?: (props: P) => string }
) {
return function EditableComponent(props: P) {
const documentId = documentConfig.resolveId
? documentConfig.resolveId(props)
: props.document?._id || props.documentId
return (
<EditableComponent
documentId={documentId}
documentType={documentConfig.type}
>
<Component {...props} />
</EditableComponent>
)
}
}
// Uso: wrap componentes con edición visual
const EditableHero = withVisualEditing(Hero, {
type: 'hero',
resolveId: (props) => props.document._id,
})
```
Takeaways Clave
→ El Visual Editing efectivo no es un plugin de Sanity. Es una arquitectura de tres capas que incluye preview resolution, content source maps, y live queries.
→ El 90% de implementaciones fracasan porque treat el preview como un iframe, no como un sistema de edición en contexto.
→ Los content source maps son opcionales en el sentido de que el sistema funciona sin ellos, pero esenciales para una experiencia de edición de campo específica.
→ La métrica que debéis medir es tiempo de iteración del editor, no velocidad de load de páginas.
→ El framework de 3 capas (Configuración → Resolución → Componentes) previene el 90% de problemas comunes en producción.
Lo Que Viene
El Visual Editing en Sanity está evolucionando hacia experiencias donde el editor no necesita distinguir entre "modo preview" y "producción". El objetivo es un estado único donde cada documento tiene una URL canónica y se puede editar in-context sin fricción.
Para 2026, esperad integraciones más profundas con frameworks de rendering edge-side donde las queries se ejecutan en el borde, cerca del editor, reduciendo latencia de preview a niveles imperceptibles.
Si vuestros editores siguen usando pestañas separadas y URLs copiadas, no es un problema de adopción. Es un problema de arquitectura. Arreglad la arquitectura primero.
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

