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.

Es Express plano: una API pura sin UI que responde JSON.

¿Cómo funciona?

Diagrama: Dynamic Snapshot en Express 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 Express, sin infraestructura extra.

Las otras dos parten de aquí. Solo cambian el snapshotLoader y el trigger.

  1. Crea el proyecto e instala las dependencias. Usamos tsx para ejecutar TypeScript sin paso de build:

    Ventana de terminal
    mkdir content-island-dynamic && cd content-island-dynamic
    npm init -y
    npm i express @content-island/api-client
    npm i -D typescript tsx @types/node @types/express dotenv

    Marca el proyecto como ESM y añade los scripts en el package.json:

    package.json
    "type": "module",
    "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "tsx src/server.ts"
    }
  2. Crea el .env con tu token de lectura, un secreto para el endpoint de refresco y el puerto. Fuera de un framework, el servidor lo carga con dotenv:

    .env
    CONTENT_ISLAND_TOKEN=tu-token-de-lectura
    REFRESH_SECRET=dev-secret
    PORT=3000
  3. Crea el cliente en modo snapshot. Este es el único fichero que cambia entre versiones. El snapshotLoader tira del API con exportSnapshot(). Como Express es un proceso único, basta un singleton a nivel de módulo:

    src/lib/content-island.ts
    import { createClient, exportSnapshot } from '@content-island/api-client';
    const accessToken = process.env.CONTENT_ISLAND_TOKEN!;
    // Express corre como un único proceso de larga vida: un singleton a nivel de
    // módulo basta para compartir un cliente (y un snapshot) entre peticiones.
    export const contentIslandClient = createClient({
    accessToken,
    mode: 'snapshot',
    // El loader tira del snapshot directamente del API de Content Island.
    snapshotLoader: async () => exportSnapshot({ accessToken }),
    });
    // Carga el snapshot la primera vez que se usa el cliente.
    let primed: Promise<unknown> | null = null;
    export function ensureSnapshot() {
    if (!primed) {
    primed = contentIslandClient.refreshSnapshot().catch(err => {
    // No cacheamos el fallo: un error transitorio no debe romper el servidor
    // para siempre. Reseteamos para que el siguiente request lo reintente.
    primed = null;
    throw err;
    });
    }
    return primed;
    }
  4. Extrae la lectura a un módulo. getHomeData() asegura el snapshot en memoria y lee de él. Cambia el contentType por el de tu proyecto (o quita el filtro para traer todo):

    src/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 };
    }
  5. Monta el servidor. GET / devuelve los datos del snapshot en JSON (API pura, sin UI) y POST /api/content-island/refresh, protegido con el secreto, es lo que llamarán la GitHub Action o el webhook:

    src/server.ts
    import 'dotenv/config';
    import express, { type ErrorRequestHandler } from 'express';
    import { getHomeData } from './lib/content';
    import { contentIslandClient } from './lib/content-island';
    const app = express();
    // GET / -> devuelve los datos del snapshot en JSON.
    app.get('/', async (_req, res, next) => {
    try {
    const data = await getHomeData(); // { exportedAt, count }
    res.json(data);
    } catch (err) {
    next(err);
    }
    });
    // POST /api/content-island/refresh -> recarga el snapshot en memoria.
    app.post('/api/content-island/refresh', async (req, res, next) => {
    try {
    if (req.get('x-refresh-secret') !== process.env.REFRESH_SECRET) {
    res.status(401).send('Unauthorized');
    return;
    }
    const result = await contentIslandClient.refreshSnapshot();
    res.json(result); // { status: 'updated' | 'unchanged', meta }
    } catch (err) {
    next(err);
    }
    });
    // Devuelve JSON en los errores en vez de la página HTML por defecto de Express.
    const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
    console.error(err);
    res.status(500).json({ error: 'Internal Server Error' });
    };
    app.use(errorHandler);
    const port = Number(process.env.PORT) || 3000;
    app.listen(port, () => {
    console.log(`▶ http://localhost:${port}`);
    });
  6. Pruébalo. Arranca la API, consulta los datos y simula el trigger con un curl:

    Ventana de terminal
    npm run dev # http://localhost:3000
    curl http://localhost:3000/ # { "exportedAt": "...", "count": N }
    curl -fsS -X POST http://localhost:3000/api/content-island/refresh \
    -H "x-refresh-secret: dev-secret"

    Publica algo en Content Island, repite el POST y vuelve a consultar GET /: 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/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;
    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-snapshotexpress/01-api-load.

Referencias