Diil Docs
  1. Documentación
  2. Guías

Caché, ETag y 304: rápido y siempre al día

Actualizado:

Las APIs de contenido suelen obligarte a elegir: rápido o fresco. Nosotros preferimos no elegir. Las respuestas se cachean en nuestro lado pero allí nunca se quedan viejas, cada respuesta lleva un ETag para que el contenido sin cambios no te cueste casi nada, y un cambio en el CRM llega a la API en cuanto el editor pulsa “Guardar”. Esta página explica cómo funciona y cómo sacarle el máximo partido en tu lado.

De un cambio en el CRM a tu página

Este es el viaje completo de un cambio, del teclado del editor a la pantalla de tu visitante:

  1. El editor guarda

    Alguien corrige una errata en el CRM o directamente en la web en vivo. Vale cualquier cambio: un texto, una imagen, un ajuste de la página, un post del blog.
  2. Sube la versión del contenido del sitio

    Cada cambio incrementa la versión del contenido del sitio. Nuestra caché del servidor (Redis) va atada a esa versión, así que todas las respuestas cacheadas antiguas dejan de servir de golpe. Nadie tiene que “limpiar la caché”.
  3. La siguiente petición a la API recibe datos frescos

    La primerísima petición a la API construye su respuesta con el contenido nuevo. En nuestro lado no hay ningún retraso: ni TTL que esperar ni cola de purgado.
  4. Las cachés entre nosotros y el visitante se ponen al día

    Lo que ve un visitante de verdad puede ir un poco por detrás: el navegador o una CDN pueden guardar la respuesta anterior hasta 60 segundos y mostrarla una vez más mientras traen la nueva en segundo plano (eso es stale-while-revalidate). Tu propia caché de servidor, si tienes, suma su propio tiempo de vida encima.

Las cabeceras de caché, explicadas

Una respuesta correcta típica llega con estas cabeceras:

200 OK — cabeceras de respuestahttp
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding
Access-Control-Allow-Origin: *
CabeceraQué les dice a las cachés
Cache-Control: publicCualquiera puede guardar la respuesta: el navegador, una CDN, un proxy. El contenido es público de todos modos: es lo que muestras en tu web.
max-age=60Durante 60 segundos la respuesta cuenta como fresca y se puede reutilizar sin preguntarnos nada.
stale-while-revalidate=600Durante los 10 minutos siguientes una caché puede entregar la copia vieja al instante mientras trae una nueva en segundo plano. Rápido para este visitante, fresco para el siguiente.
ETag: W/"…"Un ETag débil: un hash del cuerpo de la respuesta. Mismo cuerpo, mismo ETag. Devuélvelo en If-None-Match y recibirás un 304 si nada ha cambiado.
Vary: x-crm-key, Accept-EncodingLas cachés deben guardar copias separadas por clave de sitio y por compresión. Dos sitios nunca comparten una respuesta cacheada, ni siquiera con la misma URL.
Cache-Control: no-storeSe envía con cada error. Un 404 de una página que un editor está creando ahora mismo no debería quedarse rondando en la caché de nadie.

CORS está abierto a cualquier origen, la API permite explícitamente la cabecera de petición If-None-Match y expone ETag a JavaScript, así que todo lo de esta página funciona también desde el navegador.

ETag y 304 Not Modified

El ETag es la forma más barata de preguntar “¿ha cambiado algo?”. Guarda el ETag de la última respuesta, envíalo en If-None-Match la próxima vez y, si el contenido es el mismo, recibirás 304 Not Modified con el cuerpo vacío. Tu código sigue usando la copia que ya tiene.

Primera petición: la respuesta completahttp
GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

HTTP/1.1 200 OK
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding

{ "id": 26, "marker": "home", "name": "Home", "content": { … } }
Siguiente petición: nada ha cambiadohttp
GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
If-None-Match: W/"a41f9c0e7b2d58f3"

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"

En cuanto un editor cambia algo en esa página, cambia el cuerpo, cambia el hash y la misma petición devuelve un 200 fresco con un ETag nuevo.

Recetas de caché

En el navegador: ya está hecho

Si pides el contenido directamente en el navegador, no necesitas ni una línea de código de caché. Un fetch normal usa la caché HTTP del navegador: durante 60 segundos reutiliza la respuesta y después revalida con If-None-Match él solito.

Navegador: nada que configurarjs
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
  headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
  // cache: 'default' es el valor por defecto: el navegador gestiona max-age y ETag por ti
});
const page = await res.json();

No te sorprendas si tu código nunca ve un 304: el navegador lo cambia por el 200 cacheado antes de que te llegue. Abre la pestaña Network de DevTools para ver qué ha pasado de verdad: ahí encontrarás el 304 o el “disk cache”.

En un servidor Node: una caché con ETag minúscula

El fetch del lado del servidor en Node no tiene caché HTTP propia, así que cada llamada llega hasta la API. Un pequeño Map lo arregla: reutiliza la copia durante 60 segundos (igual que max-age), luego pregunta con If-None-Match y descarga el cuerpo solo si ha cambiado.

lib/content.ts — Express, Fastify, Nuxt, Remix, cualquier cosa sobre Nodets
const API = 'https://back.sitecog.com/content';
const TTL = 60_000; // igual que max-age=60

