feat: add packages/client with fetchExperience and delivery client [AIS-147]#30
Conversation
…ent re-export Creates the packages/client workspace package (@contentful/experiences-client). Replaces the ExperienceClient class approach from PR #27 — re-exports ContentfulViewDeliveryClient directly from @contentful/experience-delivery so consumers have full client access without a separate install. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ture
fetchExperience accepts either inline credentials ({ accessToken, preview?,
spaceId, environmentId, experienceId }) or a pre-created
ContentfulViewDeliveryClient ({ client, spaceId, environmentId, experienceId }).
Inline path constructs the client internally with XDN or XPA base URL based
on the preview flag. Pre-created client path is pass-through, enabling reuse
across calls. When personalization is passed, dispatches to
getExperienceWithOverrides and opts into sourceMap automatically.
Returns PortableRenderPlan | null (null when payload has no nodes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ience Both @contentful/experiences-react and @contentful/experiences-svelte now re-export ContentfulViewDeliveryClient and fetchExperience from @contentful/experiences-client, so consumers install only the framework adapter and never touch @contentful/experience-delivery or @contentful/experiences-client directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tful/experience-delivery Both example apps now import fetchExperience and ContentfulViewDeliveryClient from their framework adapter rather than @contentful/experience-delivery directly. The separate resolveExperience call is removed — fetch and resolve happen in one step via fetchExperience. delivery-client.ts in each example wraps the SDK call with a thin page-level helper that reads env vars and passes the config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… delivery-client wrappers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ttier Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…elivery namespace for request type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce unconditionally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chase Poirier (chasepoirier)
left a comment
There was a problem hiding this comment.
Overall really liking this approach, nice work 🚀 Especially the idea of moving resolveExperience into the fetchExperience function.
| locale, | ||
| })) as unknown as ExperiencePayload; | ||
|
|
||
| if (!payload?.nodes?.length) return null; |
There was a problem hiding this comment.
Do we want to only return null here or throw an error? Other option could be still return null but console.warn to let the developer know the nodes were empty.
I could go either way on leaving as is or being more opinionated that this is an error. Let me know your thoughts!
There was a problem hiding this comment.
Updating to remove this line, and instead if there is an empty experience, that is just passed through the resolver. I also am updating the examples to show better error handling for the case when there's an error fetching the experience
| context?: ResolveExperienceOptions['experience']; | ||
| } & ({ accessToken: string; preview?: boolean } | { client: ContentfulViewDeliveryClient }); | ||
|
|
||
| export async function fetchExperience( |
There was a problem hiding this comment.
Really liking how this feels to have resolve experience bundled into this helper function, feels like a very smooth DX since they don't need two separate async function calls. Very nice!
Some food for thought on this function: As this function now does more, the amount of params is growing and it may grow even more when personalization and the channels/digital properties RFC is added into the picture.
One idea we could explore is having multiple params and grouping like-params together instead of one large object. An idea here could be 3 params with param 1 being the "identifier" for the experience to fetch (idea for separating this is because maybe this because a "digital property" object in the future compared to just an experienceId), param 2 is the client and param 3 is all experience related config
Example:
// this can also be in a separate file
const client = createClient(/** all client params here */)
const experience = fetchExperience(experienceId, client, {
config,
context
})Let me know your thoughts!
There was a problem hiding this comment.
Agreed that the params for this function will increase as we start adding support for other features. I'm in support of grouping like-parms together 👍
There was a problem hiding this comment.
Based on this feedback I've grouped them into three concerns: which experience to fetch, how to fetch, and how to resolve
| @@ -0,0 +1,3 @@ | |||
| export { ContentfulViewDeliveryClient } from '@contentful/experience-delivery'; | |||
There was a problem hiding this comment.
I don't think @contentful/experience-delivery currently exposes a createClient type of function to instantiate a new client.
Thoughts on adding an additional helper here to do so? This would be a similar pattern to what the CMA does with something like:
// this can also be in a separate file
const client = createClient(/** all client params here */)Mainly would be an add for DX preference in case devs would rather use a functional approach vs doing
const client = new ContentfulViewDeliveryClient()There was a problem hiding this comment.
Yeah I like adding createClient, I agree that I think it improves the DX. I didn't see anything in the client for that, so I think we can add it here for now, and then could consider if it's worth adding it directly to the client in the future
There was a problem hiding this comment.
Added the createClient function and ensure that it uses the same param names as fetchExperience, mapping them to the proper names for the experience delivery client. Also updated so that if no host/baseURL is passed in, then that's handled in the experience delivery client to use the default instead of having that logic in this SDK.
…ce preview with host
Signature is now `fetchExperience(experienceOptions, clientOptions, resolveOptions)`,
with `ClientOptions` a discriminated union of `{ accessToken, host? }` or `{ client }`.
`host` is a full base URL string that's passed through as `baseUrl`; XPA vs XDN
endpoint selection is now the caller's concern. Defaults to XDN when omitted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…en/baseUrl createClient is a functional constructor over ContentfulViewDeliveryClient that maps the SDK's option names (accessToken, host) to the delivery client's underlying names (token, baseUrl). Other options pass through unchanged. fetchExperience's inline-creds branch now routes through createClient, making it the single source of truth for the name mapping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…se NotFoundError fetchExperience no longer treats an empty-nodes payload as "not found" — the resolver handles empty nodes gracefully and returns a valid PortableRenderPlan. Return type narrows from `PortableRenderPlan | null` to `PortableRenderPlan`. To keep the missing-experience case as a proper 404 (not a 500), NotFoundError is now re-exported from the client + adapter packages. Example call sites wrap fetchExperience in try/catch and route NotFoundError to their framework's 404 idiom (notFound() in Next, error(404) in SvelteKit). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the three review-driven changes: - 3 positional args (experienceOptions, clientOptions, resolveOptions) - createClient helper + host string in place of preview boolean - Empty-nodes payloads pass through; NotFoundError re-exported for the missing-experience case Adds design-decision entries in AGENTS.md capturing the *why* behind each choice so future contributors don't re-litigate them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AIS-147 · AIS-148
Summary
packages/client(@contentful/experiences-client) — a new internal workspace package that owns the@contentful/experience-deliverydependency and exposesfetchExperience,createClient,ContentfulViewDeliveryClient, andNotFoundError@contentful/experiences-reactand@contentful/experiences-svelteso customers install only the framework adapter;@contentful/experience-deliveryis no longer a direct customer depfetchExperiencedirectly into both example apps; removes thedelivery-client.tswrapper files that added no valuefetchExperienceFunction that fetches an Experience payload from Contentful's Experience Delivery API (XDA) and resolves it into a runtime-neutral
PortableRenderPlanready to hand to a renderer, all in one call.clientOptionsis a discriminated union — inline creds ({ accessToken, host? }) or a pre-made client ({ client })hostreplaces the oldpreview: boolean. It's a full base-URL string that passes straight through to the delivery client'sbaseUrl— the SDK doesn't own XDN/XPA URL constants, callers pick the endpointcreateClient({ accessToken, host? })is a functional constructor that maps the SDK's option names to the underlying delivery client (accessToken → token,host → baseUrl). Prefer overnew ContentfulViewDeliveryClient(...)so the two paths throughfetchExperienceuse the same field namesHow this fits with the other draft PRs
Optimization/personalization spike: That PR adds
getExperienceWithOverridesusage and sourceMap threading to wire in the Optimization SDK. This PR intentionally omits the personalization path, but when it's ready to be implemented,resolveOptions(arg 3) can add apersonalizationfield that dispatches togetExperienceWithOverrides. The call sites for other args stay unchanged for customers who don't use it.Preview SDK (three-layer workspace packages): The preview POC adds
enablePreviewonExperienceRendererand apostMessagetransport layer. The live editor preview wire in the PR is a separate concern handled by the renderer after hydration and is fully compatible with this PR.Test plan
npm run build— all 5 packages build cleanly (NX topological order: core → design → client → adapters)npx vitest run packages/client— 11 tests pass (create-client name mapping, XDN default, host passthrough, pre-created-client passthrough, empty-nodes resolves to a plan, NotFoundError propagates)@contentful/experience-deliveryabsent fromexamples/nextjs/package.jsonandexamples/sveltekit/package.jsonfetchExperience,createClient,NotFoundError, andContentfulViewDeliveryClientimportable from@contentful/experiences-react@contentful/experiences-svelteGenerated with Claude Code