Skip to content

Snapshot mode

Snapshot mode lets the Content Island API client serve all of its read methods from a local file instead of calling the API on every request. A project’s full content is exported once as a single JSON document — the snapshot (transferred gzip-compressed from the export endpoint, then stored as plain JSON on disk) — and the client reads from it with drop-in parity to the live API: the same methods, the same shapes, the same results.

Snapshot mode is designed for applications that need to read content frequently without making an API request on every operation.

Export the project once, keep the snapshot locally, and resolve all read operations directly from that snapshot. This reduces latency, eliminates unnecessary API traffic, and makes applications more resilient to network issues, rate limits, or temporary API outages.

Static site generation is a natural fit, but the same approach also works for SSR, ISR, server functions, background jobs, and other server-side workloads.

Choosing a mode

The client operates in one of two modes:

ModeReads come fromDefault
'api'The live Content Island REST API.yes
'snapshot'A local snapshot file (snapshotPath).

You set the mode when you create the client:

import { createClient } from '@content-island/api-client';
const client = createClient({
accessToken: 'YOUR_ACCESS_TOKEN',
mode: 'snapshot',
});

In 'snapshot' mode reads are served from the local snapshot file with no request to the API and with drop-in parity to the live results. When mode is omitted, the client defaults to 'api' and behaves exactly as it always has.

The snapshot file

In snapshot mode the client reads from a snapshot file on disk. You point to it with the optional snapshotPath option:

const client = createClient({
accessToken: 'YOUR_ACCESS_TOKEN',
mode: 'snapshot',
snapshotPath: './content-island-snapshot.json',
});

snapshotPath is optional and defaults to './content-island-snapshot.json'. It is not required: omitting it simply uses the default path. An error is raised only when the file at that path is missing, unreadable or invalid — never just because the option was left out.

Remote snapshot loading

snapshotPath loads a snapshot from a file on the local file system. However, a snapshot can also be stored in a remote location, such as Amazon S3, Azure Blob Storage, an HTTP endpoint, or a database.

To load a snapshot from an external source, configure a snapshotLoader:

import { createClient } from '@content-island/api-client';
const client = createClient({
accessToken: 'YOUR_ACCESS_TOKEN',
mode: 'snapshot',
snapshotLoader: async () => {
const response = await fetch('https://storage.example.com/content-island-snapshot.json');
return response.text();
},
});

snapshotLoader is an asynchronous function provided by your application. The client calls it whenever it needs to load or update the snapshot.

The function can return either:

  • a string containing the snapshot JSON;
  • an already-parsed ContentSnapshot object.

If the loader returns a string, the client parses it as JSON. In both cases, the returned content is validated before it becomes the active snapshot.

Initial loading behavior

The first read behaves differently depending on the client configuration:

ConfigurationFirst readLater use of snapshotLoader
snapshotLoader onlyRuns the loader and loads the remote snapshotCan run again through refreshSnapshot()
snapshotPath and snapshotLoaderLoads the snapshot from the local fileThe loader runs when refreshSnapshot() is called

Using both snapshotPath and snapshotLoader is especially useful for long-running SSR servers and backend processes.

The application can start with a snapshot included in the deployment and load a newer version later, without making a remote request during startup or restarting the process.

Updating the active snapshot

Call refreshSnapshot() to run the configured snapshotLoader again and check for a newer snapshot:

const result = await client.refreshSnapshot();
// {
// status: 'updated' | 'unchanged',
// meta: { ... }
// }

The method returns one of the following results:

statusMeaningmeta contains
'updated'The loaded snapshot was newer and is now activethe metadata of the new snapshot
'unchanged'The loaded snapshot was the same age as, or older than, the active snapshot and was ignoredthe metadata of the snapshot that remains active

An 'unchanged' result is not an error and does not throw an exception.

This prevents a delayed or out-of-order refresh from replacing newer content with older content.

