Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b4b01af
chore: add test-apps/ scratchpad separate from customer-facing exampl…
whitelisab Jul 20, 2026
95aca6b
feat: add fixture defining minimal ExO demo Experience [AIS-246]
whitelisab Jul 20, 2026
c2e54b4
feat: bootstrap script provisions demo Experience via CMA dev build […
whitelisab Jul 20, 2026
32c2e96
docs: document the bootstrap-then-run flow across examples [AIS-246]
whitelisab Jul 20, 2026
3023303
fix: seed page template with componentTree + composed annotation [AIS…
whitelisab Jul 20, 2026
244c808
feat: register hero-plain + card in Next.js example [AIS-246]
whitelisab Jul 20, 2026
01a0451
fix: correct hero + On case study CTA URLs in demo fixture [AIS-246]
whitelisab Jul 20, 2026
dae3a8d
feat: register hero-plain + card in advanced Next.js config [AIS-246]
whitelisab Jul 20, 2026
81f8560
fix: point example landing pages at the seeded `landing` experience […
whitelisab Jul 20, 2026
a02c3f8
feat: use CPA token for preview mode in example apps [AIS-246]
whitelisab Jul 20, 2026
3308fbf
docs: split basic vs advanced/preview instructions in example READMEs…
whitelisab Jul 20, 2026
f32a574
fix(sveltekit-example): add experiences-client to ssr.noExternal [AIS…
whitelisab Jul 20, 2026
34173cd
feat(sveltekit-example): port Next.js components + config for parity …
whitelisab Jul 20, 2026
89fa79d
style: make Next.js Page template full-bleed so hero spans the viewpo…
whitelisab Jul 20, 2026
5d6fbd8
fix(ci): exclude test-apps from Nx project graph via .nxignore [AIS-246]
whitelisab Jul 20, 2026
8fa2bf8
style: run prettier --write on new example files [AIS-246]
whitelisab Jul 20, 2026
614ca72
refactor: bundle demo assets locally instead of fetching from the sou…
whitelisab Jul 21, 2026
442ddd2
chore: update docs and comments
whitelisab Jul 21, 2026
924a8c2
chore: run prettier
whitelisab Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .nxignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Examples are local testbeds for the SDK packages, not part of the
# Nx project graph. They stay linked via npm workspaces (so they consume
# the local SDK packages by symlink) but Nx commands like `nx run-many`,
# `nx graph`, `nx affected`, and `nx release` ignore them entirely.
# Examples and test-apps are local testbeds for the SDK packages, not part
# of the Nx project graph. They stay linked via npm workspaces (so they
# consume the local SDK packages by symlink) but Nx commands like
# `nx run-many`, `nx graph`, `nx affected`, and `nx release` ignore them
# entirely.
examples
test-apps
25 changes: 19 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ experiences/
│ ├── client/ # @contentful/experiences-client (internal)
│ ├── adapter-react/ # @contentful/experiences-react (customer-facing)
│ └── adapter-svelte/ # @contentful/experiences-svelte (customer-facing)
└── examples/
├── nextjs/ # Next.js 15 example app
└── sveltekit/ # SvelteKit 2 example app (1:1 parity with nextjs)
├── examples/ # Customer-facing example apps
│ ├── nextjs/ # Next.js 15 example (external developers run this)
│ └── sveltekit/ # SvelteKit 2 example (1:1 parity with nextjs)
└── test-apps/ # Internal testing
├── nextjs/ # Next.js scratchpad
└── sveltekit/ # SvelteKit scratchpad
```

**`examples/` vs `test-apps/`.** `examples/` is the stable, external-facing surface — every commit to it should keep the customer-facing "clone + bootstrap + run" flow working. `test-apps/` is where you experiment: try new component patterns, break things, prototype features. Don't reach into `examples/` when you just need a place to poke — copy your changes into `test-apps/` first, iterate there, then port back deliberately.

### Package roles

| Folder | npm name | Audience |
Expand Down Expand Up @@ -357,11 +362,19 @@ npx nx run-many -t build --skip-nx-cache
### Run the example app

```sh
cd examples/nextjs
cp .env.example .env.local # fill in SPACE_ID + CDA_TOKEN
npm run dev # http://localhost:3000/<experience-id>
# 1. Seed the demo Experience into your target space (one-time).
cd examples/scripts
cp .env.example .env # fill in SPACE_ID, ENVIRONMENT_ID, CMA_TOKEN
npm run bootstrap # prints experienceId (default: `landing`)

# 2. Run the app.
cd ../nextjs
cp .env.example .env.local # fill in SPACE_ID, ENVIRONMENT_ID, CDA_TOKEN
npm run dev # http://localhost:3000/landing
```

The bootstrap script (`examples/scripts/bootstrap-example.ts`) provisions everything the demo Experience references — ContentType, entries, assets, design tokens, ComponentTypes, Template, DataAssemblies, Experience — via the experiences management API (currently `contentful-management@12.6.0-dev.4`). Idempotent per resource; safe to re-run against a half-seeded env. See `examples/scripts/README.md` for details.

### Add a new framework adapter

`packages/adapter-svelte` is the canonical example of "framework that isn't React" — copy from there for non-React frameworks (different build tool, peer dep, etc.); copy from `packages/adapter-react` for "framework like React" (JSX-ish + tsup).
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,16 +335,26 @@ Runnable apps for both frameworks live in [`examples/`](./examples). They regist
| [`examples/nextjs`](./examples/nextjs) | Next.js 15 (App Router) | Simple + advanced routes (preview, UA→viewport, async `resolveData`), design tokens, styling hooks |
| [`examples/sveltekit`](./examples/sveltekit) | SvelteKit 2 + Svelte 5 | 1:1 parity with the Next.js app; hydration-safe viewport seeding via `+page.server.ts` |

Both examples render the same demo Experience. To run them you first seed that Experience into your Contentful space with the one-time bootstrap script — the script uses the experiences management API to provision the ContentType, entries, assets, design tokens, ComponentTypes, template, DataAssemblies, and the Experience itself.

```sh
npm install --ignore-scripts
npm run build # build the SDK packages
npm run build # build the SDK packages

# 1. Seed the demo Experience into your Contentful space (one-time).
cd examples/scripts
cp .env.example .env # fill in SPACE_ID, ENVIRONMENT_ID, CMA_TOKEN
npm run bootstrap # prints the experienceId at the end (default: `landing`)

cd examples/nextjs # or examples/sveltekit
cp .env.example .env.local # sveltekit uses .env; fill in SPACE_ID + CDA_TOKEN
# 2. Run one of the example apps against the seeded space.
cd ../nextjs # or ../sveltekit
cp .env.example .env.local # sveltekit uses .env; fill in SPACE_ID, ENVIRONMENT_ID, CDA_TOKEN
npm run dev
```

Then visit `/<experience-id>`. See each example's README for its file map and route-by-route walkthrough.
Then visit `/landing` (or whichever experienceId the bootstrap printed). See each example's README for its file map and route-by-route walkthrough.

**Tokens.** `CMA_TOKEN` is a Personal Access Token that only the bootstrap script sees. `CDA_TOKEN` is a Content Delivery API token — this is what the running app uses at runtime. `CPA_TOKEN` is a Content Preview API token, only needed if you want to exercise `?preview=true` routes; see each example's README for details.

---

Expand Down
11 changes: 11 additions & 0 deletions examples/nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Copy this file to .env.local and fill in real values from a Contentful space.
#
# Bootstrap the demo Experience into your space first with:
# cd examples/scripts && npm run bootstrap
# The script prints the experienceId to hit at the end (default: `landing`).

SPACE_ID=
ENVIRONMENT_ID=master

# Content Delivery API token — used by every route by default.
CDA_TOKEN=

# Content Preview API token — required only for the /advanced route with
# ?preview=true (that path hits preview.xdn.contentful.com, which rejects
# CDA tokens). Leave blank if you don't need preview mode.
CPA_TOKEN=
63 changes: 50 additions & 13 deletions examples/nextjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,67 @@ A Next.js 15 App Router app demonstrating `@contentful/experiences-react` render

## Run it

The example is a real integration against Contentful, not a mock. You need:

1. **A Contentful space** with the demo content model + Experience seeded into it (a one-time step below), and
2. **Tokens** for the paths you want to hit — different Contentful APIs use different tokens.

The [`examples/scripts/bootstrap-example.ts`](../scripts/bootstrap-example.ts) script does the seeding via the management API. See [`examples/scripts/README.md`](../scripts/README.md) for what it provisions.

### 1. Seed the demo Experience (one-time)

```sh
# From the repo root:
npm install --ignore-scripts
npm run build # builds the SDK packages
npm install
npm run build # build the SDK packages

cd examples/scripts
cp .env.example .env # fill in SPACE_ID, ENVIRONMENT_ID, CMA_TOKEN
npm run bootstrap # prints the experienceId at the end (default: `landing`)
```

cd examples/nextjs
cp .env.example .env.local # fill in SPACE_ID + CDA_TOKEN
### 2a. Run the basic route (`/landing`)

```sh
cd ../nextjs
cp .env.example .env.local # fill in SPACE_ID, ENVIRONMENT_ID, CDA_TOKEN
npm run dev
```

Then visit `http://localhost:3000/<experience-id>`. The slug becomes the Experience ID passed to `client.view.getExperience`.
Visit `http://localhost:3000/landing`. This route is the minimal three-line integration described below — `fetchExperience` → `<ServerExperienceRenderer>`. It reads from the Content Delivery API using `CDA_TOKEN`.

### 2b. Run the advanced route (`/advanced/landing?preview=true`)

The advanced route can be reached at `http://localhost:3000/advanced/landing` (no query params) using the same `CDA_TOKEN` — you'll get the enrichment + viewport-seeding demo.

To exercise **preview mode** as well (the "Advanced demo (preview)" button on the index page), you also need a **Content Preview API token** — preview requests hit `preview.xdn.contentful.com`.

Add it to `.env.local`:

```
CPA_TOKEN=... # Content Preview API token, from Settings → API keys in your space
```

Then visit `http://localhost:3000/advanced/landing?preview=true&locale=en-US`.

### Tokens summary

| Token | API | Used by | Required? |
| ----------- | ------------------ | ------------------------------------------------ | -------------------------------------- |
| `CMA_TOKEN` | Content Management | The bootstrap script (one-time seed) | Yes, to run bootstrap |
| `CDA_TOKEN` | Content Delivery | The example app for `/landing` and `/advanced/*` | Yes, to run the app |
| `CPA_TOKEN` | Content Preview | The example app when `?preview=true` | Only for preview mode on `/advanced/*` |

## Two routes, same data

The example ships two side-by-side routes so you can see what each SDK option gives you. They render the same Experience id; only the SDK setup changes.

| Route | Page | Config | Demonstrates |
| ------------------ | ---------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `/[slug]` | [`app/[slug]/page.tsx`](./app/[slug]/page.tsx) | `experience-config.tsx` | The minimum: `fetchExperience` into `<ServerExperienceRenderer>` 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`, User-Agent to `initialViewportId`, async `resolveData` with external fetch. |
| Route | Try it locally | Config | Demonstrates |
| ------------------ | ------------------------------------------------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `/[slug]` | `http://localhost:3000/landing` | `experience-config.tsx` | The minimum: `fetchExperience` into `<ServerExperienceRenderer>` with `NotFoundError` routed to Next's `notFound()`. |
| `/advanced/[slug]` | `http://localhost:3000/advanced/landing?preview=true&locale=en-US` | `experience-config-advanced.tsx` | Preview mode via `?preview=true` (needs `CPA_TOKEN`), User-Agent to `initialViewportId`, async `resolveData`. |

Source: [`app/[slug]/page.tsx`](./app/[slug]/page.tsx), [`app/advanced/[slug]/page.tsx`](./app/advanced/[slug]/page.tsx).

The minimal `[slug]/page.tsx`:

Expand Down Expand Up @@ -227,7 +268,3 @@ export const experienceConfig: Config = { components, templates };
If the payload references a template id that isn't registered, the renderer
warns once and renders the nodes unwrapped, the same graceful-degradation
behavior as missing components.

## Where the live preview / editor support fits

Live preview (postMessage from the Contentful editor iframe) lands in a separate increment with a client-component wrapper that uses `ClientExperienceRenderer` and a `useMessagingClient`-style hook. SSR and interactive editor mode are mutually exclusive: the editor mode requires `'use client'` so the message listener can attach.
7 changes: 6 additions & 1 deletion examples/nextjs/app/advanced/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ export default async function AdvancedExperiencePage({ params, searchParams }: P
locale,
},
{
accessToken: process.env.CDA_TOKEN!,
// Preview mode reads from the CPA endpoint, which needs a Content
// Preview token — the CDA token is rejected by that host. If preview
// mode is on but CPA_TOKEN is unset, we still call preview but the
// request will 401; document this in .env.example.
accessToken:
previewMode && process.env.CPA_TOKEN ? process.env.CPA_TOKEN : process.env.CDA_TOKEN!,
host: previewMode ? 'https://preview.xdn.contentful.com' : 'https://xdn.contentful.com',
},
{
Expand Down
7 changes: 4 additions & 3 deletions examples/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ export default function HomePage() {
</ul>

<p style={{ display: 'flex', gap: 12, marginTop: 24 }}>
<Link href="/demo" style={linkStyle}>
<Link href="/landing" style={linkStyle}>
Simple demo
</Link>
<Link href="/advanced/demo?preview=true&locale=en-US" style={secondaryLinkStyle}>
<Link href="/advanced/landing?preview=true&locale=en-US" style={secondaryLinkStyle}>
Advanced demo (preview)
</Link>
</p>

<p style={{ color: '#9ca3af', fontSize: 13, marginTop: 24, marginBottom: 0 }}>
Replace <code>demo</code> in either URL with a real Experience id from your space.
<code>landing</code> is the id the bootstrap script (<code>examples/scripts</code>) seeds by
default. Replace it in the URL with any other Experience id from your space.
</p>
</main>
);
Expand Down
70 changes: 70 additions & 0 deletions examples/nextjs/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import type { CSSProperties } from 'react';

import { toCss, useDesignValues } from '@contentful/experiences-react';

export interface CardProps {
title?: string;
teaser?: string;
ctaLabel?: string;
ctaUrl?: string;
image?: string;
}

/**
* Compact card: image + title + teaser + CTA. Content properties come from
* a `Card from Promotion` DataAssembly binding.
*/
export function Card({ title, teaser, ctaLabel, ctaUrl, image }: CardProps) {
const design = useDesignValues<{
backgroundColor?: string;
color?: string;
}>();

const style: CSSProperties = {
display: 'flex',
flexDirection: 'column',
borderRadius: '0.5rem',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
...toCss(design),
};

return (
<article style={style}>
{image && (
<img src={image} alt="" style={{ width: '100%', height: '180px', objectFit: 'cover' }} />
)}
<div
style={{
padding: '1rem 1.25rem',
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
flex: 1,
}}
>
{title && <h3 style={{ margin: 0, fontSize: '1.25rem' }}>{title}</h3>}
{teaser && <p style={{ margin: 0, lineHeight: 1.5 }}>{teaser}</p>}
{ctaLabel && ctaUrl && (
<a
href={ctaUrl}
style={{
marginTop: 'auto',
display: 'inline-block',
padding: '0.5rem 1rem',
background: '#111',
color: '#fff',
textDecoration: 'none',
borderRadius: '0.25rem',
alignSelf: 'flex-start',
}}
>
{ctaLabel}
</a>
)}
</div>
</article>
);
}
65 changes: 65 additions & 0 deletions examples/nextjs/components/HeroPlain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import type { CSSProperties } from 'react';

import { toCss, useDesignValues } from '@contentful/experiences-react';

export interface HeroPlainProps {
title?: string;
body?: unknown;
ctaLabel?: string;
ctaUrl?: string;
image?: string;
}

/**
* Composed hero: title + optional body + CTA + hero image. All content
* properties come from a `Hero from Promotion` DataAssembly binding — this
* component just lays them out.
*/
export function HeroPlain({ title, ctaLabel, ctaUrl, image }: HeroPlainProps) {
const design = useDesignValues<{
backgroundColor?: string;
color?: string;
}>();

const style: CSSProperties = {
display: 'grid',
gridTemplateColumns: image ? '1fr 1fr' : '1fr',
alignItems: 'center',
gap: '2rem',
padding: '4rem 2rem',
...toCss(design),
};

return (
<section style={style}>
<div>
{title && <h1 style={{ margin: 0, fontSize: '2.5rem' }}>{title}</h1>}
{ctaLabel && ctaUrl && (
<a
href={ctaUrl}
style={{
display: 'inline-block',
marginTop: '1.5rem',
padding: '0.75rem 1.5rem',
background: '#111',
color: '#fff',
textDecoration: 'none',
borderRadius: '0.25rem',
}}
>
{ctaLabel}
</a>
)}
</div>
{image && (
<img
src={image}
alt=""
style={{ maxWidth: '100%', height: 'auto', borderRadius: '0.5rem' }}
/>
)}
</section>
);
}
6 changes: 1 addition & 5 deletions examples/nextjs/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ export interface PageProps {
children?: ReactNode;
}

/** Page-level template: wraps all top-level nodes in the outer page chrome. */
export function Page({ children }: PageProps) {
const wrapper: CSSProperties = {
maxWidth: 1024,
margin: '0 auto',
padding: '48px 24px',
display: 'flex',
flexDirection: 'column',
gap: 48,
gap: 16,
};
return <main style={wrapper}>{children}</main>;
}
2 changes: 2 additions & 0 deletions examples/nextjs/lib/design-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const designTokens: Record<string, string> = {
'color.none': 'transparent', // resolves "no background" to a real value
'color.white': '#ffffff',
'color.text': '#1f2937',
'color.primary': '#0f172a', // hero background
'color.primaryText': '#f8fafc', // hero foreground

'fontSize.sm': '14px',
'fontSize.md': '16px',
Expand Down
Loading