Intro
Hace unas semanas os presentamos el modo "Snapshot" de Content Island, una forma de combinar tecnologías SSR como Astro, Next.js o TanStack Start con un rendimiento muy cercano al de un sitio estático.
La idea es sencilla: en lugar de consultar el API de Content Island en cada petición, exportas el contenido de tu proyecto a un único snapshot (las imágenes y vídeos se siguen sirviéndose desde la CDN) y tu aplicación carga ese snapshot completamente en memoria.
Con un único cambio en la configuración, toda tu aplicación pasa a leer desde esa copia local.
export const contentIslandClient = createClient({
accessToken: ENV.CONTENT_ISLAND_TOKEN,
+ mode: 'snapshot',
});El resultado es un acceso prácticamente instantáneo al contenido, evitando llamadas constantes a la API.
Pero enseguida aparece una pregunta:
¿Qué ocurre cuando el contenido cambia?
La solución tradicional consiste en lanzar un nuevo build cada vez que alguien publica contenido. Es un enfoque válido y suficiente para muchos proyectos.
Sin embargo, cuando el contenido cambia varias veces al día, reconstruir y desplegar continuamente deja de ser la mejor opción.
¿Y si, en lugar de reconstruir toda la aplicación, simplemente sustituyéramos el snapshot que está en memoria?
Eso es exactamente lo que llamamos Dynamic Snapshot.
Dynamic Snapshot
Hemos incorporado una nueva funcionalidad al client API SDK de Content Island que permite recargar un snapshot en memoria sin necesidad de realizar un nuevo build.
export const contentIslandClient = createClient({
accessToken: ENV.CONTENT_ISLAND_TOKEN,
mode: "snapshot",
snapshotLoader: async () => {
// Devuelve el JSON del snapshot (string u objeto) desde donde quieras
},
});
// Cuando el contenido cambie (webhook, intervalo, pub/sub):
const result = await contentIslandClient.refreshSnapshot();
// -> { status: 'updated' | 'unchanged', meta: {...} }Todo gira alrededor de dos conceptos:
snapshotLoader→ de dónde obtener el snapshot.refreshSnapshot()→ cuándo actualizarlo.
refreshSnapshot() vuelve a ejecutar el snapshotLoader y, si existe una versión más reciente, realiza un reemplazo atómico del snapshot en memoria. Las peticiones concurrentes siempre utilizan una versión completa del contenido.
💡 El snapshot inicial puede obtenerse mediante
npx content-island export(tienes que previamente haber instalado el paquete de api clientnpm install @content-island/api-client) o directamente desde código conexportSnapshot().
💡 Una estrategia muy recomendable es generar el snapshot durante el proceso de CD y desplegarlo junto con la aplicación. Si la exportación falla, fallará también la pipeline, evitando despliegues inconsistentes. Además, la aplicación dispondrá de un snapshot desde la primera request y refreshSnapshot() se encargará de mantenerlo actualizado posteriormente.
Estrategias de actualización
Todas las estrategias siguen el mismo patrón:
- Exportar el snapshot.
- Guardarlo en un lugar accesible.
- Detectar cuándo cambia el contenido.
- Ejecutar
refreshSnapshot().
Lo único que cambia es dónde vive el snapshot y quién dispara la actualización.
Opción 1 --- Leer directamente desde la API
Ideal para una única instancia o muy pocas.
El propio snapshotLoader utiliza exportSnapshot() para obtener el snapshot directamente desde Content Island.
Flujo:
Publicación → Webhook → GitHub Action → Endpoint protegido →
refreshSnapshot()