See the refreshSnapshot() reference for its complete interface, return values, and error behavior.

When an update fails

refreshSnapshot() throws an ApiClientError and preserves the active snapshot when it cannot load or accept the new content.

An update can fail in the following situations:

SituationReason
the snapshot has a different projectIdan update cannot switch the client to another project
the snapshot has a different viewan update cannot switch between published and preview
the snapshotLoader failsthe remote snapshot could not be retrieved
the returned content is not valid JSONthe client cannot parse it
the snapshot has an invalid structurethe content does not match the expected snapshot format
the schema version is not supportedthe client cannot use that snapshot version
the client is running in 'api' modeAPI clients do not maintain an active snapshot
no snapshotLoader was configuredthere is no source from which to load an updated snapshot

If the loader throws an error, the original error is available through error.cause.

When to update the snapshot

The library does not refresh snapshots automatically. Your application decides when to call refreshSnapshot().

Common strategies include:

  • After publishing content: configure the CMS to call an application endpoint through a webhook, allowing the snapshot to be updated immediately.
  • At regular intervals: use a timer to check periodically for a newer snapshot.
  • Manually: start the update from an administration action or an internal endpoint.

Concurrent reads and updates

A read performed while refreshSnapshot() is running uses either the complete previous snapshot or the complete new snapshot. It never uses a partially loaded snapshot.

Calling refreshSnapshot() multiple times concurrently is also safe. Concurrent calls share the refresh operation, and the newest valid snapshot remains active.

If a client configured with only a snapshotLoader has not loaded a snapshot yet, the first call to refreshSnapshot() loads the initial snapshot and makes it active.

Where the mode is set

The mode lives in two places:

  • Client level — the mode option on createClient sets the default for all reads.

  • Per read — the five read methods accept a mode inside the query object, affecting only that one call:

    • getContentList
    • getContent
    • getRawContentList
    • getRawContent
    • getContentListSize
// The client reads from the API by default;
// this one read is served from the snapshot:
const client = createClient({
accessToken: 'YOUR_ACCESS_TOKEN',
snapshotPath: './content-island-snapshot.json',
});
const posts = await client.getContentList({
contentType: 'post',
mode: 'snapshot', // this read only
});

getProject does run in snapshot mode — it’s served from the snapshot’s project object (languages included). It simply takes no query-params object, so there is no per-read mode for it: it always follows the client-level mode. To inspect the on-disk snapshot without loading the project, use getSnapshotInfo.

onRelatedContentMeta

onRelatedContentMeta is an optional per-query callback that reports how the related-content resolution went. It runs on the same five read methods and has the signature:

onRelatedContentMeta: ({ resolvedDepth, partial }) => void;

It is invoked exactly once per call:

  • resolvedDepth — how deep the related-content resolution actually went.
  • partialtrue when a depth or resolution-budget cap left part of the related-content graph unresolved.

The callback works in both modes, and for the same data and query it reports identical values:

  • In api mode, the values come from the X-Related-Content-Resolved-Depth and X-Related-Content-Partial response headers (an absent partial header is treated as false).
  • In snapshot mode, the values come from the local breadth-first resolution over the snapshot.
const list = await client.getContentList({
contentType: 'post',
includeRelatedContent: 'all',
onRelatedContentMeta: ({ resolvedDepth, partial }) => {
console.log({ resolvedDepth, partial });
},
});

When onRelatedContentMeta is omitted, behaviour and return shapes are unchanged. Like mode, it is a client-only option and is never serialized to the query string.

Writes are not available in snapshot mode

A snapshot-mode client serves reads only. A snapshot is a frozen, read-only copy of your content, so there is nothing to write to. Every write and schema-management method rejects with an ApiClientError whose code is SNAPSHOT_MODE, and it performs no network call — the rejection happens locally, before any request would be made:

  • createContent
  • publishContent
  • updateContentFieldValue
  • uploadMedia
  • createModel
  • updateModel
  • deleteModel
  • createEnum
  • updateEnum
  • deleteEnum
