Skip to content

Endpoint version

A Dynamic Snapshot serves all your content from server memory and hot-refreshes it when you publish.

No rebuild. No API call per read.

The pattern has two pieces:

  • Where the snapshot is read from → the snapshotLoader.
  • Who triggers the refresh → refreshSnapshot().

This is the simplest version of all.

The snapshotLoader pulls directly from the Content Island API with exportSnapshot(). No bucket. No broker. Zero infrastructure.

The refresh is triggered by a GitHub Action that calls a protected endpoint in your app.

It’s plain Express: a pure API with no UI that responds with JSON.

How does it work?

Diagram: Dynamic Snapshot in Express, endpoint version. A GitHub Action calls the refresh endpoint and the snapshotLoader pulls from the API with exportSnapshot()
  • Source: the API itself. The snapshotLoader is async () => exportSnapshot({ accessToken }).
  • Trigger: a POST to the refresh endpoint.

That POST is issued by a GitHub Action that Content Island launches via repository_dispatch when you publish content.

Locally, you simulate it with a curl.

When to choose it?

The most direct option to get started:

  • No additional services.
  • No infrastructure to maintain.

But keep in mind that every refresh:

  • Performs a full export against the API.
  • Only updates the instance that receives the POST.

Step by step

This version is the base Express project, with no extra infrastructure.

The other two start from here. Only the snapshotLoader and the trigger change.

  1. Create the project and install the dependencies. We use tsx to run TypeScript without a build step:

    Terminal window
    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

    Mark the project as ESM and add the scripts to your package.json:

    package.json
    "type": "module",
    "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "tsx src/server.ts"
    }
  2. Create the .env with your read token, a secret for the refresh endpoint and the port. Outside a framework, the server loads it with dotenv:

    .env
    CONTENT_ISLAND_TOKEN=your-read-token
    REFRESH_SECRET=dev-secret
    PORT=3000
  3. Create the client in snapshot mode. This is the only file that changes between versions. The snapshotLoader pulls from the API with exportSnapshot(). Since Express is a single process, a module-level singleton is enough:

    src/lib/content-island.ts
    import { createClient, exportSnapshot } from '@content-island/api-client';
    const accessToken = process.env.CONTENT_ISLAND_TOKEN!;
    // Express runs as a single long-lived process: a module-level singleton is
    // enough to share a client (and a snapshot) across requests.
    export const contentIslandClient = createClient({
    accessToken,
    mode: 'snapshot',
    // The loader pulls the snapshot directly from the Content Island API.
    snapshotLoader: async () => exportSnapshot({ accessToken }),
    });
    // Loads the snapshot the first time the client is used.
    let primed: Promise<unknown> | null = null;
    export function ensureSnapshot() {
    if (!primed) {
    primed = contentIslandClient.refreshSnapshot().catch(err => {
    // Don't cache the failure: a transient error must not break the server
    // forever. Reset so the next request retries.
    primed = null;
    throw err;
    });
    }
    return primed;
    }
  4. Extract the read into a module. getHomeData() ensures the snapshot is in memory and reads from it. Change the contentType to the one in your project (or remove the filter to fetch everything):

    src/lib/content.ts
    import { contentIslandClient, ensureSnapshot } from './content-island';
    export async function getHomeData() {
    await ensureSnapshot();
    const info = await contentIslandClient.getSnapshotInfo();
    // Change the contentType to the one in your project (or remove the filter to fetch everything).
    const posts = await contentIslandClient.getContentList({ contentType: 'post' });
    return { exportedAt: info.exportedAt, count: posts.length };
    }
  5. Set up the server. GET / returns the snapshot data as JSON (pure API, no UI) and POST /api/content-island/refresh, protected with the secret, is what the GitHub Action or the webhook will call:

    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 / -> returns the snapshot data as 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 -> reloads the in-memory snapshot.
    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);
    }
    });
    // Return JSON on errors instead of Express's default HTML page.
    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. Try it. Start the API, query the data and simulate the trigger with a curl:

    Terminal window
    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"

    Publish something in Content Island, repeat the POST and query GET / again: exportedAt will have changed, with no restart and no rebuild. In production, that same POST is made by a GitHub Action launched by the Content Island webhook.

Automate it with a CD workflow

In the step by step you triggered the refresh by hand with curl.

In production that same POST is made by a GitHub Action, which Content Island launches via repository_dispatch every time you publish.

No rebuild, no redeploy. Just a POST to the endpoint you already have.

  1. Create the workflow that calls your refresh endpoint. It listens for the content-refresh event (and workflow_dispatch so you can test it by hand):

    .github/workflows/refresh.yml
    name: Refresh snapshot
    on:
    workflow_dispatch:
    repository_dispatch:
    types: [content-refresh]
    jobs:
    refresh:
    runs-on: ubuntu-latest
    steps:
    - name: POST to the refresh endpoint
    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. Add the repository secrets under Settings → Secrets and variables → Actions:

    SecretValue
    REFRESH_URLthe public URL of your endpoint, e.g. https://your-app.com/api/content-island/refresh
    REFRESH_SECRETthe same value REFRESH_SECRET has in your deployed app
  3. Connect the Content Island webhook. In your project → Webhooks section → new GitHub webhook. The Event name must match the workflow’s types: (content-refresh). Content Island also needs a fine-grained GitHub token with Contents: Read and write permission on the repository to call the repository dispatch API.

    Raw equivalent (useful for testing): Content Island makes this authenticated POST to the GitHub API.

    Terminal window
    curl -X POST https://api.github.com/repos/<owner>/<repo>/dispatches \
    -H "Authorization: Bearer <YOUR_GITHUB_PAT>" \
    -H "Accept: application/vnd.github+json" \
    -d '{"event_type":"content-refresh"}'
  4. Try it. In the Actions tab → refresh.ymlRun workflow (workflow_dispatch): it should finish green with HTTP 200. Then publish something in Content Island and you’ll see a new run launched by repository_dispatch.

Add a static snapshot as a seed

In this version the snapshotLoader pulls from the API on every refresh.

The problem? On a cold start, or in a deployment without a read token, the app has no content until the first export.

The solution: download the snapshot statically at build time and use it as a seed.

  • With a token → the loader fetches live content.
  • Without a token → it serves the JSON embedded in the bundle.
  1. Download the snapshot with the CLI and store it as a versioned file. Add a script to your package.json:

    package.json
    {
    "scripts": {
    "snapshot:export": "content-island export --access-token \"$CONTENT_ISLAND_TOKEN\" --snapshot-path content-island-snapshot.json"
    }
    }
    Terminal window
    npm run snapshot:export # generates content-island-snapshot.json
  2. Make the snapshotLoader hybrid: with a token it fetches live content, and without one it serves the snapshot from the 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. Refresh the seed in your CD. Add a step that regenerates the snapshot before building, so the versioned JSON doesn’t go stale:

    .github/workflows/deploy.yml
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: 20
    - run: npm ci
    - name: Export static snapshot
    env:
    CONTENT_ISLAND_TOKEN: ${{ secrets.CONTENT_ISLAND_TOKEN }}
    run: npm run snapshot:export
    - run: npm run build
    # ...deploy

Example

Full code: content-island/examples-dynamic-snapshotexpress/01-api-load.

References