type Entry = { etag: string | null; data: unknown; at: number };
// Un proceso, una clave de sitio. ¿Usas varias claves? Mete también la clave en la clave de caché.
const cache = new Map<string, Entry>();

export async function getContent<T>(path: string): Promise<T> {
  const url = API + path;
  const cached = cache.get(url);

  // 1. Todavía fresco: ni siquiera llamamos a la API
  if (cached && Date.now() - cached.at < TTL) return cached.data as T;

  // 2. Preguntamos "¿ha cambiado?" con el ETag que ya tenemos
  const headers: Record<string, string> = { 'x-crm-key': process.env.CRM_KEY! };
  if (cached?.etag) headers['if-none-match'] = cached.etag;

  const res = await fetch(url, { headers });

  if (res.status === 304 && cached) {
    cached.at = Date.now(); // mismo contenido, otros 60 segundos de paz
    return cached.data as T;
  }
  if (!res.ok) throw new Error(`Content API ${res.status} for ${path}`);

  const data = (await res.json()) as T;
  cache.set(url, { etag: res.headers.get('etag'), data, at: Date.now() });
  return data;
}

// uso
const home = await getContent('/v1/pages/home?lang=en');

Next.js: revalidate y refresco bajo demanda

En el App Router la caché de fetch hace el trabajo por ti. revalidate: 60 coincide con nuestro max-age: Next.js guarda la respuesta un minuto y luego la refresca en segundo plano.

app/page.tsxtsx
export default async function Home() {
  const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
    headers: { 'x-crm-key': process.env.CRM_KEY! },
    next: { revalidate: 60, tags: ['crm-content'] },
  });
  const page = await res.json();

  return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}

¿Quieres menos retraso? Baja revalidate, pero vigila los límites. ¿Quieres un botón de “publicar ya” para un gran lanzamiento? Etiqueta tus fetch y expón una pequeña ruta que invalide la etiqueta:

app/api/revalidate/route.tsts
import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  if (req.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
    return new Response('Nanay', { status: 401 });
  }
  // Next.js 16 acepta un segundo argumento; en Next.js 15 es simplemente revalidateTag('crm-content')
  revalidateTag('crm-content', { expire: 0 });
  return Response.json({ revalidated: true });
}

Llámala desde donde le venga bien a tu equipo: un script de despliegue, un bookmarklet, un bot de chat, un botón en tu propio panel de administración. El secreto es solo tuyo: no tiene nada que ver con la clave del sitio.

Modo Live: sáltate todas las cachés

Cuando un editor abre tu web en el modo Live del CRM, quiere ver su cambio nada más guardar, no un minuto después. Dentro del marco del CRM la URL recibe ?crm_live=1. Buena práctica (y exactamente lo que hace nuestro cliente de referencia): cuando la página está dentro de un iframe o tiene crm_live, haz el fetch con cache: 'no-store'. Nuestro lado ya está fresco, así que no hace falta nada más.

const isLive =
  window.self !== window.top ||
  new URLSearchParams(location.search).has('crm_live');

const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
  headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
  cache: isLive ? 'no-store' : 'default',
});

Más sobre el modo Live y el marcado que hay detrás en la página Widget y marcado.

“¿Por qué no veo mi cambio?”

La pregunta más popular sobre cualquier caché, de todos los tiempos. Recorre esta lista de arriba abajo: va de “diez segundos para comprobarlo” a “prepárate un té”.

  1. Pregunta directamente a la API. Un curl normal no tiene caché, y nuestro lado siempre está fresco. Si curl muestra el texto nuevo, la API está bien y la copia vieja vive en alguna caché por el camino. Si curl muestra el texto viejo, sigue bajando por la lista.
  2. ¿Se guardó de verdad? Compruébalo en el CRM. Para los posts del blog: los borradores nunca se devuelven, y un post programado para el futuro sigue oculto hasta su fecha (y puede aparecer unos minutos tarde).
  3. ¿El sitio correcto? Una clave pertenece exactamente a un sitio. Staging y producción con claves distintas leen contenido distinto.
  4. ¿El idioma correcto? No hay respaldo en el servidor. Si el editor cambió el texto en alemán y tú renderizas el inglés, no pasa nada visible. Una traducción vacía vuelve como "", y tu código de respaldo puede estar mostrando otro idioma sin decir nada. Consulta Idiomas y respaldos.
  5. ¿El bloque correcto? Los marcadores distinguen mayúsculas y minúsculas, y los marcadores de bloque solo son únicos dentro de una sección: /v1/blocks/title sin ?section= devuelve el bloque más antiguo con ese marcador, que puede no ser el que se editó.
  6. ¿Sección oculta? Las secciones con show: false se siguen devolviendo. Si el editor ocultó una sección y sigue en la web, tu plantilla no está comprobando show.
  7. Tu propia caché. revalidate, ISR, un mapa en memoria, una CDN delante de tu web, una página generada una sola vez en el build. Es el sospechoso habitual.
  8. El navegador. Hasta 60 segundos más un refresco en segundo plano. Una recarga forzada (Ctrl+Shift+R o Cmd+Shift+R) lo resuelve.
  9. ¿Editando en modo Live? Asegúrate de que dentro del marco del CRM haces el fetch con cache: 'no-store' (mira más arriba).
Mira qué devuelve la API ahora mismobash
curl -i "https://back.sitecog.com/content/v1/pages/home?lang=en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"