@contentful/experiences-client
Internal package. Published to npm so framework adapters can resolve it at install time, but you're not meant to import it directly. It comes in transitively via the framework adapter (
@contentful/experiences-react,@contentful/experiences-svelte, etc.).
Isolates @contentful/experience-delivery — the generated Experience Delivery client — and owns the free fetch functions, payload conversion, delivery errors, and XDA-derived request types. The shared runtime and complete EventBuilder live in @contentful/experiences-runtime, which depends on this package. Client does not build events or depend on Runtime. The existing free functions remain supported for adapter and application integration.
What's in here
fetchExperience(experienceOptions, clientOptions, resolveOptions)
The primary fetch + resolve entry point. Fetches an Experience payload and resolves it into a PortableRenderPlan in one call. There are three ways to say which Experience:
- By destination path (recommended for apps):
{ spaceId, destinationId, path }. The URL path is the Destination path. - By destination node id:
{ spaceId, destinationId, nodeId }. - By Experience id:
{ spaceId, environmentId, experienceId, … }. Use it when you already hold an id, and for preview.
The destination forms are space-scoped (no environmentId, locale, parameters or extensions) and accept personalization; with it set, the request is sent as a POST instead of the cacheable GET. They return PortableRenderPlan | DestinationRedirectResult: a redirect node resolves to { redirect: { path } }, which you follow instead of rendering.
const result = await fetchExperience(
{ spaceId, destinationId, path: '/demo/landing' },
{ accessToken: process.env.CDA_TOKEN! },
{ config: experienceConfig }
);
if ('redirect' in result) return redirect(result.redirect.path);
// result is a PortableRenderPlan
preview: true with a destination throws DestinationPreviewNotSupportedError (a bridge; use the by-id form for preview). It does not fire on the { client } branch.
By-ID options accept a first-class personalization object containing a profile id and events. Additional Experience Delivery options remain under extensions; use extensions: { sourceMap: {} } to request a content source map. Client merges both into the XDA request body and uses the POST operation whenever either capability is present.
By-ID parameters accepts strings, numbers, and one-level Records of strings/numbers:
{ featuredPosts: { limit: 6, order: '-fields.title' } }. IDs are declared in
Experience.parameters and referenced by $parameters/<id>. Delivery validates
names, requiredness, fallbacks and values. Collection order is one signed
fields.<apiName> or sys.<id> path, never OrderTerm[].
GET uses parameter.<id> and parameter.<id>.<field> in the generated client's
queryParams, separate from locale. Empty Records or defined extensions select
POST, with structured, unprefixed parameters in the body. dev.10 supports that
body through serializer passthrough, although its generated type omits the key;
the SDK uses a local typed request extension. Authentication and rename headers
remain owned by the generated client. Refetch with changed values and replace
the renderer's plan; see the runtime parameters example.
Three positional args group by concern:
| Arg | Type | Purpose |
|---|---|---|
experienceOptions | { spaceId, destinationId, path | nodeId, personalization? } or { spaceId, environmentId, experienceId, locale?, parameters?, personalization?, extensions? } | Which Experience to fetch: by destination or by id. |
clientOptions | { accessToken, previewToken?, preview?, host? } or { client } | How to fetch — inline credentials (with optional preview toggle) or a pre-made client. |
resolveOptions | { config, metadata?, debug? } | How to resolve — component registry, per-render metadata, and a debug switch. |
import { fetchExperience } from '@contentful/experiences-react'; // or experiences-svelte
// Inline credentials — client created internally
const plan = await fetchExperience(
{ spaceId: '...', environmentId: 'master', experienceId: slug, locale: 'en-US' },
{
accessToken: process.env.CDA_TOKEN!,
previewToken: process.env.PREVIEW_TOKEN!, // optional — only required when preview: true
preview: false, // flip to true for preview mode; picks previewToken + preview host
},
{
config: experienceConfig,
metadata: { slug }, // flows into resolveData hooks as ctx.experience.metadata
debug: false, // logs + visible missing-component box when true
}
);
// Pre-created client — useful when you manage the client lifecycle yourself
import { createClient } from '@contentful/experiences-react';
const client = createClient({ accessToken: process.env.CDA_TOKEN! });
const plan = await fetchExperience(
{ spaceId, environmentId, experienceId },
{ client },
{ config: experienceConfig }
);
Returns PortableRenderPlan. An empty-nodes payload (draft / unpublished / empty locale) resolves to a valid plan with nodes: [] — it is not a 404. For the missing-experience case, catch NotFoundError (re-exported below).
Successful responses can carry upstream field-resolution errors alongside useful
content. Core projects recognized errors into optional plan.deliveryDiagnostics
records with string fieldPath and message, preserving order and omitting
originalError and unknown fields. Client retains the payload errors through
by-ID GET/POST and destination conversion; generated-schema compatibility remains
Client-owned, with a single normalization point in Core.
Destination errors may have broader shapes, so this projection does not promise
complete coverage. An absent field does not prove personalization success or the
absence of other upstream errors. Existing diagnostics: Error[], exceptions,
404 handling, and profile behavior are unchanged. See the delivery diagnostic
guidance for safe presentation and disclosure.
Preview mode
Configure both tokens up front and flip preview: true per call to hit the preview API.
const experience = await fetchExperience(
{ spaceId, environmentId, experienceId },
{
accessToken: process.env.CDA_TOKEN!,
previewToken: process.env.PREVIEW_TOKEN!,
preview: previewMode, // boolean — flip per request
},
{ config: experienceConfig }
);
preview: false(or unset) → usesaccessTokenagainst the delivery host.preview: true→ usespreviewTokenagainst the preview host. ThrowsfetchExperience() called with preview: true but no previewToken was providedifpreviewTokenis missing.hostis only for custom base URLs (staging, proxy, per-region). When set, it wins over thepreview-derived host — so{ previewToken, preview: true, host: 'https://preview-staging…' }uses the preview token against your custom URL.- With the
{ client }option,previewis ignored (bring your own client, bring your own token/host choice).
createClient(options)
Functional constructor over ContentfulViewDeliveryClient matching the SDK's option shape. Maps accessToken → token and host → baseUrl; passes everything else through unchanged. Prefer over new ContentfulViewDeliveryClient({...}) so field names stay consistent with fetchExperience's inline-credentials path.
createClient is a one-time setup primitive — it builds a single client bound to a single token and does not participate in the per-request preview toggle. If you need runtime-dynamic swaps between delivery and preview, use fetchExperience's inline-credentials path ({ accessToken, previewToken, preview }) instead of pre-building a client via createClient and passing { client }.
import { createClient, PREVIEW_HOST } from '@contentful/experiences-react';
// Delivery (default)
const client = createClient({ accessToken: process.env.CDA_TOKEN! });
// Preview — use PREVIEW_HOST + a CPA token
const previewClient = createClient({
accessToken: process.env.PREVIEW_TOKEN!,
host: PREVIEW_HOST,
});
// Custom base URL (staging, proxy, per-region)
const customClient = createClient({
accessToken: process.env.CDA_TOKEN!,
host: 'https://preview-staging.example.com',
// headers, timeoutInSeconds, maxRetries, fetch, logging pass through
});
DELIVERY_HOST / PREVIEW_HOST / PREVIEW_WEBSOCKET_HOST
Named constants for the canonical delivery, preview, and Preview Session WebSocket URLs. Use them so you don't have to hardcode the URL strings in your app.
import { DELIVERY_HOST, PREVIEW_HOST, PREVIEW_WEBSOCKET_HOST } from '@contentful/experiences-react';
DELIVERY_HOST; // 'https://xdn.contentful.com'
PREVIEW_HOST; // 'https://preview.xdn.contentful.com'
PREVIEW_WEBSOCKET_HOST; // 'wss://preview.xdn.contentful.com'
createClient is the fixed-mode path — one client, one token, one host. If you need to flip between delivery and preview per request (e.g. an isPreview URL param), use fetchExperience's inline-credentials form with preview: boolean instead of pre-building a client here. See "Preview mode" above.
fetchDestinationSitemap(destinationOptions, clientOptions, paginationOptions?)
Lists every routable path in a published Destination, for static generation, sitemap.xml, or any other bulk walk. It does not hydrate Experiences; feed each path to fetchExperience.
import { fetchDestinationSitemap } from '@contentful/experiences-react';
const paths: string[] = [];
let pageNext: string | undefined;
do {
const sitemap = await fetchDestinationSitemap(
{ spaceId, destinationId },
{ accessToken: process.env.CDA_TOKEN! },
{ pageNext } // limit defaults to 100, max 1000
);
paths.push(...sitemap.items.map((item) => item.path));
pageNext = sitemap.pages.next;
} while (pageNext);
Each item is { path, experienceId, type }. Cursors (pages.next, pages.prev) are opaque; don't combine pageNext and pagePrev. A missing Destination throws NotFoundError. It is a free function only; the Node and Web runtimes have no sitemap method.
NotFoundError
Re-exported from @contentful/experience-delivery as a value + type. Thrown by the underlying delivery client on 404 responses. Route it to your framework's 404 idiom:
import { fetchExperience, NotFoundError } from '@contentful/experiences-react';
try {
const experience = await fetchExperience(/* … */);
// …
} catch (err) {
if (err instanceof NotFoundError) notFound(); // Next.js
throw err;
}
The full delivery-client error namespace is also re-exported as ContentfulViewDelivery (UnauthorizedError, ForbiddenError, ConflictError, UnprocessableEntityError, InternalServerError, ContentfulViewDeliveryError, ContentfulViewDeliveryTimeoutError).
ContentfulViewDeliveryClient
Re-exported directly from @contentful/experience-delivery. Exposed for advanced use cases where you want full control over the client (custom base URL, request options, reuse across calls). Most consumers should prefer createClient (above).
import { ContentfulViewDeliveryClient } from '@contentful/experiences-react';
const client = new ContentfulViewDeliveryClient({
token: process.env.CDA_TOKEN!,
baseUrl: 'https://xdn.contentful.com', // default delivery endpoint
});
Why a separate package?
@contentful/experiences-sdk-core is intentionally zero-dep and runtime-neutral — it must stay importable without pulling in any network or platform code. The experience delivery client is large (~3,000 generated files) and only needed when doing server-side fetching. Isolating it here means:
- Core stays lean and usable in any environment (edge, SSR, test fixtures).
- Future adapters that render from local fixtures or a custom fetch path don't pay the delivery client's weight.
- The delivery client version can be bumped in one place.
Package conventions
- Do not import
@contentful/experience-deliveryfrom anywhere except this package. - Re-export only what framework adapters need to surface to their users.
- Keep
fetchExperiencethin — fetch + cast + resolve. Business logic belongs inpackages/core. - Name mappings between SDK options and delivery-client options live in
create-delivery-client.ts— one place to change.
License
MIT. See the repository LICENSE and NOTICE for full attribution.