Skip to content

feat: decouple example apps from test space + add setup script [AIS-246]#77

Open
Lisa White (whitelisab) wants to merge 19 commits into
mainfrom
feat/ais-246-example-app
Open

feat: decouple example apps from test space + add setup script [AIS-246]#77
Lisa White (whitelisab) wants to merge 19 commits into
mainfrom
feat/ais-246-example-app

Conversation

@whitelisab

@whitelisab Lisa White (whitelisab) commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

AIS-246

Summary

  • Adds examples/scripts/bootstrap-example.ts — a one-time setup script that provisions the demo Experience into a caller-specified space + environment via contentful-management@12.6.0-dev.4. Idempotent in 9 phases: ContentTypes → Assets → Entries → DesignTokens (raw HTTP; not yet in the plain client) → ComponentTypes → Template → DataAssemblies → DA↔ComponentType links → Experience. Prints the seeded experienceId at the end.
  • Splits examples/ (stable, customer-facing) from test-apps/ (internal scratchpad). Both example apps + the SvelteKit port are wired up to render the seeded Experience end-to-end; test-apps carry the original placeholder content so we can iterate without touching the customer surface.
  • Rewrites the Next.js and SvelteKit example READMEs (plus the top-level README and AGENTS.md) with the new bootstrap-then-run flow: numbered steps for basic vs preview, a 3-row token summary (CMA_TOKEN / CDA_TOKEN / CPA_TOKEN), and clickable "Try it locally" URLs.

Test plan

  • npm run bootstrap against a clean space — provisions everything, publishes clean, prints experienceId: landing. Re-running skips existing resources per phase.
  • Next.js /landing renders the seeded hero + 2 cards against that space (HTTP 200, content resolved through DataAssembly, assets served from images.ctfassets.net).
  • Next.js /advanced/landing?preview=true&locale=en-US renders identically to the basic route with CPA_TOKEN set — async resolveData on card, User-Agent → viewport seeding, preview host swap all firing.
  • SvelteKit /landing renders 1:1 parity output against the same seeded space.
  • npx tsc --noEmit clean in examples/scripts; example apps build and dev-serve without errors.

Known limitations (worth follow-ups)

  • SDK ESM extension gappackages/core/dist/index.js emits extensionless relative imports (export * from "./types"), which strict-ESM consumers (Vite/SvelteKit SSR) reject. Worked around in examples/sveltekit/vite.config.ts via ssr.noExternal; real fix is .js suffixes at the source. Not scoped to this ticket.

🤖 Generated with Claude Code

Lisa White (whitelisab) and others added 19 commits July 20, 2026 12:40
…es/ [AIS-246]

Copy examples/{nextjs,sveltekit} to test-apps/{nextjs,sveltekit} so we have
an internal place to iterate without mutating the customer-facing example
apps. Test-app package names renamed to @contentful/experiences-testapp-*
to avoid workspace collisions. examples/ untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fresh minimal fixture that a bootstrap script will provision into a caller's
space. Shape: 1 promotion ContentType, 3 promotion entries, 3 image assets,
8 ComponentTypes (Section/Heading/RichText/Text/Button/Image + hero-plain +
card), 1 page template, 2 DataAssemblies (hero/card), and 1 Experience
composed of 1 hero + 2 cards. Uses tempId placeholders so the fixture is
self-referential without baking target-space ids in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…AIS-246]

examples/scripts/bootstrap-example.ts provisions the fixture into a caller-
specified space + env in 9 steps: ContentTypes, Assets, Entries, Design
tokens, ComponentTypes, Template, DataAssemblies, DA→ComponentType links,
Experience. Uses contentful-management@12.6.0-dev.4 for the ExO surface
(componentType / template / dataAssembly / experience plain clients) and
raw HTTP for design_tokens (not yet in the plain client). Idempotent per
resource.

Verified end-to-end against f7tvosi12w6q / exo-sdk — Experience "landing"
publishes cleanly.

