diff --git a/AGENTS.md b/AGENTS.md
index bfd4d53..7c8cbb3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -32,6 +32,7 @@ experiences/
├── packages/
│ ├── core/ # @contentful/experiences-core (internal)
│ ├── design/ # @contentful/experiences-design (internal)
+│ ├── client/ # @contentful/experiences-client (internal)
│ ├── adapter-react/ # @contentful/experiences-react (customer-facing)
│ └── adapter-svelte/ # @contentful/experiences-svelte (customer-facing)
└── examples/
@@ -41,12 +42,13 @@ experiences/
### Package roles
-| Folder | npm name | Audience |
-| ------------------------- | -------------------------------- | ------------------------------------------------------------------ |
-| `packages/core` | `@contentful/experiences-core` | **Internal.** Runtime-neutral types + `resolveExperience`. |
-| `packages/design` | `@contentful/experiences-design` | **Internal.** Pure viewport math. |
-| `packages/adapter-react` | `@contentful/experiences-react` | **Customer-facing.** React renderer + re-exports of everything. |
-| `packages/adapter-svelte` | `@contentful/experiences-svelte` | **Customer-facing.** Svelte 5 renderer + re-exports of everything. |
+| Folder | npm name | Audience |
+| ------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------- |
+| `packages/core` | `@contentful/experiences-core` | **Internal.** Runtime-neutral types + `resolveExperience`. |
+| `packages/design` | `@contentful/experiences-design` | **Internal.** Pure viewport math. |
+| `packages/client` | `@contentful/experiences-client` | **Internal.** Experience delivery client + `fetchExperience`. Keeps the delivery dep isolated. |
+| `packages/adapter-react` | `@contentful/experiences-react` | **Customer-facing.** React renderer + re-exports of everything. |
+| `packages/adapter-svelte` | `@contentful/experiences-svelte` | **Customer-facing.** Svelte 5 renderer + re-exports of everything. |
**Customers install only the framework adapter for their stack.** The internal packages are workspace dependencies of the adapter — they get installed transitively, but customers never reach into them.
@@ -59,10 +61,14 @@ Future framework adapters slot in under the same naming pattern: `packages/adapt
The customer pipeline is three steps:
```
-fetch payload (delivery client) → resolveExperience(payload, config) →
+fetchExperience(experienceOptions, clientOptions, resolveOptions) →
```
-`resolveExperience` is the **single async entry**:
+`fetchExperience` is the **single async entry** for most customers — it fetches the payload from the Experience Delivery API and calls `resolveExperience` internally. The three positional args group by concern: **which experience**, **how to fetch**, **how to resolve**. Each grouping evolves independently (personalization slots into arg 3; digital-property identifiers widen arg 1) — a shape chosen to avoid one flat options object growing unbounded.
+
+Customers who want to manage the delivery client themselves have two paths: `createClient({ accessToken, host? })` (functional constructor, our option shape), or `new ContentfulViewDeliveryClient({ token, baseUrl? })` (underlying delivery client, its option shape). Either way, pass the resulting client as `{ client }` in `clientOptions`.
+
+`resolveExperience` (called internally by `fetchExperience`) is the **resolve step**:
1. Walks the XDA payload's `nodes[]` recursively.
2. Extracts `componentTypeId` from each node's `componentType.sys.urn` (last slash-segment).
@@ -86,6 +92,37 @@ The React adapter then:
## Design decisions worth knowing
+### Why three positional args on `fetchExperience` instead of one options object?
+
+Earlier the SDK had a single flat options object with an `accessToken | client` discriminated union mixed in. As `fetchExperience` picked up more responsibilities (fetch + resolve, preview mode, per-render context) the object was already ~7 fields and had two more inbound (personalization params, digital-property identifiers from the channels RFC). Rather than let one bag balloon to a dozen fields with three different concerns, the signature is split into three grouped args:
+
+1. `experienceOptions` — **which** experience (`spaceId`, `environmentId`, `experienceId`, `locale`). Digital-property identifiers widen this type when the channels RFC lands.
+2. `clientOptions` — **how** to fetch. Discriminated union: `{ accessToken, host? }` OR `{ client }`. Kept intentionally as one arg (not split further) so users can move between inline creds and a pre-made client with only that arg changing.
+3. `resolveOptions` — **how** to resolve (`config`, `context`). Personalization params (`audienceIds`, `userTraits`, etc.) go here.
+
+Each grouping evolves without touching the others.
+
+### Why `host: string` instead of `preview: boolean`?
+
+Two reasons. (1) The SDK shouldn't own the URL constants for XDN vs XPA — those are Contentful platform concerns that can add non-prod endpoints (staging, EU-region, per-account) which a boolean can't express. (2) A raw base URL passes cleanly to `ContentfulViewDeliveryClient.Options.baseUrl` — no translation layer. Callers write `host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com'` at the call site; the SDK just passes through.
+
+### Why `createClient` in addition to the raw `ContentfulViewDeliveryClient` constructor?
+
+`createClient` is a value-added passthrough that maps the SDK's option names (`accessToken`, `host`) onto the underlying client's names (`token`, `baseUrl`). Everything else flows through unchanged. It exists so the "inline creds" and "bring your own client" paths of `fetchExperience` share exactly the same field names — users can move between them mechanically instead of relearning vocabulary.
+
+`fetchExperience`'s own inline-creds branch routes through `createClient` — single source of truth for the name mapping. If we ever add auth flavors beyond bearer-token, this is where they'd land.
+
+### Why an empty-nodes payload is NOT a "not found"
+
+`fetchExperience` used to return `null` when the payload had `nodes: []`. That conflated two states the CMS considers distinct:
+
+- **Experience doesn't exist** (404 from the delivery API) — the delivery client throws `NotFoundError`. Caller should route to their framework's 404 idiom.
+- **Experience exists, empty content** (200 with `nodes: []`) — draft, unpublished, empty locale fallback, editor-in-progress. Legitimate CMS state; renders as an empty page.
+
+The empty-nodes payload now flows straight through to `resolveExperience`, which handles it gracefully (no walker iterations, template still resolves if present, returns `{ viewports, nodes: [] }`). `fetchExperience`'s return type narrowed from `PortableRenderPlan | null` to `PortableRenderPlan`.
+
+For the missing-experience case, `NotFoundError` is re-exported from the adapter (via `packages/client`) so example call sites can wrap `fetchExperience` in try/catch without adding `@contentful/experience-delivery` as a direct dep — preserving the invariant that customers install only the framework adapter.
+
### Why a single `resolveExperience` entry instead of `buildPlan` + `resolveExperience`?
Earlier the SDK had two functions. The customer page needed three lines of imports + four function calls + two passes of `componentMap`. We collapsed to one. The sync vs async distinction (tree-walking is synchronous; `resolveData` hooks are async) is implementation detail customers don't care about.
@@ -119,9 +156,9 @@ React's RSC compilation requires a `'use client'` directive at the top of files
Each `defineComponent(...)` is parameterized over the design-system component's prop type. No separate `ContentfulButtonProps` interface to keep in sync. The design system owns the contract; the integration layer adapts to it. When a Contentful payload field doesn't map 1:1, the customer either (a) renames at the render-fn call site, or (b) uses `resolveData` to reshape, or (c) extends the type at the map level (`type ButtonMapProps = ButtonProps & { testSlot?: ReactNode }`).
-### Why is the delivery client (`@contentful/experience-delivery`) a customer dep, not bundled?
+### Why is the delivery client (`@contentful/experience-delivery`) isolated in `packages/client`, not a direct dep of the adapters?
-Three reasons. (1) Customers can pin it independently. (2) The renderer SDK can render any compatible payload — mocks, local fixtures, custom fetch paths — not just the official client. (3) The delivery client is large (~3,000 generated files); bundling it would dwarf our package's bundle budget.
+Two reasons. (1) `packages/core` must stay zero-dep and runtime-neutral — pulling the delivery client into core would break that invariant for all current and future adapters. (2) The delivery client is large (~3,000 generated files); centralizing it in one internal package (`packages/client`) means adapters that don't need it (e.g. a future server-only adapter) won't pull it in transitively. Customers who want to manage the client themselves can import `ContentfulViewDeliveryClient` directly from the adapter and call `resolveExperience` with their own payload.
### Why two separate registries (`components` and `templates`) instead of one?
@@ -152,8 +189,9 @@ Conventional commits (`feat:`, `fix:`, `chore:`, `docs:`, etc.). Enforced via `c
### Package boundaries
-- **`core` may not depend on `react` or any framework-specific package.** Enforced by code review (no module-boundary lint rule yet, but it should land).
+- **`core` may not depend on `react`, the delivery client, or any framework-specific package.** Enforced by code review (no module-boundary lint rule yet, but it should land).
- **`design` may not depend on `core` for runtime; it imports types only.** This keeps `design` a pure-utility package usable in isolation.
+- **`client` is the only package that may depend on `@contentful/experience-delivery`.** All delivery-client usage must go through `packages/client` — never import it directly from an adapter or from `core`.
- **The customer-facing adapter (`adapter-react`) is the ONLY package that re-exports everything**. Internal packages don't re-export from each other.
### File naming
diff --git a/README.md b/README.md
index d34ac79..70e7ae9 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,10 @@
A renderer SDK for Contentful's **Experience Orchestration (ExO)**. You bring a design system; the SDK takes the Experience payload from the Experience Delivery API (XDA) and renders it.
```sh
-npm install @contentful/experiences-react @contentful/experience-delivery
+npm install @contentful/experiences-react
```
-That's the only SDK package customers install — it re-exports everything you need (resolver, types, renderer, design utilities). The `@contentful/experiences-core` and `@contentful/experiences-design` packages are workspace-internal implementation details.
+That's the only SDK package customers install — it re-exports everything you need (resolver, types, renderer, design utilities, and the experience delivery client). The `@contentful/experiences-core`, `@contentful/experiences-design`, and `@contentful/experiences-client` packages are workspace-internal implementation details.
A Svelte adapter (`@contentful/experiences-svelte`) ships in parallel with the same public-API shape; see [`packages/adapter-svelte`](./packages/adapter-svelte). The rest of this README focuses on React.
@@ -16,7 +16,7 @@ A Svelte adapter (`@contentful/experiences-svelte`) ships in parallel with the s
## Getting started — the simple path
-Three steps: register your components, fetch + resolve, render. The minimal page is **three functional lines**.
+Three steps: register your components, fetch + resolve, render. The minimal page is one `fetchExperience` call feeding one ``.
### 1. Register your components and (optional) templates
@@ -62,22 +62,33 @@ export const experienceConfig: Config = { components, templates };
```tsx
// app/[slug]/page.tsx (Next.js App Router)
-import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
-import { resolveExperience, ServerExperienceRenderer } from '@contentful/experiences-react';
+import { notFound } from 'next/navigation';
+import {
+ fetchExperience,
+ NotFoundError,
+ ServerExperienceRenderer,
+} from '@contentful/experiences-react';
import { experienceConfig } from '@/lib/experience-config';
-const client = new ContentfulViewDeliveryClient({ token: process.env.CDA_TOKEN! });
-
export default async function ExperiencePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
- const payload = await client.view.getExperience(process.env.SPACE_ID!, 'master', slug);
- const experience = await resolveExperience(payload, experienceConfig);
-
- return ;
+ try {
+ const experience = await fetchExperience(
+ { spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId: slug },
+ { accessToken: process.env.CDA_TOKEN! },
+ { config: experienceConfig }
+ );
+ return ;
+ } catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+ }
}
```
-That's it. The renderer walks the payload, resolves design properties to plain scalars, runs any `resolveData` hooks (in parallel), and dispatches each node to your registered React component.
+`fetchExperience` fetches the payload from the Experience Delivery API and resolves it in one call — the renderer walks the payload, resolves design properties to plain scalars, runs any `resolveData` hooks (in parallel), and dispatches each node to your registered React component.
+
+The signature is three grouped params: **what to fetch** (space/env/experience), **how to fetch** (auth), **how to resolve** (component config + per-render context). Each grouping evolves independently — future personalization params, digital-property identifiers, and transport options fit their respective group without cross-cutting the signature.
A working version is at [`examples/nextjs/app/[slug]/page.tsx`](./examples/nextjs/app/[slug]/page.tsx).
@@ -92,10 +103,15 @@ A full working advanced route is at [`examples/nextjs/app/advanced/[slug]/page.t
```tsx
// app/advanced/[slug]/page.tsx
import { headers } from 'next/headers';
-import { ServerExperienceRenderer, resolveExperience } from '@contentful/experiences-react';
+import { notFound } from 'next/navigation';
+import {
+ fetchExperience,
+ NotFoundError,
+ ServerExperienceRenderer,
+} from '@contentful/experiences-react';
import { detectViewportFromUserAgent } from '@/lib/detect-viewport';
-import { experienceConfig } from '@/lib/experience-config';
+import { advancedExperienceConfig } from '@/lib/experience-config-advanced';
export default async function AdvancedPage({
params,
@@ -104,7 +120,7 @@ export default async function AdvancedPage({
params: Promise<{ slug: string }>;
searchParams: Promise>;
}) {
- const { slug } = await params;
+ const { slug: experienceId } = await params;
const sp = await searchParams;
const previewMode = sp.preview === 'true';
const locale = (sp.locale as string) ?? 'en-US';
@@ -113,24 +129,32 @@ export default async function AdvancedPage({
const userAgent = (await headers()).get('user-agent') ?? '';
const initialViewportId = detectViewportFromUserAgent(userAgent);
- const payload = await client.view.getExperience(process.env.SPACE_ID!, 'master', slug, {
- locale,
- preview: previewMode ? 'true' : undefined,
- });
-
- // 2. Per-page metadata flows into every resolveData hook.
- const experience = await resolveExperience(payload, experienceConfig, {
- experience: { isPreview: previewMode, metadata: { slug, locale } },
- });
-
- return (
-
- );
+ try {
+ // 2. Per-page metadata flows into every resolveData hook via context.
+ const experience = await fetchExperience(
+ { spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId, locale },
+ {
+ accessToken: process.env.CDA_TOKEN!,
+ host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com',
+ },
+ {
+ config: advancedExperienceConfig,
+ context: { isPreview: previewMode, metadata: { slug: experienceId, locale } },
+ }
+ );
+
+ return (
+
+ );
+ } catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+ }
}
```
@@ -159,6 +183,64 @@ button: defineComponent({
## API reference
+### `fetchExperience(experienceOptions, clientOptions, resolveOptions)`
+
+Async. Fetches an Experience from the Experience Delivery API and resolves it in one call — equivalent to fetching the payload yourself and then calling `resolveExperience`. Returns a `PortableRenderPlan`.
+
+Three positional args map to three concerns that evolve independently:
+
+| Arg | Type | Purpose |
+| ------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| `experienceOptions` | `{ spaceId, environmentId, experienceId, locale? }` | Which Experience to fetch. Future digital-property identifiers widen this type. |
+| `clientOptions` | `{ accessToken, host? }` **or** `{ client }` | How to fetch. Discriminated union — pass creds inline or bring your own `ContentfulViewDeliveryClient`. |
+| `resolveOptions` | `{ config, context? }` | How to resolve. `context` flows into every `resolveData` hook as `ctx.experience`. |
+
+`host` is a full base-URL string (e.g. `'https://preview.xdn.contentful.com'` for the preview API). When omitted, the delivery client's default endpoint is used.
+
+```ts
+// Inline credentials — client created internally
+const plan = await fetchExperience(
+ { spaceId: '...', environmentId: 'master', experienceId: slug, locale: 'en-US' },
+ { accessToken: process.env.CDA_TOKEN!, host: 'https://preview.xdn.contentful.com' },
+ { config: experienceConfig, context: { isPreview: true, metadata: { slug } } }
+);
+
+// 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 }
+);
+```
+
+**Error handling.** The underlying delivery client throws `NotFoundError` (re-exported from the adapter) when the Experience ID doesn't exist, plus `UnauthorizedError`, `ForbiddenError`, etc. on other 4xx/5xx responses. An Experience with no published nodes is **not** a 404 — it resolves to a valid `PortableRenderPlan` with `nodes: []` (draft / unpublished / empty-locale content). Route the missing-experience case to your framework's 404 idiom:
+
+```ts
+try {
+ const experience = await fetchExperience(/* … */);
+ return ;
+} catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+}
+```
+
+### `createClient(options)`
+
+Functional constructor over `ContentfulViewDeliveryClient` for the SDK's option shape — maps `accessToken → token` and `host → baseUrl`, passes everything else through. Prefer this over `new ContentfulViewDeliveryClient({...})` so field names stay consistent with `fetchExperience`'s inline-creds path.
+
+```ts
+import { createClient } from '@contentful/experiences-react';
+
+const client = createClient({
+ accessToken: process.env.CDA_TOKEN!,
+ host: 'https://preview.xdn.contentful.com', // optional — overrides the default endpoint
+ // headers, timeoutInSeconds, maxRetries, fetch, logging, etc. all pass through
+});
+```
+
### `resolveExperience(payload, config, opts?)`
Async. Walks the payload, classifies properties, runs every component's `resolveData` in parallel, and returns a `PortableRenderPlan` ready to hand to a renderer.
@@ -175,7 +257,7 @@ SSR-friendly renderer. No reactive subscriptions; the active viewport is resolve
| Prop | Type | Required | Default | Description |
| ------------------- | --------------------------------------------- | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ |
-| `experience` | `PortableRenderPlan \| null \| undefined` | yes | — | The resolved plan from `resolveExperience`. Renders nothing if null/undefined. |
+| `experience` | `PortableRenderPlan` | yes | — | The resolved plan from `fetchExperience` or `resolveExperience`. An empty-nodes plan renders nothing. |
| `config` | `Config` | yes | — | Same registry passed to `resolveExperience`. Looked up at render time for dispatch. |
| `initialViewportId` | `string` | no | First viewport id | Seeds the active viewport. Typically derived from User-Agent server-side. |
| `context` | `Partial` | no | `{ isPreview: false, metadata: {} }` | Shallow-merged into the render-time `experience` context customer components receive. |
@@ -293,6 +375,7 @@ This is an Nx monorepo. Customers install only the framework adapter; the rest i
| ------------------------------------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`packages/core`](./packages/core) | `@contentful/experiences-core` | **Internal.** Runtime-neutral types + `resolveExperience`. |
| [`packages/design`](./packages/design) | `@contentful/experiences-design` | **Internal.** Viewport math (`getValueForViewport`, `resolveDesignProperties`, `toCssMediaQuery`). |
+| [`packages/client`](./packages/client) | `@contentful/experiences-client` | **Internal.** Experience delivery client + `fetchExperience`. |
| [`packages/adapter-react`](./packages/adapter-react) | `@contentful/experiences-react` | **Customer-facing.** React renderer + re-exports of everything else. |
| [`packages/adapter-svelte`](./packages/adapter-svelte) | `@contentful/experiences-svelte` | **Customer-facing.** Svelte 5 renderer with the same public API shape. |
diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md
index 5790948..1b1f24d 100644
--- a/examples/nextjs/README.md
+++ b/examples/nextjs/README.md
@@ -4,10 +4,9 @@ A Next.js 15 App Router app demonstrating `@contentful/experiences-react` render
## What it shows
-- **Server-side fetch** via `@contentful/experience-delivery`'s `ContentfulViewDeliveryClient`. The response is structurally compatible with our `ExperiencePayload` — pass it straight to `resolveExperience`, no normalization.
-- **Runtime-neutral plan resolution** with `resolveExperience` (re-exported from `@contentful/experiences-react`) — one async step walks the tree, classifies props, and runs any component-declared `resolveData` hooks in parallel.
+- **Server-side fetch + resolve** via `fetchExperience` (re-exported from `@contentful/experiences-react`) — one async call fetches the payload from the Experience Delivery API, walks the tree, classifies props, and runs any component-declared `resolveData` hooks in parallel.
- **SSR rendering** with `ServerExperienceRenderer` from `@contentful/experiences-react`.
-- **Three-line page** — `fetch` → `resolveExperience` → ``. Preview mode, viewport seeding, and metadata are all optional advanced features (see [`opts`](#optional-resolveexperience-opts) below); the minimal app needs none of them.
+- **Minimal page** — `fetchExperience` → ``, wrapped in a try/catch that routes `NotFoundError` to Next's `notFound()`. Preview mode, viewport seeding, and metadata are all optional advanced features; the minimal app needs none of them.
- **`defineComponent` authoring** — each component file declares its props and renderer in one place; no casts, no boilerplate map wrappers.
## Run it
@@ -28,19 +27,29 @@ Then visit `http://localhost:3000/` — the slug becomes the Expe
The example ships two side-by-side routes so you can see what each SDK option buys you. They render the same Experience id; only the SDK plumbing changes.
-| Route | Page | Config | Demonstrates |
-| ------------------ | ---------------------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------- |
-| `/[slug]` | [`app/[slug]/page.tsx`](./app/[slug]/page.tsx) | `experience-config.tsx` | The minimum: `fetch` → `resolveExperience(payload, config)` → ``. 3 lines. |
-| `/advanced/[slug]` | [`app/advanced/[slug]/page.tsx`](./app/advanced/[slug]/page.tsx) | `experience-config-advanced.tsx` | Preview mode via `?preview=true`, UA → `initialViewportId`, async `resolveData` with external fetch. |
+| Route | Page | Config | Demonstrates |
+| ------------------ | ---------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
+| `/[slug]` | [`app/[slug]/page.tsx`](./app/[slug]/page.tsx) | `experience-config.tsx` | The minimum: `fetchExperience` → `` with `NotFoundError` routed to Next's `notFound()`. |
+| `/advanced/[slug]` | [`app/advanced/[slug]/page.tsx`](./app/advanced/[slug]/page.tsx) | `experience-config-advanced.tsx` | Preview mode via `?preview=true`, UA → `initialViewportId`, async `resolveData` with external fetch. |
-The minimal `[slug]/page.tsx` is three functional lines:
+The minimal `[slug]/page.tsx`:
```tsx
-const { payload } = await fetchExperience(experienceId);
-const experience = await resolveExperience(payload, experienceConfig);
-return ;
+try {
+ const experience = await fetchExperience(
+ { spaceId, environmentId, experienceId },
+ { accessToken },
+ { config: experienceConfig }
+ );
+ return ;
+} catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+}
```
+`NotFoundError` is thrown by the delivery client on 404 (Experience ID doesn't exist). An empty-nodes payload (draft / unpublished / empty locale) is **not** a 404 — it resolves to a plan with `nodes: []` and renders as an empty page.
+
Slug-to-ID mapping is left to the customer — see the SDK roadmap in [`AGENTS.md`](../../AGENTS.md) for the longer-term direction.
## File map
@@ -50,7 +59,7 @@ examples/nextjs/
├── app/
│ ├── layout.tsx # root layout
│ ├── page.tsx # index — links to both demo routes
-│ ├── [slug]/page.tsx # SIMPLE — 3-line page
+│ ├── [slug]/page.tsx # SIMPLE — fetch + render + 404 handling
│ └── advanced/[slug]/page.tsx # ADVANCED — preview, UA seeding, async resolveData
├── components/ # plain design-system components — no SDK imports
│ ├── Button.tsx
@@ -58,7 +67,6 @@ examples/nextjs/
│ ├── Page.tsx # used as the page-level template
│ └── Text.tsx
└── lib/
- ├── delivery-client.ts # ContentfulViewDeliveryClient factory
├── detect-viewport.ts # User-Agent → viewport id (used by the advanced route)
├── experience-config.tsx # the integration layer for /[slug]
└── experience-config-advanced.tsx # the integration layer for /advanced/[slug] — async fetch + metadata-aware
@@ -155,31 +163,43 @@ priceTag: defineComponent({
}),
```
-The Next.js page calls `resolveExperience` once before rendering:
+The route calls `fetchExperience` once — it handles the API call and resolution in one step:
```ts
-const experience = await resolveExperience(payload, experienceConfig);
+const experience = await fetchExperience(
+ { spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId: slug },
+ { accessToken: process.env.CDA_TOKEN! },
+ { config: experienceConfig }
+);
```
Resolvers run in parallel across nodes. Viewport resolution stays at render
time, so client-side viewport changes never re-trigger `resolveData`.
-#### Optional `resolveExperience` opts
+#### Optional `context`
-The third argument lets you pass per-render context that flows into every
-component's `resolveData` (and into the rendered components as `experience.*`).
+The `context` option (on `resolveOptions`, the third arg) passes per-render context into every component's `resolveData` hook (and into the rendered components as `experience.*`).
Default is `{ isPreview: false, metadata: {} }` — fine for production. Add
fields when:
-- **Preview mode**: `{ experience: { isPreview: true } }` — `MissingComponent`
- renders a visible red box; some customer components might branch on this.
-- **Per-page metadata**: `{ experience: { metadata: { slug, locale } } }` —
- available to every `resolveData` for URL building, locale-aware lookups, etc.
+- **Preview mode**: `{ isPreview: true }` — `MissingComponent` renders a visible
+ red box; some customer components might branch on this. Set `host` to the
+ preview endpoint on `clientOptions` to also hit the preview API.
+- **Per-page metadata**: `{ metadata: { slug, locale } }` — available to every
+ `resolveData` for URL building, locale-aware lookups, etc.
```ts
-const experience = await resolveExperience(payload, experienceConfig, {
- experience: { isPreview, metadata: { slug, locale } },
-});
+const experience = await fetchExperience(
+ { spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId: slug, locale },
+ {
+ accessToken: process.env.CDA_TOKEN!,
+ host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com',
+ },
+ {
+ config: experienceConfig,
+ context: { isPreview: previewMode, metadata: { slug, locale } },
+ }
+);
```
Pair with `` (User-Agent
diff --git a/examples/nextjs/app/[slug]/page.tsx b/examples/nextjs/app/[slug]/page.tsx
index 198fc66..1cfa4fd 100644
--- a/examples/nextjs/app/[slug]/page.tsx
+++ b/examples/nextjs/app/[slug]/page.tsx
@@ -1,7 +1,10 @@
import { notFound } from 'next/navigation';
-import { ServerExperienceRenderer, resolveExperience } from '@contentful/experiences-react';
+import {
+ NotFoundError,
+ ServerExperienceRenderer,
+ fetchExperience,
+} from '@contentful/experiences-react';
-import { fetchExperience } from '@/lib/delivery-client';
import { experienceConfig } from '@/lib/experience-config';
interface PageProps {
@@ -11,10 +14,20 @@ interface PageProps {
export default async function ExperiencePage({ params }: PageProps) {
const { slug: experienceId } = await params;
- const { payload } = await fetchExperience(experienceId);
- if (!payload.nodes.length) notFound();
+ try {
+ const experience = await fetchExperience(
+ {
+ spaceId: process.env.SPACE_ID ?? '',
+ environmentId: process.env.ENVIRONMENT_ID ?? 'master',
+ experienceId,
+ },
+ { accessToken: process.env.CDA_TOKEN! },
+ { config: experienceConfig }
+ );
- const experience = await resolveExperience(payload, experienceConfig);
-
- return ;
+ return ;
+ } catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+ }
}
diff --git a/examples/nextjs/app/advanced/[slug]/page.tsx b/examples/nextjs/app/advanced/[slug]/page.tsx
index aed22d8..617ff67 100644
--- a/examples/nextjs/app/advanced/[slug]/page.tsx
+++ b/examples/nextjs/app/advanced/[slug]/page.tsx
@@ -1,8 +1,11 @@
import { headers } from 'next/headers';
import { notFound } from 'next/navigation';
-import { ServerExperienceRenderer, resolveExperience } from '@contentful/experiences-react';
+import {
+ NotFoundError,
+ ServerExperienceRenderer,
+ fetchExperience,
+} from '@contentful/experiences-react';
-import { fetchExperience } from '@/lib/delivery-client';
import { detectViewportFromUserAgent } from '@/lib/detect-viewport';
import { advancedExperienceConfig } from '@/lib/experience-config-advanced';
@@ -15,8 +18,8 @@ interface PageProps {
* Advanced version of the [slug] route. Demonstrates three SDK features the
* minimal three-line page in `app/[slug]/page.tsx` doesn't reach for:
*
- * 1. **Preview mode + per-page metadata** via the `opts` arg of
- * `resolveExperience`. `?preview=true` flips `MissingComponent` from
+ * 1. **Preview mode + per-page metadata** via the `context` arg of
+ * `fetchExperience`. `?preview=true` flips `MissingComponent` from
* "silent null" to "visible red box"; metadata flows into every
* `resolveData` hook.
* 2. **User-Agent → viewport seeding** via `initialViewportId` so SSR
@@ -38,22 +41,37 @@ export default async function AdvancedExperiencePage({ params, searchParams }: P
const userAgent = (await headers()).get('user-agent') ?? '';
const initialViewportId = detectViewportFromUserAgent(userAgent);
- const { payload } = await fetchExperience(experienceId, { preview: previewMode, locale });
- if (!payload.nodes.length) notFound();
+ try {
+ const experience = await fetchExperience(
+ {
+ spaceId: process.env.SPACE_ID ?? '',
+ environmentId: process.env.ENVIRONMENT_ID ?? 'master',
+ experienceId,
+ locale,
+ },
+ {
+ accessToken: process.env.CDA_TOKEN!,
+ host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com',
+ },
+ {
+ config: advancedExperienceConfig,
+ context: {
+ isPreview: previewMode,
+ metadata: { slug: experienceId, locale },
+ },
+ }
+ );
- const experience = await resolveExperience(payload, advancedExperienceConfig, {
- experience: {
- isPreview: previewMode,
- metadata: { slug: experienceId, locale },
- },
- });
-
- return (
-
- );
+ return (
+
+ );
+ } catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+ }
}
diff --git a/examples/nextjs/lib/delivery-client.ts b/examples/nextjs/lib/delivery-client.ts
deleted file mode 100644
index a97de8c..0000000
--- a/examples/nextjs/lib/delivery-client.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Server-side fetch of an Experience via @contentful/experience-delivery.
- * Requires SPACE_ID and CDA_TOKEN in the environment.
- *
- * The delivery client's `GetExperienceViewResponse` is structurally
- * compatible with our `ExperiencePayload`, so we hand it straight to
- * `buildPlan` without normalization.
- */
-
-import 'server-only';
-
-import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
-import type { ExperiencePayload } from '@contentful/experiences-react';
-
-const experienceClient = new ContentfulViewDeliveryClient({ token: process.env.CDA_TOKEN! });
-
-export interface FetchExperienceResult {
- payload: ExperiencePayload;
-}
-
-/**
- * The delivery client returns a `GetExperienceViewResponse` whose shape is
- * structurally compatible with our `ExperiencePayload`. No normalization
- * step needed — pass it straight to `buildPlan`.
- */
-export async function fetchExperience(
- experienceId: string,
- options: { locale?: string; preview?: boolean } = {}
-): Promise {
- const spaceId = process.env.SPACE_ID || '';
- const environmentId = process.env.ENVIRONMENT_ID ?? 'master';
-
- const response = await experienceClient.view.getExperience(spaceId, environmentId, experienceId, {
- locale: options.locale,
- preview: options.preview ? 'true' : undefined,
- });
-
- return { payload: response };
-}
diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json
index b2db7e6..a52d2aa 100644
--- a/examples/nextjs/package.json
+++ b/examples/nextjs/package.json
@@ -12,7 +12,6 @@
},
"dependencies": {
"@contentful/experiences-react": "*",
- "@contentful/experience-delivery": "1.0.0-dev.3",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
diff --git a/examples/sveltekit/README.md b/examples/sveltekit/README.md
index 20676ea..67810df 100644
--- a/examples/sveltekit/README.md
+++ b/examples/sveltekit/README.md
@@ -4,8 +4,7 @@ A SvelteKit 2 + Svelte 5 app demonstrating `@contentful/experiences-svelte` rend
## What it shows
-- **Server-side fetch** via `@contentful/experience-delivery`'s `ContentfulViewDeliveryClient` (same client as the React example).
-- **Runtime-neutral plan resolution** with `resolveExperience` re-exported from `@contentful/experiences-svelte` — proves the resolver is genuinely framework-agnostic.
+- **Server-side fetch + resolve** via `fetchExperience` re-exported from `@contentful/experiences-svelte` — proves the fetch + resolver pipeline is genuinely framework-agnostic.
- **SSR rendering** with `ServerExperienceRenderer` from `@contentful/experiences-svelte`.
- **Hydration-safe viewport seeding** — User-Agent parsed on the server in `+page.server.ts`, passed as `initialViewportId`.
- **`defineComponent` authoring** — same pattern as the React example, with `component: SvelteComponent` instead of `render: () => ReactNode`.
@@ -41,7 +40,6 @@ examples/sveltekit/
│ │ ├── Header.svelte
│ │ ├── Page.svelte # used as the page-level template
│ │ └── Text.svelte
-│ ├── delivery-client.ts
│ ├── detect-viewport.ts
│ └── experience-config.ts # integration layer (components + templates → experienceConfig)
├── svelte.config.js
@@ -55,6 +53,6 @@ Identical to the Next.js example:
1. **Design-system components** stay portable — no `@contentful/*` imports.
2. **`experience-config.ts`** is the wiring layer that maps Contentful component-type IDs to your design-system components.
-3. **Routes** call `fetchExperience` → `resolveExperience` → ``.
+3. **Routes** call `fetchExperience(experienceOptions, clientOptions, resolveOptions)` → ``, wrapped in a try/catch that routes `NotFoundError` to SvelteKit's `error(404, ...)`.
The only Svelte-specific divergence: a customer component opts into slots via the `slot: Snippet<[string]>` dispatcher prop, calling `{@render slot('children')}` to render a named slot. Compare to the React adapter where each slot becomes its own named prop. See [`packages/adapter-svelte/README.md`](../../packages/adapter-svelte/README.md) for the full Svelte API surface.
diff --git a/examples/sveltekit/package.json b/examples/sveltekit/package.json
index e9b269d..f72b048 100644
--- a/examples/sveltekit/package.json
+++ b/examples/sveltekit/package.json
@@ -11,8 +11,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
- "@contentful/experiences-svelte": "*",
- "@contentful/experience-delivery": "1.0.0-dev.3"
+ "@contentful/experiences-svelte": "*"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.0",
diff --git a/examples/sveltekit/src/lib/delivery-client.ts b/examples/sveltekit/src/lib/delivery-client.ts
deleted file mode 100644
index 4c483cb..0000000
--- a/examples/sveltekit/src/lib/delivery-client.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Server-side fetch of an Experience via @contentful/experience-delivery.
- * Requires SPACE_ID and CDA_TOKEN in the environment (.env file).
- *
- * The delivery client's `GetExperienceViewResponse` is structurally
- * compatible with our `ExperiencePayload`, so we hand it straight to
- * `resolveExperience` without normalization.
- *
- * Env access goes through `$env/static/private` (SvelteKit's typed
- * server-only env import) instead of `process.env`. SvelteKit doesn't
- * auto-populate `process.env` from `.env` like Next.js does — it exposes
- * server-only vars via this module so they can never leak to the client
- * bundle. The values are inlined at build time.
- */
-
-import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
-import type { ExperiencePayload } from '@contentful/experiences-svelte';
-
-import { CDA_TOKEN, ENVIRONMENT_ID, SPACE_ID } from '$env/static/private';
-
-const experienceClient = new ContentfulViewDeliveryClient({ token: CDA_TOKEN });
-
-export interface FetchExperienceResult {
- payload: ExperiencePayload;
-}
-
-export async function fetchExperience(
- experienceId: string,
- options: { locale?: string; preview?: boolean } = {}
-): Promise {
- const response = await experienceClient.view.getExperience(
- SPACE_ID,
- ENVIRONMENT_ID || 'master',
- experienceId,
- {
- locale: options.locale,
- preview: options.preview ? 'true' : undefined,
- }
- );
-
- return { payload: response };
-}
diff --git a/examples/sveltekit/src/routes/[slug]/+page.server.ts b/examples/sveltekit/src/routes/[slug]/+page.server.ts
index 66518d8..f5412f6 100644
--- a/examples/sveltekit/src/routes/[slug]/+page.server.ts
+++ b/examples/sveltekit/src/routes/[slug]/+page.server.ts
@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit';
-import { resolveExperience } from '@contentful/experiences-svelte';
-import { fetchExperience } from '$lib/delivery-client.js';
+import { NotFoundError, fetchExperience } from '@contentful/experiences-svelte';
+import { CDA_TOKEN, ENVIRONMENT_ID, SPACE_ID } from '$env/static/private';
import { detectViewportFromUserAgent } from '$lib/detect-viewport.js';
import { experienceConfig } from '$lib/experience-config.js';
@@ -12,12 +12,26 @@ export const load: PageServerLoad = async ({ params, url, request }) => {
url.searchParams.get('preview') === 'true' || url.searchParams.get('preview') === '1';
const initialViewportId = detectViewportFromUserAgent(request.headers.get('user-agent') ?? '');
- const { payload } = await fetchExperience(params.slug, { preview: previewMode });
- if (!payload.nodes.length) error(404, 'Experience not found');
+ try {
+ const experience = await fetchExperience(
+ {
+ spaceId: SPACE_ID,
+ environmentId: ENVIRONMENT_ID || 'master',
+ experienceId: params.slug,
+ },
+ {
+ accessToken: CDA_TOKEN,
+ host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com',
+ },
+ {
+ config: experienceConfig,
+ context: { isPreview: previewMode, metadata: { slug: params.slug } },
+ }
+ );
- const experience = await resolveExperience(payload, experienceConfig, {
- experience: { isPreview: previewMode, metadata: { slug: params.slug } },
- });
-
- return { experience, previewMode, slug: params.slug, initialViewportId };
+ return { experience, previewMode, slug: params.slug, initialViewportId };
+ } catch (err) {
+ if (err instanceof NotFoundError) error(404, 'Experience not found');
+ throw err;
+ }
};
diff --git a/package-lock.json b/package-lock.json
index 229a002..403a25c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,7 +47,6 @@
"name": "@contentful/experiences-example-nextjs",
"version": "0.0.0",
"dependencies": {
- "@contentful/experience-delivery": "1.0.0-dev.3",
"@contentful/experiences-react": "*",
"next": "^15.0.0",
"react": "^19.0.0",
@@ -64,7 +63,6 @@
"name": "@contentful/experiences-example-sveltekit",
"version": "0.0.0",
"dependencies": {
- "@contentful/experience-delivery": "1.0.0-dev.3",
"@contentful/experiences-svelte": "*"
},
"devDependencies": {
@@ -150,6 +148,7 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -2137,6 +2136,10 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@contentful/experiences-client": {
+ "resolved": "packages/client",
+ "link": true
+ },
"node_modules/@contentful/experiences-core": {
"resolved": "packages/core",
"link": true
@@ -2249,6 +2252,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -2272,6 +2276,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -4539,6 +4544,7 @@
"integrity": "sha512-JXHbsDwRes1Wgyof3q5ApzzpbCWvinKXMQCiV67TFO6xlZPYLoK0fq3xQMqSicDMgCtFGqLkrQXkseOcASdZ8A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.9",
@@ -4644,6 +4650,7 @@
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
"debug": "^4.3.7",
@@ -4880,6 +4887,7 @@
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4904,6 +4912,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4960,6 +4969,7 @@
"integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.0",
"@typescript-eslint/types": "8.62.0",
@@ -5067,6 +5077,7 @@
"integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -5329,6 +5340,7 @@
"integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -5342,6 +5354,7 @@
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5763,6 +5776,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.38",
"caniuse-lite": "^1.0.30001799",
@@ -6273,6 +6287,7 @@
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
@@ -6778,6 +6793,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -6842,6 +6858,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9462,6 +9479,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@emnapi/core": "1.4.5",
"@emnapi/runtime": "1.4.5",
@@ -10103,6 +10121,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
@@ -10265,6 +10284,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10274,6 +10294,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -11343,6 +11364,7 @@
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -11856,6 +11878,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11987,6 +12010,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -12527,6 +12551,7 @@
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "1.6.1",
"@vitest/runner": "1.6.1",
@@ -13060,6 +13085,7 @@
"version": "0.4.0",
"license": "MIT",
"dependencies": {
+ "@contentful/experiences-client": "*",
"@contentful/experiences-core": "*",
"@contentful/experiences-design": "*"
},
@@ -13078,6 +13104,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
+ "@contentful/experiences-client": "*",
"@contentful/experiences-core": "*",
"@contentful/experiences-design": "*"
},
@@ -13095,6 +13122,15 @@
"svelte": "^5.0.0"
}
},
+ "packages/client": {
+ "name": "@contentful/experiences-client",
+ "version": "0.0.0-determined-by-semantic-release",
+ "license": "MIT",
+ "dependencies": {
+ "@contentful/experience-delivery": "1.0.0-dev.3",
+ "@contentful/experiences-core": "*"
+ }
+ },
"packages/core": {
"name": "@contentful/experiences-core",
"version": "0.4.0",
diff --git a/packages/adapter-react/README.md b/packages/adapter-react/README.md
index bf64022..d77fed4 100644
--- a/packages/adapter-react/README.md
+++ b/packages/adapter-react/README.md
@@ -5,10 +5,10 @@
The React adapter for the Contentful Experiences SDK suite. Renders Experience payloads from the Experience Delivery API (XDA) using customer-supplied React components.
```sh
-npm install @contentful/experiences-react @contentful/experience-delivery
+npm install @contentful/experiences-react
```
-This is the **only SDK package customers install** — it re-exports everything from `@contentful/experiences-core` and `@contentful/experiences-design` that consumers need. The other packages are workspace-internal.
+This is the **only SDK package customers install** — it re-exports everything from `@contentful/experiences-core`, `@contentful/experiences-design`, and `@contentful/experiences-client` that consumers need. The other packages are workspace-internal.
---
@@ -21,6 +21,17 @@ defineComponent(config); // Type-narrowing identity for component-type co
defineTemplate(config); // Same shape, for page-level template wrappers
```
+### Fetching
+
+```ts
+fetchExperience(experienceOptions, clientOptions, resolveOptions) // Async — fetches from XDA + resolves in one call
+createClient(options) // Functional constructor matching the SDK's option shape
+ContentfulViewDeliveryClient // Re-exported delivery client for advanced use cases
+NotFoundError // Thrown when the Experience ID doesn't exist
+ContentfulViewDelivery // Full error namespace from the delivery client
+type ExperienceOptions, ClientOptions, ResolveOptions, CreateClientOptions
+```
+
### Resolver
```ts
@@ -59,10 +70,12 @@ getValueForViewport, getViewportIndex, resolveDesignProperties, toCssMediaQuery
## Quick reference
```tsx
+import { notFound } from 'next/navigation';
import {
defineComponent,
defineTemplate,
- resolveExperience,
+ fetchExperience,
+ NotFoundError,
ServerExperienceRenderer,
type Config,
type Components,
@@ -82,8 +95,17 @@ const components: Components = {
const experienceConfig: Config = { components };
// In a server component:
-const experience = await resolveExperience(payload, experienceConfig);
-return ;
+try {
+ const experience = await fetchExperience(
+ { spaceId: process.env.SPACE_ID!, environmentId: 'master', experienceId: slug },
+ { accessToken: process.env.CDA_TOKEN! },
+ { config: experienceConfig }
+ );
+ return ;
+} catch (err) {
+ if (err instanceof NotFoundError) notFound();
+ throw err;
+}
```
For the full getting-started walkthrough, the merge-precedence rules, viewport handling, and design rationale, see the [root README](../../README.md) and [`AGENTS.md`](../../AGENTS.md).
diff --git a/packages/adapter-react/package.json b/packages/adapter-react/package.json
index 2ee4488..60ce4b0 100644
--- a/packages/adapter-react/package.json
+++ b/packages/adapter-react/package.json
@@ -30,6 +30,7 @@
"directory": "packages/adapter-react"
},
"dependencies": {
+ "@contentful/experiences-client": "*",
"@contentful/experiences-core": "*",
"@contentful/experiences-design": "*"
},
diff --git a/packages/adapter-react/src/index.ts b/packages/adapter-react/src/index.ts
index bb304e6..42478dc 100644
--- a/packages/adapter-react/src/index.ts
+++ b/packages/adapter-react/src/index.ts
@@ -87,3 +87,18 @@ export {
resolveDesignProperties,
toCssMediaQuery,
} from '@contentful/experiences-design';
+
+// ─── Delivery client + fetchExperience ────────────────────────────────────
+export {
+ ContentfulViewDelivery,
+ ContentfulViewDeliveryClient,
+ NotFoundError,
+ createClient,
+ fetchExperience,
+} from '@contentful/experiences-client';
+export type {
+ ClientOptions,
+ CreateClientOptions,
+ ExperienceOptions,
+ ResolveOptions,
+} from '@contentful/experiences-client';
diff --git a/packages/adapter-svelte/package.json b/packages/adapter-svelte/package.json
index 9527fd1..cdfe371 100644
--- a/packages/adapter-svelte/package.json
+++ b/packages/adapter-svelte/package.json
@@ -35,6 +35,7 @@
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
+ "@contentful/experiences-client": "*",
"@contentful/experiences-core": "*",
"@contentful/experiences-design": "*"
},
diff --git a/packages/adapter-svelte/src/index.ts b/packages/adapter-svelte/src/index.ts
index 893c596..e8d166a 100644
--- a/packages/adapter-svelte/src/index.ts
+++ b/packages/adapter-svelte/src/index.ts
@@ -91,3 +91,18 @@ export {
resolveDesignProperties,
toCssMediaQuery,
} from '@contentful/experiences-design';
+
+// ─── Delivery client + fetchExperience ────────────────────────────────────
+export {
+ ContentfulViewDelivery,
+ ContentfulViewDeliveryClient,
+ NotFoundError,
+ createClient,
+ fetchExperience,
+} from '@contentful/experiences-client';
+export type {
+ ClientOptions,
+ CreateClientOptions,
+ ExperienceOptions,
+ ResolveOptions,
+} from '@contentful/experiences-client';
diff --git a/packages/client/README.md b/packages/client/README.md
new file mode 100644
index 0000000..eea2e50
--- /dev/null
+++ b/packages/client/README.md
@@ -0,0 +1,113 @@
+# @contentful/experiences-client
+
+> **Internal package.** Not published separately. Consumed via the framework adapter (`@contentful/experiences-react`, `@contentful/experiences-svelte`, etc.).
+
+Isolates `@contentful/experience-delivery` — the generated experience delivery client — so that `@contentful/experiences-core` stays zero-dep and framework adapters that don't need network access don't pull it in transitively.
+
+---
+
+## What's in here
+
+### `fetchExperience(experienceOptions, clientOptions, resolveOptions)`
+
+The primary fetch + resolve entry point. Fetches an Experience payload from the Experience Delivery API and resolves it into a `PortableRenderPlan` in one call.
+
+Three positional args group by concern:
+
+| Arg | Type | Purpose |
+| ------------------- | --------------------------------------------------- | ------------------------------------------------- |
+| `experienceOptions` | `{ spaceId, environmentId, experienceId, locale? }` | Which Experience to fetch. |
+| `clientOptions` | `{ accessToken, host? }` **or** `{ client }` | How to fetch — inline creds or a pre-made client. |
+| `resolveOptions` | `{ config, context? }` | How to resolve — component registry + context. |
+
+```ts
+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!,
+ host: 'https://preview.xdn.contentful.com', // optional — omit for default endpoint
+ },
+ {
+ config: experienceConfig,
+ context: { isPreview: false, metadata: { slug } }, // flows into resolveData hooks
+ }
+);
+
+// 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).
+
+### `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-creds path.
+
+```ts
+import { createClient } from '@contentful/experiences-react';
+
+const client = createClient({
+ accessToken: process.env.CDA_TOKEN!,
+ host: 'https://preview.xdn.contentful.com', // optional
+ // headers, timeoutInSeconds, maxRetries, fetch, logging pass through
+});
+```
+
+### `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:
+
+```ts
+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).
+
+```ts
+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-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-delivery` from anywhere except this package.
+- Re-export only what framework adapters need to surface to customers.
+- Keep `fetchExperience` thin — fetch + cast + resolve. Business logic belongs in `packages/core`.
+- Name mappings between SDK options and delivery-client options live in `create-client.ts` — one place to change.
diff --git a/packages/client/package.json b/packages/client/package.json
new file mode 100644
index 0000000..aa68e16
--- /dev/null
+++ b/packages/client/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@contentful/experiences-client",
+ "version": "0.0.0-determined-by-semantic-release",
+ "description": "Delivery client + fetchExperience for Contentful Experiences",
+ "license": "MIT",
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "sideEffects": false,
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist",
+ "README.md",
+ "CHANGELOG.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/contentful/experiences.git",
+ "directory": "packages/client"
+ },
+ "dependencies": {
+ "@contentful/experience-delivery": "1.0.0-dev.3",
+ "@contentful/experiences-core": "*"
+ }
+}
diff --git a/packages/client/project.json b/packages/client/project.json
new file mode 100644
index 0000000..4786a8c
--- /dev/null
+++ b/packages/client/project.json
@@ -0,0 +1,28 @@
+{
+ "name": "client",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/client/src",
+ "projectType": "library",
+ "tags": ["scope:runtime-neutral", "layer:client"],
+ "targets": {
+ "build": {
+ "cache": true,
+ "dependsOn": ["^build"],
+ "outputs": ["{projectRoot}/dist"],
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "packages/client",
+ "command": "tsup"
+ }
+ },
+ "test": {
+ "cache": true,
+ "dependsOn": ["^build"],
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "packages/client",
+ "command": "vitest run"
+ }
+ }
+ }
+}
diff --git a/packages/client/src/create-client.test.ts b/packages/client/src/create-client.test.ts
new file mode 100644
index 0000000..c647061
--- /dev/null
+++ b/packages/client/src/create-client.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
+import { createClient } from './create-client.js';
+
+vi.mock('@contentful/experience-delivery', () => ({
+ ContentfulViewDeliveryClient: vi.fn().mockImplementation((options) => ({ _options: options })),
+}));
+
+describe('createClient', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('maps accessToken → token and passes host through as baseUrl', () => {
+ createClient({ accessToken: 'token-123', host: 'https://preview.xdn.contentful.com' });
+
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledWith({
+ token: 'token-123',
+ baseUrl: 'https://preview.xdn.contentful.com',
+ });
+ });
+
+ it('omits baseUrl when host is not provided', () => {
+ createClient({ accessToken: 'token-123' });
+
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledWith({
+ token: 'token-123',
+ baseUrl: undefined,
+ });
+ });
+
+ it('passes through additional client options', () => {
+ createClient({
+ accessToken: 'token-123',
+ host: 'https://xdn.contentful.com',
+ headers: { 'x-custom': 'value' },
+ timeoutInSeconds: 30,
+ maxRetries: 5,
+ });
+
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledWith({
+ token: 'token-123',
+ baseUrl: 'https://xdn.contentful.com',
+ headers: { 'x-custom': 'value' },
+ timeoutInSeconds: 30,
+ maxRetries: 5,
+ });
+ });
+
+ it('returns a ContentfulViewDeliveryClient instance', () => {
+ const client = createClient({ accessToken: 'token-123' });
+
+ expect(client).toBeDefined();
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledOnce();
+ });
+});
diff --git a/packages/client/src/create-client.ts b/packages/client/src/create-client.ts
new file mode 100644
index 0000000..b9a699a
--- /dev/null
+++ b/packages/client/src/create-client.ts
@@ -0,0 +1,15 @@
+import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
+
+export type CreateClientOptions = {
+ accessToken: string;
+ host?: string;
+} & Omit;
+
+export function createClient(options: CreateClientOptions): ContentfulViewDeliveryClient {
+ const { accessToken, host, ...rest } = options;
+ return new ContentfulViewDeliveryClient({
+ ...rest,
+ token: accessToken,
+ baseUrl: host,
+ });
+}
diff --git a/packages/client/src/fetch-experience.test.ts b/packages/client/src/fetch-experience.test.ts
new file mode 100644
index 0000000..583a97e
--- /dev/null
+++ b/packages/client/src/fetch-experience.test.ts
@@ -0,0 +1,140 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
+import { fetchExperience } from './fetch-experience.js';
+
+const { mockGetExperience, mockPayload, mockPlan } = vi.hoisted(() => {
+ const mockPayload = {
+ sys: { id: 'exp-1' },
+ viewports: [{ id: 'default', query: '*' }],
+ nodes: [{ sys: { urn: 'urn:ctfl:component:hero' }, content: {}, design: {}, slots: {} }],
+ errors: [],
+ };
+
+ const mockPlan = {
+ viewports: mockPayload.viewports,
+ nodes: [],
+ };
+
+ const mockGetExperience = vi.fn().mockResolvedValue(mockPayload);
+
+ return { mockGetExperience, mockPayload, mockPlan };
+});
+
+vi.mock('@contentful/experiences-core', () => ({
+ resolveExperience: vi.fn().mockResolvedValue(mockPlan),
+}));
+
+vi.mock('@contentful/experience-delivery', () => ({
+ ContentfulViewDeliveryClient: vi.fn().mockImplementation(() => ({
+ view: {
+ getExperience: mockGetExperience,
+ },
+ })),
+}));
+
+const experienceOptions = {
+ spaceId: 'space-1',
+ environmentId: 'master',
+ experienceId: 'exp-1',
+};
+
+const resolveOptions = {
+ config: { components: {} },
+};
+
+describe('fetchExperience', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetExperience.mockResolvedValue(mockPayload);
+ });
+
+ describe('inline credentials', () => {
+ it('constructs client without baseUrl when host is not provided', async () => {
+ await fetchExperience(experienceOptions, { accessToken: 'token-123' }, resolveOptions);
+
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledWith({
+ token: 'token-123',
+ baseUrl: undefined,
+ });
+ });
+
+ it('constructs client with provided host', async () => {
+ await fetchExperience(
+ experienceOptions,
+ { accessToken: 'token-123', host: 'https://preview.xdn.contentful.com' },
+ resolveOptions
+ );
+
+ expect(ContentfulViewDeliveryClient).toHaveBeenCalledWith({
+ token: 'token-123',
+ baseUrl: 'https://preview.xdn.contentful.com',
+ });
+ });
+
+ it('calls getExperience with spaceId, environmentId, experienceId, locale', async () => {
+ await fetchExperience(
+ { ...experienceOptions, locale: 'en-US' },
+ { accessToken: 'token-123' },
+ resolveOptions
+ );
+
+ expect(mockGetExperience).toHaveBeenCalledWith('space-1', 'master', 'exp-1', {
+ locale: 'en-US',
+ });
+ });
+ });
+
+ describe('pre-created client', () => {
+ it('uses provided client directly without constructing a new one', async () => {
+ const client = new ContentfulViewDeliveryClient({ token: 'token-123' });
+ vi.mocked(ContentfulViewDeliveryClient).mockClear();
+
+ await fetchExperience(experienceOptions, { client }, resolveOptions);
+
+ expect(ContentfulViewDeliveryClient).not.toHaveBeenCalled();
+ expect(mockGetExperience).toHaveBeenCalledWith('space-1', 'master', 'exp-1', {
+ locale: undefined,
+ });
+ });
+ });
+
+ describe('return value', () => {
+ it('passes empty-nodes payloads through to the resolver', async () => {
+ const emptyPayload = { ...mockPayload, nodes: [] };
+ mockGetExperience.mockResolvedValue(emptyPayload);
+ const { resolveExperience } = await import('@contentful/experiences-core');
+
+ const result = await fetchExperience(
+ experienceOptions,
+ { accessToken: 'token-123' },
+ resolveOptions
+ );
+
+ expect(resolveExperience).toHaveBeenCalledWith(
+ emptyPayload,
+ resolveOptions.config,
+ expect.anything()
+ );
+ expect(result).toEqual(mockPlan);
+ });
+
+ it('returns resolved PortableRenderPlan on success', async () => {
+ const result = await fetchExperience(
+ experienceOptions,
+ { accessToken: 'token-123' },
+ resolveOptions
+ );
+
+ expect(result).toEqual(mockPlan);
+ });
+
+ it('propagates NotFoundError from the delivery client to the caller', async () => {
+ class MockNotFoundError extends Error {}
+ mockGetExperience.mockRejectedValue(new MockNotFoundError('experience not found'));
+
+ await expect(
+ fetchExperience(experienceOptions, { accessToken: 'token-123' }, resolveOptions)
+ ).rejects.toThrow('experience not found');
+ });
+ });
+});
diff --git a/packages/client/src/fetch-experience.ts b/packages/client/src/fetch-experience.ts
new file mode 100644
index 0000000..ce53d98
--- /dev/null
+++ b/packages/client/src/fetch-experience.ts
@@ -0,0 +1,46 @@
+import type { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';
+import { resolveExperience } from '@contentful/experiences-core';
+import type {
+ ExperiencePayload,
+ PortableRenderPlan,
+ ResolveExperienceOptions,
+ ResolverConfig,
+} from '@contentful/experiences-core';
+import { createClient } from './create-client.js';
+
+export type ExperienceOptions = {
+ spaceId: string;
+ environmentId: string;
+ experienceId: string;
+ locale?: string;
+};
+
+export type ClientOptions =
+ | { accessToken: string; host?: string }
+ | { client: ContentfulViewDeliveryClient };
+
+export type ResolveOptions = {
+ config: ResolverConfig;
+ context?: ResolveExperienceOptions['experience'];
+};
+
+export async function fetchExperience(
+ experienceOptions: ExperienceOptions,
+ clientOptions: ClientOptions,
+ resolveOptions: ResolveOptions
+): Promise {
+ const { spaceId, environmentId, experienceId, locale } = experienceOptions;
+ const { config, context } = resolveOptions;
+
+ const client =
+ 'client' in clientOptions
+ ? clientOptions.client
+ : createClient({ accessToken: clientOptions.accessToken, host: clientOptions.host });
+
+ // Response from the experience delivery client is structurally compatible with ExperiencePayload (superset)
+ const payload = (await client.view.getExperience(spaceId, environmentId, experienceId, {
+ locale,
+ })) as unknown as ExperiencePayload;
+
+ return resolveExperience(payload, config, { experience: context });
+}
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
new file mode 100644
index 0000000..c37aeeb
--- /dev/null
+++ b/packages/client/src/index.ts
@@ -0,0 +1,12 @@
+import { ContentfulViewDelivery } from '@contentful/experience-delivery';
+
+export {
+ ContentfulViewDelivery,
+ ContentfulViewDeliveryClient,
+} from '@contentful/experience-delivery';
+export const NotFoundError = ContentfulViewDelivery.NotFoundError;
+export type NotFoundError = InstanceType;
+export { createClient } from './create-client.js';
+export type { CreateClientOptions } from './create-client.js';
+export { fetchExperience } from './fetch-experience.js';
+export type { ExperienceOptions, ClientOptions, ResolveOptions } from './fetch-experience.js';
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
new file mode 100644
index 0000000..24fcdff
--- /dev/null
+++ b/packages/client/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "types": ["node", "vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "vitest.config.ts", "tsup.config.ts"]
+}
diff --git a/packages/client/tsconfig.lib.json b/packages/client/tsconfig.lib.json
new file mode 100644
index 0000000..6f37ea2
--- /dev/null
+++ b/packages/client/tsconfig.lib.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "noEmit": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["**/*.test.ts", "**/*.spec.ts", "vitest.config.ts", "tsup.config.ts"]
+}
diff --git a/packages/client/tsup.config.ts b/packages/client/tsup.config.ts
new file mode 100644
index 0000000..e59a519
--- /dev/null
+++ b/packages/client/tsup.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['src/**/*.ts', '!src/**/*.test.ts'],
+ format: ['esm'],
+ dts: true,
+ clean: true,
+ sourcemap: true,
+ target: 'es2022',
+ outDir: 'dist',
+ tsconfig: 'tsconfig.lib.json',
+ bundle: false,
+ external: [/^@contentful\//],
+});
diff --git a/packages/client/vitest.config.ts b/packages/client/vitest.config.ts
new file mode 100644
index 0000000..5cf3b04
--- /dev/null
+++ b/packages/client/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ include: ['src/**/*.{test,spec}.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'lcov'],
+ include: ['src/**/*.ts'],
+ exclude: ['**/*.test.ts', '**/*.spec.ts', '**/*.d.ts'],
+ },
+ },
+});