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 TanStack Start, sin infraestructura extra.
Las otras dos parten de aquí. Solo cambian el snapshotLoader y el trigger.
-
Crea el proyecto e instala el cliente:
Ventana de terminal npx @tanstack/cli create content-island-dynamiccd content-island-dynamicnpm i @content-island/api-client dotenv -
Crea el
.envcon tu token de lectura y un secreto para el endpoint de refresco:.env CONTENT_ISLAND_TOKEN=tu-token-de-lecturaREFRESH_SECRET=pon-aqui-un-secreto-unico-y-seguro -
Crea el cliente en modo snapshot. Este es el único fichero que cambia entre versiones. El
snapshotLoadertira del API conexportSnapshot():src/server/content-island.ts import 'dotenv/config';import { createClient, exportSnapshot } from '@content-island/api-client';const accessToken = process.env.CONTENT_ISLAND_TOKEN!;export const contentIslandClient = createClient({accessToken,mode: 'snapshot',snapshotLoader: async () => exportSnapshot({ accessToken }),});// Carga el snapshot la primera vez que se usa el cliente en el servidor.let primed: Promise<unknown> | null = null;export function ensureSnapshot() {if (!primed) primed = contentIslandClient.refreshSnapshot();return primed;} -
Lee del snapshot con una server function y muéstralo en una página. El
exportedAtte deja ver de un vistazo cuándo se refrescó:src/server/content.ts import { createServerFn } from '@tanstack/react-start';import { contentIslandClient, ensureSnapshot } from './content-island';export const getHomeData = createServerFn({ method: 'GET' }).handler(async () => {await ensureSnapshot();const info = await contentIslandClient.getSnapshotInfo();const posts = await contentIslandClient.getContentList({ contentType: 'post' });return { exportedAt: info.exportedAt, count: posts.length };});src/routes/index.tsx import { createFileRoute } from '@tanstack/react-router';import { getHomeData } from '../server/content';export const Route = createFileRoute('/')({loader: () => getHomeData(),component: Home,});function Home() {const { exportedAt, count } = Route.useLoaderData();return (<main><p>Snapshot exportado: {exportedAt}</p><p>Contenidos en memoria: {count}</p></main>);} -
Crea el endpoint de refresco, protegido con el secreto. Es lo que llamarán la GitHub Action o el webhook:
// src/routes/api.content-island.refresh.ts -> POST /api/content-island/refreshimport { createFileRoute } from '@tanstack/react-router';import { contentIslandClient } from '../server/content-island';export const Route = createFileRoute('/api/content-island/refresh')({server: {handlers: {POST: async ({ request }) => {if (request.headers.get('x-refresh-secret') !== process.env.REFRESH_SECRET) {return new Response('Unauthorized', { status: 401 });}const result = await contentIslandClient.refreshSnapshot();return Response.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: pon-aqui-un-secreto-unico-y-seguro"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:src/server/content-island.ts import 'dotenv/config';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;export const contentIslandClient = createClient({accessToken: accessToken ?? 'snapshot-mode',mode: 'snapshot',snapshotLoader: accessToken? async () => exportSnapshot({ accessToken }): async () => snapshot as ContentSnapshot,}); -
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
Código completo: content-island/examples-dynamic-snapshot → 01-api-load.