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.

How does it work?

Diagram: Dynamic Snapshot in TanStack Start, 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 TanStack Start 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 client:

    Terminal window
    npx @tanstack/cli create content-island-dynamic
    cd content-island-dynamic
    npm i @content-island/api-client dotenv
  2. Create the .env with your read token and a secret for the refresh endpoint:

    .env
    CONTENT_ISLAND_TOKEN=your-read-token
    REFRESH_SECRET=put-a-unique-and-secure-secret-here
  3. Create the client in snapshot mode. This is the only file that changes between versions. The snapshotLoader pulls from the API with 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 }),
    });
    // Loads the snapshot the first time the client is used on the server.
    let primed: Promise<unknown> | null = null;
    export function ensureSnapshot() {
    if (!primed) primed = contentIslandClient.refreshSnapshot();
    return primed;
    }
  4. Read from the snapshot with a server function and show it on a page. The exportedAt lets you see at a glance when it was refreshed:

    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 exported: {exportedAt}</p>
    <p>Entries in memory: {count}</p>
    </main>
    );
    }
  5. Create the refresh endpoint, protected with the secret. This is what the GitHub Action or the webhook will call:

    // 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. Try it. Start the app and simulate the trigger with a curl:

    Terminal window
    npm run dev # http://localhost:3000
    curl -fsS -X POST http://localhost:3000/api/content-island/refresh \
    -H "x-refresh-secret: put-a-unique-and-secure-secret-here"

    Publish something in Content Island, repeat the curl and reload the page: 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/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. 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-snapshot01-api-load.

References