import { createClient, ApiClientError } from '@content-island/api-client';
const client = createClient({
accessToken: 'YOUR_ACCESS_TOKEN',
mode: 'snapshot',
});
try {
await client.createContent(/* … */);
} catch (error) {
if (error instanceof ApiClientError && error.code === 'SNAPSHOT_MODE') {
// Writes are rejected in snapshot mode — no request was sent.
}
}

If you need to read from a snapshot and also write, create a second, separate client in 'api' mode for the writes, authenticated with a Write Token:

import { createClient } from '@content-island/api-client';
// Reader: serves reads from the local snapshot.
const reader = createClient({
accessToken: 'YOUR_READ_TOKEN',
mode: 'snapshot',
});
// Writer: a separate api-mode client for writes.
const writer = createClient({
accessToken: 'YOUR_WRITE_TOKEN',
// mode defaults to 'api'
});
const posts = await reader.getContentList({ contentType: 'post' });
await writer.createContent(/* … */);

Exporting a snapshot from the CLI

Generate a snapshot with the content-island export command. It exports your project’s content and writes it to disk:

Terminal window
npx content-island export \
--access-token <your-read-token> \
--snapshot-path ./content-island-snapshot.json

--snapshot-path is optional; if you omit it, the snapshot is written to ./content-island-snapshot.json, relative to the current working directory.

Before you can run this command, you need to install the Content Island API client package with npm i @content-island/api-client, or run it directly with npx @content-island/api-client export --access-token <your-read-token>.

The command mirrors the exportSnapshot() programmatic API one-to-one (the CLI is built on it). Its flags are:

FlagDescriptionDefault
--access-tokenRequired. The access token for the export. Falls back to the env var below.env CONTENT_ISLAND_ACCESS_TOKEN
--snapshot-pathWhere the snapshot file is written../content-island-snapshot.json
--domainOverride the Content Island domain (for self-hosted instances).
--secure-protocolUse HTTPS explicitly (this is the default).HTTPS
--no-secure-protocolUse HTTP instead of HTTPS (local / self-hosted instances).
--api-versionOverride the API version used for the export.

The export uses HTTPS by default. Pass --no-secure-protocol only for a local or self-hosted instance served over plain HTTP:

Terminal window
npx content-island export \
--access-token <your-read-token> \
--domain localhost:3000 \
--no-secure-protocol

On success the command prints a summary — the output path, the file size, the exportedAt timestamp and the view — and exits 0.

If the token is missing, the request fails, or the downloaded snapshot fails validation, the command writes the error to stderr (standard error) and exits with a non-zero exit code. Because it writes through a temporary file that is renamed into place only after a successful, validated download, a failed run leaves no partial or invalid file at --snapshot-path: either you get a complete snapshot or the previous file is left untouched.

A common workflow

One common setup — it depends on your team and project — is api mode in local development and snapshot mode in production builds:

  • Local dev (mode: 'api') — always fresh content, no export step to remember.
  • Production build (mode: 'snapshot') — fast, repeatable, with zero per-request network calls.

Drive the choice from an environment variable so the same code runs in both:

import { createClient } from '@content-island/api-client';
const client = createClient({
accessToken: process.env.CONTENT_ISLAND_ACCESS_TOKEN,
mode: process.env.NODE_ENV === 'production' ? 'snapshot' : 'api',
});

In production, generate the snapshot first (an export step), then run your build so it reads from the freshly exported file.

GitHub Action

This workflow exports a snapshot at build time, then builds your site in snapshot mode:

name: Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
- run: npm ci
- name: Export content snapshot
env:
CONTENT_ISLAND_ACCESS_TOKEN: ${{ secrets.CONTENT_ISLAND_ACCESS_TOKEN }}
run: npx content-island export
- name: Build (snapshot mode)
env:
NODE_ENV: production
run: npm run build