Saltearse al contenido

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?

Diagrama: Dynamic Snapshot en TanStack Start con versión por endpoint. Una GitHub Action llama al endpoint de refresco y el snapshotLoader tira del API con exportSnapshot()
  • Origen: el propio API. El snapshotLoader es async () => exportSnapshot({ accessToken }).
  • Trigger: un POST al 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 export completo 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.

  1. Crea el proyecto e instala el cliente:

    Ventana de terminal
    npx @tanstack/cli create content-island-dynamic
    cd content-island-dynamic
    npm i @content-island/api-client dotenv
  2. Crea el .env con tu token de lectura y un secreto para el endpoint de refresco:

    .env
    CONTENT_ISLAND_TOKEN=tu-token-de-lectura
    REFRESH_SECRET=pon-aqui-un-secreto-unico-y-seguro
  3. Crea el cliente en modo snapshot. Este es el único fichero que cambia entre versiones. El snapshotLoader tira del API con exportSnapshot():

    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;
    }
  4. Lee del snapshot con una server function y muéstralo en una página. El exportedAt te 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>
    );
    }
  5. 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/refresh
    import { 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 }
    },
    },
    },
    });
  6. Pruébalo. Arranca la app y simula el trigger con un curl:

    Ventana de terminal
    npm run dev # http://localhost:3000
    curl -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 curl y recarga la página: exportedAt habrá cambiado, sin reiniciar ni rebuild. En producción, ese mismo POST lo 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.

  1. Crea el workflow que llama a tu endpoint de refresco. Escucha el evento content-refresh (y workflow_dispatch para poder probarlo a mano):

    .github/workflows/refresh.yml
    name: Refrescar snapshot
    on:
    workflow_dispatch:
    repository_dispatch:
    types: [content-refresh]
    jobs:
    refresh:
    runs-on: ubuntu-latest
    steps:
    - name: POST al endpoint de refresco
    env:
    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
  2. Añade los secrets del repositorio en Settings → Secrets and variables → Actions:

    SecretValor
    REFRESH_URLla URL pública de tu endpoint, p. ej. https://tu-app.com/api/content-island/refresh
    REFRESH_SECRETel mismo valor que REFRESH_SECRET tiene en tu app desplegada
  3. 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 POST autenticado 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"}'
  4. Pruébalo. En la pestaña Actionsrefresh.ymlRun workflow (workflow_dispatch): debe terminar en verde con HTTP 200. Luego publica algo en Content Island y verás una ejecución nueva lanzada por repository_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.
  1. 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
  2. Haz el snapshotLoader hí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,
    });
  3. 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@v4
    with:
    node-version: 20
    - run: npm ci
    - name: Exportar snapshot estático
    env:
    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-snapshot01-api-load.

Referencias