RichText body is temporarily dropped from the DataAssembly return mapping —
the DA validator rejects our body→document GraphQL alias with "Field path
_node.body not found." Symbol fields (title, ctaLabel, ctaUrl, image.url)
work. Follow-up needed to figure out the RichText mapping shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates the Next.js + SvelteKit example READMEs, the top-level README's
Examples section, and AGENTS.md "Run the example app" so the customer flow
is clear: cd examples/scripts && npm run bootstrap; then run the app with a
CDA token. Adds a scripts/README.md that explains what the bootstrap
provisions and calls out known limitations (RichText body omission,
CMA dev-build pin, design_tokens via raw HTTP).

Adds scripts/.env.example and updates both example .env.example files with
inline pointers to the bootstrap flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-246]

The page template was being seeded without a componentTree, so the CDA
wrapped the whole Experience in a Template-variant node whose slots.content
never surfaced. The SDK's resolver skips Template-variant nodes (v1 scope),
so the entire tree rendered empty.

Two changes:

1. Fixture: the page template now declares slots: [{ id: 'content' }] plus
   a componentTree with a single Slot node pointing at that slot. That's
   what tells the resolver where in the template to drop the Experience's
   content-slot nodes.

2. Bootstrap: templates carrying a componentTree need the
   Contentful:ComposedImplementation annotation — the server-assigned
   default (Contentful:CodedImplementation) requires an empty tree and
   publish 422s otherwise. seedTemplate now sets that annotation on both
   create and update, and update-detection compares annotations so
   fixture edits actually re-publish. seedExperience always upserts on
   existing records so template/DA changes further up the run don't leave
   the Experience with a stale defining-entity version at publish time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Next.js implementations for the two composed ComponentTypes the
bootstrap fixture provisions:

- components/HeroPlain.tsx — title + CTA + hero image, 2-column grid
- components/Card.tsx — image + title + teaser + CTA in a shadowed panel

Both read design values via useDesignValues() and consume tempate-bound
content properties as regular React props. Wired into experience-config.tsx
as `'hero-plain'` and `card` alongside the existing primitives.

Also adds color.primary and color.primaryText to design-tokens.ts (the hero
uses them as background + foreground).

Verified: `npm run dev` against f7tvosi12w6q/exo-sdk renders /landing with
the hero + 2 cards, all content resolved from Promotion entries via
DataAssembly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hero CTA now points at /contact/sales/ (not /request-demo/), and the On
card CTA at /case-studies/on/ (not /customers/on/). Live entries in
f7tvosi12w6q/exo-sdk were also patched + republished so the running demo
reflects the fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /advanced/[slug] route was crashing because its config only knew
about the six primitives — hero-plain and card weren't registered, so the
SDK's MissingComponent kicked in and hit an SSR bug in useExperience().

Registers hero-plain (bare) and card (with async resolveData) on the
advanced config, adds resolveToken so design tokens flow the same way as
the simple route, and drops the old Button-level resolveData demo (Button
is an internal render detail of the composed CTs now).

The card's resolveData does two things worth showing:
  1. A stand-in async enrichment fetch — prepends "Featured:" to the title.
  2. A metadata-aware CTA rewrite — non-http URLs get prefixed with
     locale + slug from experience.metadata.

Verified: /advanced/landing renders end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…AIS-246]

The Next.js and SvelteKit example indexes had /demo hardcoded from before
the fixture existed, which 404s against a freshly bootstrapped space (the
seeded id is `landing`). Also updated the "replace demo with a real id"
note on the Next.js index to reflect the new default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Preview mode reads from preview.xdn.contentful.com, which requires a
Content Preview token — the CDA token is rejected there. Both examples
were hardcoded to always use CDA_TOKEN, so /advanced/[slug]?preview=true
(and the "Advanced demo (preview)" button on the Next.js index) crashed
with UnauthorizedError on any real space.

Both routes now pick CPA_TOKEN when previewMode is on (falls back to CDA
if CPA is unset — the request will 401 in that case, which is still an
improvement over silently sending the wrong token to the wrong host).

Adds CPA_TOKEN to both .env.example files with a note on when it's needed,
and updates both READMEs to document the three tokens (CMA / CDA / CPA)
and which route uses which.

Also switches sveltekit to $env/dynamic/private so new env vars don't
need a code change to be readable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… [AIS-246]

The old "Run it" section only walked through the basic /landing path;
readers who then clicked the "Advanced demo (preview)" button on the
index page got a 500 with no docs pointing at CPA_TOKEN.

