Versión por endpoint
Un Dynamic Snapshot sirve todo el contenido desde la memoria del servidor y lo refresca en caliente cuando publicas.
Sin rebuild. Sin una llamada al API por lectura.
El patrón tiene dos piezas:
- De dónde se lee el snapshot → el
snapshotLoader. - Quién lanza el refresco →
refreshSnapshot().
Esta es la versión más simple de todas.
El snapshotLoader tira directamente del API de Content Island con exportSnapshot(). Sin bucket. Sin broker. Cero infraestructura.
El refresco lo lanza una GitHub Action que llama a un endpoint protegido de tu app.
¿Cómo funciona?
- Origen: el propio API. El
snapshotLoaderesasync () => exportSnapshot({ accessToken }). - Trigger: un
POSTal endpoint de refresco.
Ese POST lo emite una GitHub Action que Content Island lanza vía repository_dispatch al publicar contenido.
En local lo simulas con un curl.
¿Cuándo elegirla?
La opción más directa para empezar:
- Sin servicios adicionales.
- Sin infraestructura que mantener.
Pero ten en cuenta que cada refresco:
- Hace un
exportcompleto contra el API. - Solo actualiza la instancia que recibe el
POST.
Paso a paso
Esta versión es el proyecto base de Next.js, sin infraestructura extra.
Las otras dos parten de aquí. Solo cambian el snapshotLoader y el trigger.
-
Crea el proyecto (App Router, TypeScript) e instala el cliente:
Ventana de terminal npx create-next-app@latest content-island-dynamiccd content-island-dynamicnpm i @content-island/api-client -
Crea el
.envcon tu token de lectura y un secreto para el endpoint de refresco. Next.js lo carga solo, no hace faltadotenv:.env CONTENT_ISLAND_TOKEN=tu-token-de-lecturaREFRESH_SECRET=dev-secret -
Crea el cliente en modo snapshot. Este es el único fichero que cambia entre versiones. El
snapshotLoadertira del API conexportSnapshot(). Guardamos el cliente englobalThispara que el endpoint de refresco y la página compartan la misma instancia y, por tanto, el mismo snapshot en memoria:lib/content-island.ts import { createClient, exportSnapshot } from '@content-island/api-client';const accessToken = process.env.CONTENT_ISLAND_TOKEN!;// Guardamos el cliente en globalThis para que la API de refresco y la página// usen la misma instancia y, por tanto, el mismo snapshot en memoria.const globalForClient = globalThis as unknown as {contentIslandClient?: ReturnType<typeof createClient>;primed?: Promise<unknown> | null;};export const contentIslandClient =globalForClient.contentIslandClient ??createClient({accessToken,mode: 'snapshot',snapshotLoader: async () => exportSnapshot({ accessToken }),});globalForClient.contentIslandClient = contentIslandClient;// Carga el snapshot la primera vez que se usa el cliente.export function ensureSnapshot() {if (!globalForClient.primed) globalForClient.primed = contentIslandClient.refreshSnapshot();return globalForClient.primed;} -
Extrae la lectura a un módulo de servidor.
getHomeData()asegura el snapshot en memoria y lee de él. Cambia elcontentTypepor el de tu proyecto (o quita el filtro para traer todo):lib/content.ts import { contentIslandClient, ensureSnapshot } from './content-island';export async function getHomeData() {await ensureSnapshot();const info = await contentIslandClient.getSnapshotInfo();// Cambia el contentType por el de tu proyecto (o quita el filtro para traer todo).const posts = await contentIslandClient.getContentList({ contentType: 'post' });return { exportedAt: info.exportedAt, count: posts.length };} -
Léelo en un Server Component. El
exportedAtte deja ver de un vistazo cuándo se refrescó. Marca la página como dinámica para que refleje el snapshot en memoria en cada request:app/page.tsx import { getHomeData } from '@/lib/content';export const dynamic = 'force-dynamic';export default async function Home() {const { exportedAt, count } = await getHomeData();return (<main><p>Snapshot exportado: {exportedAt}</p><p>Contenidos en memoria: {count}</p></main>);} -
Crea el endpoint de refresco como Route Handler, protegido con el secreto. Es lo que llamarán la GitHub Action o el webhook:
// app/api/content-island/refresh/route.ts -> POST /api/content-island/refreshimport { NextResponse } from 'next/server';import { contentIslandClient } from '@/lib/content-island';export async function POST(request: Request) {if (request.headers.get('x-refresh-secret') !== process.env.REFRESH_SECRET) {return new NextResponse('Unauthorized', { status: 401 });}const result = await contentIslandClient.refreshSnapshot();return NextResponse.json(result); // { status: 'updated' | 'unchanged', meta }} -
Pruébalo. Arranca la app y simula el trigger con un
curl:Ventana de terminal npm run dev # http://localhost:3000curl -fsS -X POST http://localhost:3000/api/content-island/refresh \-H "x-refresh-secret: dev-secret"Publica algo en Content Island, repite el
curly recarga la página:exportedAthabrá cambiado, sin reiniciar ni rebuild. En producción, ese mismoPOSTlo hace una GitHub Action lanzada por el webhook de Content Island.
Automatízalo con un workflow de CD
En el paso a paso lanzabas el refresco a mano con curl.
En producción ese mismo POST lo hace una GitHub Action, que Content Island lanza vía repository_dispatch cada vez que publicas.
Ni rebuild ni redeploy. Solo un POST al endpoint que ya tienes.
-
Crea el workflow que llama a tu endpoint de refresco. Escucha el evento
content-refresh(yworkflow_dispatchpara poder probarlo a mano):.github/workflows/refresh.yml name: Refrescar snapshoton:workflow_dispatch:repository_dispatch:types: [content-refresh]jobs:refresh:runs-on: ubuntu-lateststeps:- name: POST al endpoint de refrescoenv:REFRESH_URL: ${{ secrets.REFRESH_URL }}REFRESH_SECRET: ${{ secrets.REFRESH_SECRET }}run: |response=$(curl -sS -o /tmp/body -w "%{http_code}" -X POST "$REFRESH_URL" \-H "x-refresh-secret: $REFRESH_SECRET")echo "HTTP $response"cat /tmp/body; echo[ "$response" = "200" ] || exit 1 -
Añade los secrets del repositorio en Settings → Secrets and variables → Actions:
Secret Valor REFRESH_URLla URL pública de tu endpoint, p. ej. https://tu-app.com/api/content-island/refreshREFRESH_SECRETel mismo valor que REFRESH_SECRETtiene en tu app desplegada -
Conecta el webhook de Content Island. En tu proyecto → sección Webhooks → nuevo webhook de GitHub. El Event name debe coincidir con el
types:del workflow (content-refresh). Content Island también necesita un token de GitHub fine-grained con permiso Contents: Read and write sobre el repositorio para llamar a la API de repository dispatch.Equivalente en crudo (útil para probarlo): Content Island hace este
POSTautenticado a la API de GitHub.Ventana de terminal curl -X POST https://api.github.com/repos/<owner>/<repo>/dispatches \-H "Authorization: Bearer <TU_PAT_DE_GITHUB>" \-H "Accept: application/vnd.github+json" \-d '{"event_type":"content-refresh"}' -
Pruébalo. En la pestaña Actions →
refresh.yml→ Run workflow (workflow_dispatch): debe terminar en verde conHTTP 200. Luego publica algo en Content Island y verás una ejecución nueva lanzada porrepository_dispatch.
Añade un snapshot estático como semilla
En esta versión el snapshotLoader tira del API en cada refresco.
¿El problema? En un arranque en frío, o en un despliegue sin token de lectura, la app no tiene contenido hasta el primer export.
La solución: descarga el snapshot de forma estática en el build y úsalo como semilla.
- Con token → el loader pide contenido en vivo.
- Sin token → sirve el JSON incrustado en el bundle.
-
Descarga el snapshot con la CLI y guárdalo como un fichero versionado. Añade un script a tu
package.json:package.json {"scripts": {"snapshot:export": "content-island export --access-token \"$CONTENT_ISLAND_TOKEN\" --snapshot-path content-island-snapshot.json"}}Ventana de terminal npm run snapshot:export # genera content-island-snapshot.json -
Haz el
snapshotLoaderhíbrido: con token pide contenido en vivo y sin token sirve el snapshot del bundle. Mantén el cliente englobalThiscomo en el paso a paso:lib/content-island.ts import { type ContentSnapshot, createClient, exportSnapshot } from '@content-island/api-client';import snapshot from '../content-island-snapshot.json' with { type: 'json' };const accessToken = process.env.CONTENT_ISLAND_TOKEN;const globalForClient = globalThis as unknown as {contentIslandClient?: ReturnType<typeof createClient>;primed?: Promise<unknown> | null;};export const contentIslandClient =globalForClient.contentIslandClient ??createClient({accessToken: accessToken ?? 'snapshot-mode',mode: 'snapshot',snapshotLoader: accessToken? async () => exportSnapshot({ accessToken }): async () => snapshot as ContentSnapshot,});globalForClient.contentIslandClient = contentIslandClient; -
Refresca la semilla en el CD. Añade un paso que regenere el snapshot antes de construir, para que el JSON versionado no se quede obsoleto:
.github/workflows/deploy.yml steps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 20- run: npm ci- name: Exportar snapshot estáticoenv:CONTENT_ISLAND_TOKEN: ${{ secrets.CONTENT_ISLAND_TOKEN }}run: npm run snapshot:export- run: npm run build# ...despliega
Ejemplo
El repositorio de ejemplos implementa el patrón en TanStack Start; los pasos anteriores son la adaptación a Next.js (solo cambia el pegamento del framework): content-island/examples-dynamic-snapshot.