Sanity Webhooks y Content Pipelines: La Arquitectura que Nadie Te Explica
Aprende a usar Sanity webhooks para revalidación granular en Next.js, pipelines de contenido autónomos y orquestación real en producción.
La Mayoría Usa Sanity como Almacén. Los Mejores lo Usan como Orquestador
Publicas un post. Esperas que Next.js lo pille. Rezas para que el cache se invalide.
Eso no es un content pipeline. Es contenido por fe.
*El problema real no es Sanity. Es que no entiendes lo que sus webhooks pueden hacer.*
No son simples notificaciones de "algo cambió". Son eventos tipados, filtrables y enrutables que pueden desencadenar revalidación granular, disparar agentes de contenido, sincronizar múltiples destinos, y mantener coherencia entre sistemas completamente distintos.
Este artículo te muestra la arquitectura que separa los proyectos que escalan de los que se rompen en producción.
---
Lo que la Mayoría Configura Mal desde el Primer Día
El setup típico de un developer que integra Sanity con Next.js:
❌ El enfoque equivocado:
```typescript
// pages/api/revalidate.ts — lo que el 80% hace
export default async function handler(req, res) {
await res.revalidate('/blog');
return res.json({ revalidated: true });
}
```
Esto invalida toda la ruta `/blog` cada vez que cambias cualquier documento. Un cambio en un post de 2021 invalida el cache de 500 artículos. Brutal.
✅ El enfoque correcto — revalidación granular:
```typescript
// app/api/sanity/webhook/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { parseBody } from 'next-sanity/webhook';
export async function POST(req: Request) {
const { isValidSignature, body } = await parseBody<{
_type: string;
slug?: { current: string };
_id: string;
}>(req, process.env.SANITY_WEBHOOK_SECRET);
if (!isValidSignature) {
return new Response('Invalid signature', { status: 401 });
}
// Revalidación quirúrgica por tipo de documento
switch (body._type) {
case 'post':
revalidateTag(`post-${body._id}`);
if (body.slug?.current) {
revalidatePath(`/blog/${body.slug.current}`);
}
revalidatePath('/blog'); // Solo el listing, no cada post
break;
case 'author':
revalidateTag(`author-${body._id}`);
break;
case 'siteSettings':
revalidatePath('/', 'layout'); // Invalida el layout global
break;
}
return Response.json({ revalidated: true, type: body._type });
}
```
La diferencia es revalidación quirúrgica. Solo invalidas lo que cambió. No el universo entero.
---
Sanity Webhooks: Configuración Real para Múltiples Entornos
Esta es la parte que los tutoriales básicos ignoran completamente.
Cuando tienes un proyecto real, necesitas al menos tres webhooks distintos:
→ Producción: `https://tudominio.com/api/sanity/webhook` — solo documentos publicados
→ Preview/Staging: `https://staging.tudominio.com/api/sanity/webhook` — borradores incluidos
→ Pipeline de contenido: tu sistema de agentes o worker externo
En el dashboard de Sanity, cada webhook acepta un filtro GROQ. Aquí está la clave que nadie usa:
```groq
// Solo dispara para posts publicados en producción
_type == "post" && !(_id in path("drafts.**"))
// Para staging: incluye borradores
_type == "post"
// Para una sección específica del site
_type in ["post", "author", "category"]
```
Filtra en el origen. No en tu handler. Cada webhook innecesario que llega a tu server es latencia y coste que evitas con dos líneas de GROQ.
Verificación de Firma — No Es Opcional
```typescript
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(body: string, signature: string, secret: string): boolean {
const hmac = createHmac('sha256', secret);
hmac.update(body);
const digest = hmac.digest('base64');
// timingSafeEqual previene timing attacks
return timingSafeEqual(
Buffer.from(digest),
Buffer.from(signature.replace('sha256=', ''))
);
}
```
Sin verificación de firma, cualquiera puede invalidar tu cache. O peor: disparar tu pipeline de contenido con datos fabricados.
---
El Pattern Avanzado: Webhook como Disparador de Pipeline Autónomo
El real potencial de los webhooks de Sanity no es revalidar Next.js. Es orquestar sistemas completos.
En FindEmergencyPlumber.com — un directorio con más de 1.000 piezas de contenido publicado y un pipeline de agentes autónomo — Sanity actúa como el punto de entrada del sistema. Un webhook dispara una cadena de procesos que no requiere intervención humana.
Así se estructura:
```typescript
// app/api/content-pipeline/route.ts
import { createClient } from '@sanity/client';
const sanityClient = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
useCdn: false,
apiVersion: '2024-01-01',
token: process.env.SANITY_WRITE_TOKEN,
});
type PipelineJob = {
documentId: string;
documentType: string;
action: 'publish' | 'update' | 'delete';
timestamp: string;
};
async function enqueuePipelineJob(job: PipelineJob) {
// En este sistema: Supabase como state machine y job queue
// El webhook escribe el job, el worker lo procesa de forma asíncrona
const { error } = await supabase
.from('content_pipeline_jobs')
.insert({
document_id: job.documentId,
document_type: job.documentType,
action: job.action,
status: 'pending',
created_at: job.timestamp,
});
if (error) throw new Error(`Failed to enqueue job: ${error.message}`);
}
export async function POST(req: Request) {
const { isValidSignature, body } = await parseBody(req, process.env.SANITY_WEBHOOK_SECRET);
if (!isValidSignature) return new Response('Unauthorized', { status: 401 });
// Determina la acción basándose en el estado del documento
const action = body._id.startsWith('drafts.') ? 'update' : 'publish';
await enqueuePipelineJob({
documentId: body._id,
documentType: body._type,
action,
timestamp: new Date().toISOString(),
});
// Revalidación inmediata en paralelo, pipeline asíncrono
if (action === 'publish' && body.slug?.current) {
revalidatePath(`/blog/${body.slug.current}`);
}
return Response.json({ queued: true, action });
}
```
*La clave de este pattern: el webhook responde en menos de 200ms y delega el trabajo pesado a un sistema asíncrono.*
Nunca hagas trabajo blocking en un webhook handler. Sanity espera respuesta. Si tardas, el webhook falla.
---
GROQ dentro del Webhook: Enriquecer el Payload al Vuelo
El payload que Sanity envía en el webhook es minimalista. Solo el `_id`, `_type`, y los campos que configures en el dashboard.
Pero a veces necesitas más contexto. Este es el pattern correcto:
```typescript
async function enrichPayload(documentId: string, documentType: string) {
const queries: Record<string, string> = {
post: `*[_type == "post" && _id == $id][0]{
_id,
title,
"slug": slug.current,
"author": author->name,
"categories": categories[]->title,
publishedAt,
"relatedPosts": *[_type == "post" && references(^._id)][0..2]._id
}`,
author: `*[_type == "author" && _id == $id][0]{
_id,
name,
"postCount": count(*[_type == "post" && references(^._id)])
}`,
};
const query = queries[documentType];
if (!query) return null;
return sanityClient.fetch(query, { id: documentId });
}
```
No uses `fetch` genérico contra la Content API aquí. Usa el cliente de Sanity con tu token de servidor. Más rápido, más seguro, con acceso a borradores si los necesitas.
---
Monitorización: Lo que Falla Silenciosamente en Producción
El problema más común que nadie menciona: los webhooks fallan sin que te enteres.
Sanity reintenta los webhooks fallidos, pero solo hasta cierto punto. Si tu handler devuelve un 500 consistentemente, eventualmente Sanity deja de intentarlo. Tu contenido se publica pero nada se revalida.
✅ Implementa logging mínimo desde el día uno:
```typescript
export async function POST(req: Request) {
const startTime = Date.now();
try {
const { isValidSignature, body } = await parseBody(req, process.env.SANITY_WEBHOOK_SECRET);
if (!isValidSignature) {
console.error('[Sanity Webhook] Invalid signature', {
timestamp: new Date().toISOString()
});
return new Response('Unauthorized', { status: 401 });
}
// ... lógica de revalidación
const duration = Date.now() - startTime;
console.log('[Sanity Webhook] Processed', {
type: body._type,
id: body._id,
duration: `${duration}ms`,
});
return Response.json({ revalidated: true });
} catch (error) {
console.error('[Sanity Webhook] Error', { error, duration: Date.now() - startTime });
// Devuelve 200 para que Sanity no reintente si el error es tuyo
// Devuelve 500 solo si quieres que Sanity reintente
return Response.json({ error: 'Internal error' }, { status: 200 });
}
}
```
El detalle del status code es crítico: si devuelves 500, Sanity reintenta. A veces eso es lo que quieres. A veces genera bucles infinitos. Decide conscientemente.
---
Takeaways Finales
→ Revalidación granular siempre. Usa `revalidateTag` y `revalidatePath` con identificadores específicos, nunca invalides rutas completas por defecto.
→ Filtra en GROQ, no en tu handler. Configura los filtros del webhook en el dashboard de Sanity para que solo lleguen los eventos que te interesan.
→ Verifica firmas sin excepciones. `timingSafeEqual` en criptografía, no comparación directa de strings.
→ Responde rápido, procesa asíncrono. Tu webhook handler debe responder en menos de 200ms. El trabajo pesado va a una cola.
→ Loguea desde el primer deploy. Los webhooks fallan silenciosamente. Sin logs, no sabes qué está pasando hasta que un usuario reporta contenido desactualizado.
Sanity no es un CMS con webhooks. *Es un sistema de eventos con una interfaz de edición de contenido.* Cuando lo entiendes así, construyes arquitecturas que funcionan de verdad en producción.
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