Restructures both example READMEs (and touches the top-level README) so:

- Steps are numbered and labeled (1 = seed, 2a = basic, 2b = preview) and
  each step tells you exactly which token env var it needs.
- The Next.js "Two routes, same data" table gets a "Try it locally"
  column with clickable URLs instead of just source-file links (source
  links moved into a caption below).
- Tokens moved into a compact three-row summary table so it's clear
  which token each step needs, without repeating the guidance inline.
- SvelteKit README gets the same shape — one main route + a "preview
  mode" subsection instead of burying it in a bullet.

Verified preview mode end-to-end against f7tvosi12w6q/exo-sdk with a
CPA token — /advanced/landing?preview=true returns 200 and renders the
hero + 2 cards with async resolveData firing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-246]

Node's strict ESM loader rejects the extensionless relative imports in
@contentful/experiences-client's dist (e.g. `from "./types"`), which
comes in transitively via @contentful/experiences-svelte. The other three
workspace packages were already listed in ssr.noExternal — this one was
missed.

Underlying dist-shape issue is a separate SDK-wide bug worth tracking
(the source uses extensionless relative imports and tsup bundle:false
emits them verbatim, which breaks any strict-ESM consumer). Contained
here to unblock the sveltekit example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…[AIS-246]

The SvelteKit example was authored against a placeholder content model
(button/text/header + hi/hero templates), so nothing rendered against the
seeded landing Experience even though the SSR pipeline worked. Now it
mirrors the Next.js example 1:1 in component vocabulary.

Adds:
  - Section.svelte, Heading.svelte, RichText.svelte, Image.svelte
  - HeroPlain.svelte, Card.svelte (composed CTs)
  - design-tokens.ts (same 15 tokens as Next.js)

Rewrites (props changed to match the seeded content model):
  - Text.svelte      value → text
  - Button.svelte    text → label, adds target from design values
  - experience-config.ts fully rewritten to register the 8 CTs and page

Small tweak to Page.svelte so the max-width doesn't crush the multi-column
card layout.

Removes Header.svelte (superseded by Heading.svelte with the shape our
seeded ComponentType expects).

Verified: http://localhost:5173/landing renders the seeded Experience
end-to-end against f7tvosi12w6q/exo-sdk — same content as the Next.js
example, same three CTA URLs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rt [AIS-246]

The Page template was boxing everything into a 1024px max-width column,
which crushed the hero (its background never reached the edge) and left
awkward margins on the two-column card grid. SvelteKit's Page component
was already a passthrough, so the two examples looked different for no
good reason.

Strip Page down to a bare flex-column, matching the SvelteKit shape.
Each top-level node now handles its own containment: the hero renders
edge-to-edge, the cards Section applies horizontal padding via its own
design values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The existing `.nxignore` already lists `examples` to keep them out of
`nx run-many`, `nx graph`, `nx affected`, and `nx release`. test-apps
serve the same role (internal scratchpads, not part of the SDK
release surface) but weren't in the ignore list, so they got picked
up by generic Nx detection and CI tried to `next build`/`vite build`
them without the required env — the sveltekit one 401'd fetching from
the seeded space and failed the build.

Adding `test-apps` to `.nxignore` puts them back on the same footing
as `examples`: npm workspaces still link them, but Nx skips them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI's format:check step flagged 13 files added on this branch that weren't
prettier-formatted. `npm run format` applied the rewrites; behavior
unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rce CDN [AIS-246]

The bootstrap used to fetch three images from the shared demo test space's
public CDN at seed time. That worked, but tied the demo to a URL we don't
own — if those assets moved or the space got cleaned up, first-time
bootstrap runs would 404.

Ships the source bytes in-repo under examples/scripts/fixture/assets/
(hero-bg.png ~53 KB, card-on.png ~35 KB, card-guide.webp ~15 KB, ~103 KB
total). AssetFixture.sourceUrl → sourcePath; seedAsset reads the bytes
with readFileSync resolved relative to the script's own directory so the
demo works from any CWD.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@whitelisab
Lisa White (whitelisab) marked this pull request as ready for review July 21, 2026 16:58
@whitelisab
Lisa White (whitelisab) requested a review from a team as a code owner July 21, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant