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?
- Source: the API itself. The
snapshotLoaderisasync () => exportSnapshot({ accessToken }). - Trigger: a
POSTto 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
exportagainst 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.
-
Create the project and install the dependencies. We use
tsxto run TypeScript without a build step:Terminal window mkdir content-island-dynamic && cd content-island-dynamicnpm init -ynpm i express @content-island/api-clientnpm i -D typescript tsx @types/node @types/express dotenvMark 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"} -
Create the
.envwith your read token, a secret for the refresh endpoint and the port. Outside a framework, the server loads it withdotenv:.env CONTENT_ISLAND_TOKEN=your-read-tokenREFRESH_SECRET=dev-secretPORT=3000 -
Create the client in snapshot mode. This is the only file that changes between versions. The
snapshotLoaderpulls from the API withexportSnapshot(). 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;} -
Extract the read into a module.
getHomeData()ensures the snapshot is in memory and reads from it. Change thecontentTypeto 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 };} -
Set up the server.
GET /returns the snapshot data as JSON (pure API, no UI) andPOST /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}`);}); -
Try it. Start the API, query the data and simulate the trigger with a
curl:Terminal window npm run dev # http://localhost:3000curl 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
POSTand queryGET /again:exportedAtwill have changed, with no restart and no rebuild. In production, that samePOSTis 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.
-
Create the workflow that calls your refresh endpoint. It listens for the
content-refreshevent (andworkflow_dispatchso you can test it by hand):.github/workflows/refresh.yml name: Refresh snapshoton:workflow_dispatch:repository_dispatch:types: [content-refresh]jobs:refresh:runs-on: ubuntu-lateststeps:- name: POST to the refresh endpointenv: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 -
Add the repository secrets under Settings → Secrets and variables → Actions:
Secret Value REFRESH_URLthe public URL of your endpoint, e.g. https://your-app.com/api/content-island/refreshREFRESH_SECRETthe same value REFRESH_SECREThas in your deployed app -
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
POSTto 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"}' -
Try it. In the Actions tab →
refresh.yml→ Run workflow (workflow_dispatch): it should finish green withHTTP 200. Then publish something in Content Island and you’ll see a new run launched byrepository_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.
-
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 -
Make the
snapshotLoaderhybrid: 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,}); -
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@v4with:node-version: 20- run: npm ci- name: Export static snapshotenv:CONTENT_ISLAND_TOKEN: ${{ secrets.CONTENT_ISLAND_TOKEN }}run: npm run snapshot:export- run: npm run build# ...deploy
Example
Full code: content-island/examples-dynamic-snapshot → express/01-api-load.