Skip to content
brix.v2 stable

Search documentation

70 guides

Documentation menu

Contentful Clients

Use typed Delivery and Management clients for published or draft reads, content-model inspection, caching, retries, and incremental synchronization.

Configure Delivery or Preview reads

Create a request-derived service so credentials and the configured server cache follow the request context:

import { createContentfulClient } from "brix/contentful";
import { brixServer } from "#brix/server";
import type { ContentTypeMap } from "#/contentful-types.ts";

export const getContentful = brixServer.createService((c) =>
  createContentfulClient<ContentTypeMap>({
    spaceId: c.env.CONTENTFUL_SPACE_ID,
    accessToken: c.env.CONTENTFUL_ACCESS_TOKEN,
    environment: c.env.CONTENTFUL_ENVIRONMENT,
    cache: brixServer.dataCache(c).public("1h"),
  }),
);

environment defaults to master; host defaults to cdn.contentful.com. Set host: "preview.contentful.com" for draft reads. The equivalent EU Delivery and Preview hosts are supported. Non-loopback custom hosts must use HTTPS and require allowCustomHostWithAccessToken: true; reserve that opt-in for a trusted proxy. Loopback HTTP URLs are accepted for local mocks.

Invalid host or token policy throws while creating the client. Request methods return typed ResultPromise values, so use .unwrap(), .unwrapOr(), .map(), or .match() for expected outcomes.

Query entries and assets

const contentful = getContentful(c);
const page = await contentful.getEntry("page", pageId, { locale: "en-GB" }).unwrap();
const hotel = await contentful
  .findEntry("hotel", { "fields.slug": slug, locale: "en-GB" })
  .unwrap();
const hotels = await contentful
  .getEntries("hotel", {
    locale: "en-GB",
    limit: 20,
    skip: 0,
    order: "-sys.createdAt",
    include: 3,
    "fields.city": "stockholm",
    "fields.stars[gte]": 4,
    "fields.amenities[in]": ["pool"],
  })
  .unwrap();
const asset = await contentful.getAsset("asset-id").unwrap();

getEntry and findEntry return ContentfulEntryNotFoundError when absent. Collections preserve total, skip, limit, and includes metadata. Typed options cover locale, pagination, selection, ordering, full-text search, include depth, equality, range, list, match, existence, and known linked-field filters.

Entry reads automatically resolve included entry and asset links while preserving metadata. Missing links become null, and cycles are handled. Sync remains unresolved by design.

Delivery calls retry 429, 500, 502, 503, and 504 by default, making three total attempts and honoring Contentful retry headers. Override the client retry option when the application's latency budget requires a different policy.

Cache Delivery reads

The cache option covers entry, collection, find, and asset reads. Cache identity includes the host, space, environment, credentials, locale, and complete query. Bypass an individual read with { cache: false }.

Preview, sync, and Management calls never use this cache. invalidateCache() invalidates all native Delivery reads for that host, space, and environment. Use a shared store for replicas and verified webhooks to trigger invalidation. This does not purge edge pages, browser data, or Contentful's CDN; see caching.

Synchronize content incrementally

const initial = await contentful.sync(undefined, { type: "Entry", limit: 100 }).unwrap();
await saveSyncToken(initial.nextSyncToken);

const delta = await contentful.sync(initial.nextSyncToken, { limit: 100 }).unwrap();

sync follows all continuation pages and returns raw entries, assets, deletion stubs, and nextSyncToken. type applies only to the initial sync; a continuation token already carries its scope. The page limit is preserved across requests.

Use syncPage only when the caller must own pagination:

let page = await contentful.syncPage({ type: "Entry", limit: 100 }).unwrap();
while (page.nextPageToken) {
  page = await contentful.syncPage({ syncToken: page.nextPageToken, limit: 100 }).unwrap();
}

Persist only the final nextSyncToken. Narrow mixed resources with the guards described in type generation.

Inspect models with the Management client

Use the CMA client in server-side scripts, never in browser code:

import { createContentfulCmaClient } from "brix/contentful";
import { loadEnv } from "#brix/env/script";

const env = await loadEnv();
const cma = createContentfulCmaClient({
  spaceId: env.CONTENTFUL_SPACE_ID,
  accessToken: env.CONTENTFUL_CMA_TOKEN,
  environment: "master",
  includeDrafts: true,
});

const contentTypes = await cma.getContentTypes().unwrap();

Pagination is automatic. Omit includeDrafts to read published models; set it to true to include unpublished content types.