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:
| Mode | Reads come from | Default |
|---|---|---|
'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
ContentSnapshotobject.
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:
| Configuration | First read | Later use of snapshotLoader |
|---|---|---|
snapshotLoader only | Runs the loader and loads the remote snapshot | Can run again through refreshSnapshot() |
snapshotPath and snapshotLoader | Loads the snapshot from the local file | The 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:
status | Meaning | meta contains |
|---|---|---|
'updated' | The loaded snapshot was newer and is now active | the metadata of the new snapshot |
'unchanged' | The loaded snapshot was the same age as, or older than, the active snapshot and was ignored | the 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:
| Situation | Reason |
|---|---|
the snapshot has a different projectId | an update cannot switch the client to another project |
the snapshot has a different view | an update cannot switch between published and preview |
the snapshotLoader fails | the remote snapshot could not be retrieved |
| the returned content is not valid JSON | the client cannot parse it |
| the snapshot has an invalid structure | the content does not match the expected snapshot format |
| the schema version is not supported | the client cannot use that snapshot version |
the client is running in 'api' mode | API clients do not maintain an active snapshot |
no snapshotLoader was configured | there 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
modeoption oncreateClientsets the default for all reads. -
Per read — the five read methods accept a
modeinside the query object, affecting only that one call:getContentListgetContentgetRawContentListgetRawContentgetContentListSize
// 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.partial—truewhen 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-DepthandX-Related-Content-Partialresponse headers (an absentpartialheader is treated asfalse). - 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:
createContentpublishContentupdateContentFieldValueuploadMediacreateModelupdateModeldeleteModelcreateEnumupdateEnumdeleteEnum
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:
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 withnpx @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:
| Flag | Description | Default |
|---|---|---|
--access-token | Required. The access token for the export. Falls back to the env var below. | env CONTENT_ISLAND_ACCESS_TOKEN |
--snapshot-path | Where the snapshot file is written. | ./content-island-snapshot.json |
--domain | Override the Content Island domain (for self-hosted instances). | — |
--secure-protocol | Use HTTPS explicitly (this is the default). | HTTPS |
--no-secure-protocol | Use HTTP instead of HTTPS (local / self-hosted instances). | — |
--api-version | Override 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:
npx content-island export \ --access-token <your-read-token> \ --domain localhost:3000 \ --no-secure-protocolOn 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