Al crear el cliente le indicas que quieres usar el modo snapshot y le pasas un snapshotLoader que cargue de la API de Content Island:
import { createClient, exportSnapshot } from "@content-island/api-client";
export const contentIslandClient = createClient({
accessToken: ENV.CONTENT_ISLAND_TOKEN,
mode: "snapshot",
snapshotLoader: async () =>
exportSnapshot({ accessToken: ENV.CONTENT_ISLAND_TOKEN }),
});¿Y cuándo se refresca? Expones un pequeño endpoint (por ejemplo POST /api/content-island/refresh) que ejecuta contentIslandClient.refreshSnapshot(). La cadena queda así: Content Island publica contenido → dispara una GitHub Action (vía repository_dispatch) → la Action llama a tu endpoint → tu app refresca el JSON en memoria al instante.
⚠️ Protege ese endpoint. Cualquiera que descubra la URL puede invocarlo, tienes un vector de abuso (por ejemplo, forzar exportaciones contra el API una y otra vez). Una forma de mitigar esto es añadir secreto compartido / API key que la Action envíe en una cabecera y tu endpoint valide antes de hacer nada.
Un ejemplo de flujo en Github Action que actuaría de disparador:
name: Refresh Content Island snapshot
on:
# Content Island dispara esta Action al publicar contenido (vía su webhook)
repository_dispatch:
types: [content-updated]
jobs:
refresh:
runs-on: ubuntu-latest
steps:
# Llama al endpoint de tu app para que ejecute refreshSnapshot()
- name: Notify app to refresh snapshot
run: |
curl -fsS -X POST "$APP_URL/api/content-island/refresh" \
-H "x-refresh-secret: $REFRESH_SECRET" # secreto compartido: el endpoint lo valida
env:
APP_URL: ${{ secrets.APP_URL }}
REFRESH_SECRET: ${{ secrets.REFRESH_SECRET }}Y el endpoint, que valida el secreto antes de refrescar:
// POST /api/content-island/refresh
export async function POST(req: Request) {
if (req.headers.get("x-refresh-secret") !== ENV.REFRESH_SECRET) {
return new Response("Unauthorized", { status: 401 });
}
const result = await contentIslandClient.refreshSnapshot();
return Response.json(result);
}Ventajas
- ✅ Muy sencilla.
- ✅ Sin infraestructura adicional.
Inconvenientes
- ⚠️ Cada instancia descarga su propio snapshot.
- ⚠️ Escala peor cuando aumenta el número de réplicas.
Opción 2 --- Bucket + CDN
Una GitHub Action exporta el snapshot una única vez, lo publica en un bucket (S3, R2, GCS, Azure Blob Storage...) y después notifica a la aplicación para ejecutar refreshSnapshot().
Las aplicaciones leen el snapshot directamente desde el bucket.

Aquí es donde entra una GitHub Action, disparada por el webhook de Content Island (vía repository_dispatch):
name: Update Content Island snapshot
on:
# Content Island dispara esta Action al publicar contenido (vía su webhook)
repository_dispatch:
types: [content-updated]
jobs:
export-and-upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
# 1) Baja el snapshot desde Content Island a un fichero JSON
- run: npx @content-island/api-client export --access-token ${{ secrets.CONTENT_ISLAND_TOKEN }} --snapshot-path ./content-island-snapshot.json
# 2) Sube ese JSON al bucket (a partir de aquí ya se sirve la versión nueva)
- name: Upload to bucket
run: aws s3 cp ./content-island-snapshot.json s3://my-bucket/content-island-snapshot.json --acl public-read
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# 3) Avisa a la app para que recargue en memoria (mismo endpoint que la Opción 1)
- name: Notify app to refresh snapshot
run: |
curl -fsS -X POST "$APP_URL/api/content-island/refresh" \
-H "x-refresh-secret: $REFRESH_SECRET" # secreto compartido: el endpoint lo valida
env:
APP_URL: ${{ secrets.APP_URL }}
REFRESH_SECRET: ${{ secrets.REFRESH_SECRET }}Fíjate en el orden: primero sube el nuevo JSON al bucket y después avisa a la app. Así, cuando tu endpoint ejecute refreshSnapshot(), el bucket ya sirve la versión nueva. Ese endpoint es exactamente el mismo de la Opción 1 (protegido con el secreto compartido) — lo único que cambia es de dónde lee el snapshotLoader.
Y en tu aplicación, el cliente consume el JSON directamente del bucket:
import { createClient } from "@content-island/api-client";
export const contentIslandClient = createClient({
accessToken: ENV.CONTENT_ISLAND_TOKEN,
mode: "snapshot",
snapshotLoader: async () => {
const res = await fetch(BUCKET_SNAPSHOT_URL, { cache: "no-store" });
return res.text();
},
});Lo ideal en este escenario es que en el flujo de CD se baje un primer snapshot a una carpeta conocida del servidor donde va a correr la aplicación y lo use como primer snapshot (así, el primer usuario que entre no tiene que esperar a que se descargue el JSON de la API, y nos aseguramos que hay algo funcionando, si por cualquier motivo la API de Content Island no estuviera disponible, el despliegue rompería y no afectaría a producción), y las siguientes ya se realizan vía S3 bucket.
Un truco útil: puedes usar getSnapshotInfo() para leer el meta del snapshot (exportedAt, schemaVersion...) y decidir si de verdad merece la pena recargar o si ya tienes la última versión.
Ventajas
- ✅ Una sola exportación.
- ✅ Menor carga sobre el API (sólo se hace un export por publicación, no por instancia).
- ✅ Baja latencia utilizando CDN.
Inconvenientes
- ⚠️ Requiere mantener un bucket.
- ⚠️ Propagar el refresco a N réplicas vía webhook empieza a ser incómodo... y eso nos lleva a la Opción 3.
Opción 3 --- Redis Pub/Sub
Pensada para despliegues horizontales con múltiples instancias.
Una única exportación guarda el snapshot en Redis y publica un mensaje Pub/Sub.

