Integración con Sanity
El frontend consume el contenido de Sanity directamente desde Server Components en tiempo de build. No hay servidor Node en producción — los datos se resuelven durante next build y el output es HTML estático.
Configuración
Variables de entorno
Agregar en apps/web/.env.local:
NEXT_PUBLIC_SANITY_PROJECT_ID=<project-id>
NEXT_PUBLIC_SANITY_DATASET=productionEl project-id es el mismo que usa el Studio en SANITY_STUDIO_API_PROJECT_ID. Consultar apps/studio/.env.local para obtenerlo.
En producción estas variables deben estar configuradas en la plataforma de deploy (Vercel, Cloudflare Pages, etc.) antes del build.
Cliente (apps/web/lib/sanity.ts)
import { createClient } from "@sanity/client"
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET ?? "production",
apiVersion: "2025-06-03",
useCdn: true,
})| Opción | Valor | Por qué |
|---|---|---|
apiVersion | fecha fija | Sanity versiona su API por fecha — fijar la versión evita cambios inesperados |
useCdn: true | true | El CDN de Sanity acelera las lecturas. Solo usar false para mutaciones o datos en tiempo real |
Queries
Todos los queries viven en apps/web/lib/utils.ts e importan sanityClient.
Proyección reutilizable
const eventFields = `
"slug": slug.current,
eyebrow,
title,
description,
longDescription,
imagePublicId,
imageAlt,
eventDate,
date,
dateShort,
timeRange,
location,
stateOrProvince,
city,
price,
priceAmount,
mapsEmbedUrl,
mapsDirectionsUrl,
reservationUrl,
itinerary[] { time, activity },
comingSoon
`;La proyección hace dos cosas importantes:
"slug": slug.current— aplana el objeto{current: string}de Sanity a unstringplano, que es lo que esperaEventContent- Selección explícita — solo se transfieren los campos necesarios, no el documento completo
getEventsList()
export async function getEventsList(): Promise<EventContent[]> {
const events = await sanityClient.fetch<EventContent[]>(
`*[_type == "event" && (eventDate >= now() || !defined(eventDate))]
| order(eventDate asc) { ${eventFields} }`
);
// Eventos sin eventDate (próximamente sin fecha) van al final
return [
...events.filter((e) => e.eventDate != null),
...events.filter((e) => e.eventDate == null),
];
}El filtro GROQ eventDate >= now() || !defined(eventDate) excluye eventos pasados y conserva los que no tienen fecha confirmada. El post-procesado en JS mueve los eventos sin fecha al final de la lista (en GROQ, los valores null se ordenan antes que los definidos en orden ascendente).
getEventBySlug()
export async function getEventBySlug(
slug: string
): Promise<EventContent | null> {
return sanityClient.fetch(
`*[_type == "event" && slug.current == $slug][0] { ${eventFields} }`,
{ slug }
);
}Recibe el parámetro slug como variable GROQ ($slug) en lugar de interpolarlo en el string — esto previene inyección y permite que Sanity optimice la query en caché.
Flujo de datos
Sanity CMS (API)
│
│ sanityClient.fetch() ← build time
▼
lib/utils.ts
├─ getEventsList() → EventContent[]
└─ getEventBySlug() → EventContent | null
│
▼
Server Components (app/page.tsx, app/events/[slug]/page.tsx)
│
├─ generateStaticParams() → genera rutas estáticas por slug
└─ page() → pasa datos como props a los componentes
│
▼
Componentes (EventsSection, EventCard, etc.)
│ solo reciben props, no hacen fetch
▼
HTML estático en out/Los datos de Sanity solo se consultan en tiempo de build. En producción no hay ninguna llamada a la API de Sanity desde el navegador.
Agregar queries para nuevos tipos
Al agregar un nuevo tipo de documento en Sanity (ej. post), el patrón a seguir es:
- Definir la proyección de campos en
utils.ts:
const postFields = `
"slug": slug.current,
title,
// ... campos del documento
`;- Agregar las funciones de query:
export async function getPostsList(): Promise<PostContent[]> {
return sanityClient.fetch(
`*[_type == "post"] | order(_createdAt desc) { ${postFields} }`
);
}
export async function getPostBySlug(slug: string): Promise<PostContent | null> {
return sanityClient.fetch(
`*[_type == "post" && slug.current == $slug][0] { ${postFields} }`,
{ slug }
);
}- Agregar el tipo correspondiente en
types/index.tssiguiendo el patrón deEventContent.
Notas sobre static export y Sanity
apps/web usa output: "export" en next.config.ts. Esto implica:
- Sin ISR ni revalidación en runtime — para actualizar el contenido se requiere un nuevo build
- Sin Server Components con fetch en runtime — todos los fetches a Sanity ocurren en build
useCdn: truees suficiente — no se necesita el token de escritura ni elwithCredentials
Si en el futuro se activa SSR (eliminando output: "export"), el cliente puede cambiar a useCdn: false con revalidación para obtener contenido actualizado sin rebuilds.