Todas las instancias reciben el evento y ejecutan refreshSnapshot() prácticamente al mismo tiempo.
Un único publisher, cómo hicimos antes, podemos usar webhooks + GitHub Action para que sirve de disparador y llame a un método de la API que descargue el, lo guarde en Redis y lo publique en un canal:
const snapshot = await exportSnapshot({
accessToken: ENV.CONTENT_ISLAND_TOKEN,
});
await redis.set("content-island:snapshot", JSON.stringify(snapshot));
await redis.publish("content-island:updated", "1");Cada instancia lee el JSON de Redis en su snapshotLoader:
snapshotLoader: async () => (await redis.get('content-island:snapshot')) ?? '',Y se suscribe al canal para refrescarse en cuanto llega el aviso:
await sub.subscribe("content-island:updated", () =>
contentIslandClient.refreshSnapshot(),
);Un solo publish y toda la flota refresca su JSON en memoria a la vez. Sin builds, sin N llamadas al API, sin webhooks por instancia.
Ventajas
- ✅ Actualización sincronizada.
- ✅ Una sola exportación alimenta toda la infraestructura.
- ✅ Muy escalable.
Inconvenientes
- ⚠️ Requiere Redis.
Opción ...
Estas son las tres que cubren la mayoría de casos, pero recuerda: como el patrón es siempre snapshotLoader + refreshSnapshot(), puedes adaptarlo a lo que tengas (una cola de mensajes, un sistema de ficheros compartido, tu propio broker...). El mecanismo no cambia.
Conclusión
El modo Dynamic Snapshot te da lo mejor de dos mundos: el rendimiento de leer el contenido de un JSON en memoria, y la frescura de poder actualizarlo al instante cuando el contenido cambia, sin relanzar un build.
No hace falta empezar por la arquitectura más compleja. La Opción 1 cubre perfectamente la mayoría de proyectos. A medida que tus necesidades crezcan, podrás evolucionar a la Opción 2 para desacoplarte del API y, finalmente, a la Opción 3 si necesitas sincronizar múltiples instancias en tiempo real.
La idea realmente importante es que todas las estrategias comparten el mismo patrón: snapshotLoader define de dónde obtener el snapshot y refreshSnapshot() cuándo actualizarlo. exportSnapshot() es simplemente una de las formas de obtener ese snapshot desde código. A partir de ahí, solo tienes que decidir dónde almacenarlo y cómo propagar las actualizaciones.