@contentful/experiences-svelte 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.5.6 (2026-08-12)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **core,adapters:** render Experience Templates as ordinary nodes [AIS-413] ([#133](https://github.com/contentful/experiences/pull/133))
6
+
7
+ ### 🧱 Updated Dependencies
8
+
9
+ - Updated client to 0.3.5
10
+ - Updated design to 0.7.5
11
+ - Updated core to 0.7.5
12
+
1
13
  ## 0.5.5 (2026-08-11)
2
14
 
3
15
  ### 🚀 Features
package/README.md CHANGED
@@ -20,7 +20,7 @@ The public API mirrors `@contentful/experiences-react` 1:1 in shape; only the re
20
20
 
21
21
  ```ts
22
22
  defineComponent<Props>(config); // Type-narrowing identity for component-type configs
23
- defineExperienceTemplate<Props>(config); // Same shape, for page-level Experience Template wrappers
23
+ defineExperienceTemplate<Props>(config); // Same shape, for coded Experience Template configs
24
24
  ```
25
25
 
26
26
  ### Resolver
@@ -35,7 +35,7 @@ resolveExperience(payload, config, opts?); // Async; walks payload, runs resolve
35
35
  ServerExperienceRenderer; // SSR-safe; active viewport seeded from initialViewportId
36
36
  ClientExperienceRenderer; // Subscribes to window.matchMedia via runes
37
37
  MissingComponent; // Default fallback for unregistered component types
38
- NodesRenderer; // Exposed so you can render non-`children` slots manually (see Slot children)
38
+ NodesRenderer; // Exposed so you can re-render a slot's raw nodes yourself (see Slot children)
39
39
  useActiveViewport; // Rune-backed reactive object; you'll rarely need it directly
40
40
  ```
41
41
 
@@ -46,7 +46,7 @@ getDesignValues<T>(); // Optional helper: the same resolved design record that a
46
46
  toCss(design, options?); // Turns a design record into a plain style object, keeping only real CSS keys
47
47
  getExperience(); // RenderContext: debug, metadata, viewports, activeViewport
48
48
  getContentfulComponent(); // Raw payload for the enclosing node (or undefined)
49
- getContentfulExperienceTemplate(); // Same, for the page-level Experience Template
49
+ getContentfulExperienceTemplate(); // Same, for an enclosing coded Experience Template node
50
50
  type ToCssOptions;
51
51
  ```
52
52
 
@@ -61,7 +61,7 @@ type ComponentConfig, ExperienceTemplateConfig,
61
61
  type ContentfulComponent, ContentfulExperienceTemplate,
62
62
  type RenderContext, ResolveToken,
63
63
  type ExperiencePayload, ExperienceNode, ComponentNode, ExperienceTemplateNode,
64
- type PortableRenderPlan, PortableRenderNode, PortableExperienceTemplate,
64
+ type PortableRenderPlan, PortableRenderNode, PortableRegistration,
65
65
  type DesignPropValue, ManualDesignValue, DesignToken, ValuesByViewport,
66
66
  type ViewportDef, ExperienceContext, ResolveContext,
67
67
  type ResolverConfig, ResolveExperienceOptions
@@ -129,7 +129,7 @@ export const experienceConfig: Config = { components, resolveToken };
129
129
 
130
130
  ### Slot children
131
131
 
132
- A component's default slot children arrive as an **array of Snippets** (`Snippet[]`), one per child — not a single wrapping Snippet. For the common "just render them" case, iterate the array with `{#each}` and `{@render}`; to wrap, reorder, or drop children individually, do it inside that loop.
132
+ Every slot arrives as a prop named after the slot, holding an **array of Snippets** (`Snippet[]`), one per child — not a single wrapping Snippet. For the common "just render them" case, iterate the array with `{#each}` and `{@render}`; to wrap, reorder, or drop children individually, do it inside that loop.
133
133
 
134
134
  ```svelte
135
135
  <!-- Section.svelte -->
@@ -156,7 +156,9 @@ A component's default slot children arrive as an **array of Snippets** (`Snippet
156
156
  -->
157
157
  ```
158
158
 
159
- The default slot is injected as the `children` prop. Any **additional named slots** are not auto-injected — reach them from the raw payload via `getContentfulComponent().slots` (a `Record<string, PortableRenderNode[]>`) and render them yourself with `NodesRenderer`.
159
+ `children` is not special — it is simply the conventional name for the default slot. **Every** slot in the payload becomes a same-named `Snippet[]` prop, so a component with a `header` slot just declares `header?: Snippet[]` and renders it the same way. This applies identically to coded Experience Templates: a template with a `content` slot receives a `content` prop.
160
+
161
+ If you'd rather render a slot's raw nodes yourself, they are still on the payload at `getContentfulComponent().slots` (a `Record<string, PortableRenderNode[]>`) — hand them to `NodesRenderer`.
160
162
 
161
163
  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).
162
164
 
@@ -14,7 +14,6 @@
14
14
  import DebugExperience from './DebugExperience.svelte';
15
15
  import MissingComponent from './MissingComponent.svelte';
16
16
  import NodesRenderer from './NodesRenderer.svelte';
17
- import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
18
17
  import type { ClientExperienceRendererProps } from './component-props.js';
19
18
  import { setExperience } from './context.js';
20
19
  import type { RenderContext } from './types.js';
@@ -75,14 +74,10 @@
75
74
  {#if debug}
76
75
  <DebugExperience {experience} />
77
76
  {/if}
78
- <WrapWithExperienceTemplate experienceTemplate={experience.experienceTemplate} {config} experience={liveContext}>
79
- {#snippet children()}
80
- <NodesRenderer
81
- nodes={experience.nodes}
82
- {config}
83
- experience={liveContext}
84
- {renderUnknown}
85
- />
86
- {/snippet}
87
- </WrapWithExperienceTemplate>
77
+ <NodesRenderer
78
+ nodes={experience.nodes}
79
+ {config}
80
+ experience={liveContext}
81
+ {renderUnknown}
82
+ />
88
83
  {/if}
@@ -1 +1 @@
1
- {"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"AA2FA,QAAA,MAAM,wBAAwB,kGAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
1
+ {"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"AAqFA,QAAA,MAAM,wBAAwB,kGAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
@@ -17,10 +17,17 @@
17
17
  let { experience, defaultOpen = false }: DebugExperienceProps = $props();
18
18
 
19
19
  const nodeCount = $derived(experience.nodes.length);
20
+ // Coded Experience Templates are ordinary top-level nodes, so name them from
21
+ // the node list — a composite Experience simply has none to report.
22
+ const templateIds = $derived(
23
+ experience.nodes
24
+ .filter((node) => node.registration.kind === 'experienceTemplate')
25
+ .map((node) => node.registration.id)
26
+ );
20
27
  const summary = $derived(
21
28
  `Experience debug — ${nodeCount} top-level node${nodeCount === 1 ? '' : 's'}${
22
- experience.experienceTemplate
23
- ? `, experience template: ${experience.experienceTemplate.experienceTemplateId}`
29
+ templateIds.length
30
+ ? `, experience template${templateIds.length === 1 ? '' : 's'}: ${templateIds.join(', ')}`
24
31
  : ''
25
32
  }`
26
33
  );
@@ -1 +1 @@
1
- {"version":3,"file":"DebugExperience.svelte.d.ts","sourceRoot":"","sources":["../src/DebugExperience.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAqDjE,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"DebugExperience.svelte.d.ts","sourceRoot":"","sources":["../src/DebugExperience.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AA4DjE,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -2,13 +2,20 @@ import { render } from '@testing-library/svelte';
2
2
  import { describe, expect, it } from 'vitest';
3
3
  import DebugExperience from './DebugExperience.svelte';
4
4
  const emptyPlan = { nodes: [], viewports: [], fallbackViewportIndex: 0 };
5
- function node(componentId, content = {}) {
5
+ function node(id, content = {}) {
6
6
  return {
7
- registration: { componentId },
7
+ registration: { kind: 'component', id },
8
8
  props: { content, design: {}, designRaw: {} },
9
9
  slots: {},
10
10
  };
11
11
  }
12
+ function templateNode(id) {
13
+ return {
14
+ registration: { kind: 'experienceTemplate', id },
15
+ props: { content: {}, design: {}, designRaw: {} },
16
+ slots: {},
17
+ };
18
+ }
12
19
  describe('DebugExperience.svelte', () => {
13
20
  it('renders a collapsible details panel with the debug marker, collapsed by default', () => {
14
21
  const { container } = render(DebugExperience, { props: { experience: emptyPlan } });
@@ -33,19 +40,25 @@ describe('DebugExperience.svelte', () => {
33
40
  const { container } = render(DebugExperience, { props: { experience: one } });
34
41
  expect(container.innerHTML).toContain('Experience debug — 1 top-level node');
35
42
  });
36
- it('names the experienceTemplate in the summary when present', () => {
43
+ it('names experienceTemplate nodes in the summary', () => {
37
44
  const plan = {
38
45
  viewports: [],
39
- nodes: [],
40
- experienceTemplate: {
41
- experienceTemplateId: 'page',
42
- props: { content: {}, design: {}, designRaw: {} },
43
- },
46
+ nodes: [templateNode('page')],
44
47
  fallbackViewportIndex: 0,
45
48
  };
46
49
  const { container } = render(DebugExperience, { props: { experience: plan } });
47
50
  expect(container.innerHTML).toContain('experience template: page');
48
51
  });
52
+ it('omits the experience-template summary for a composite experience', () => {
53
+ const plan = {
54
+ viewports: [],
55
+ nodes: [node('button'), node('text')],
56
+ fallbackViewportIndex: 0,
57
+ };
58
+ const { container } = render(DebugExperience, { props: { experience: plan } });
59
+ expect(container.innerHTML).toContain('Experience debug — 2 top-level nodes');
60
+ expect(container.innerHTML).not.toContain('experience template');
61
+ });
49
62
  it('dumps the plan as pretty JSON', () => {
50
63
  const plan = {
51
64
  viewports: [],
@@ -53,7 +66,7 @@ describe('DebugExperience.svelte', () => {
53
66
  fallbackViewportIndex: 0,
54
67
  };
55
68
  const { container } = render(DebugExperience, { props: { experience: plan } });
56
- expect(container.innerHTML).toContain('componentId');
69
+ expect(container.innerHTML).toContain('registration');
57
70
  expect(container.innerHTML).toContain('button');
58
71
  expect(container.innerHTML).toContain('Go');
59
72
  });
@@ -2,18 +2,29 @@
2
2
  * Single-node renderer. Splits out of NodesRenderer so the per-node
3
3
  * `setContext` for the contentful payload happens during *that node's*
4
4
  * component init (Svelte's setContext can only be called at top level).
5
+ *
6
+ * A node's `registration.kind` picks which half of the customer Config owns
7
+ * its implementation — `experienceTemplates` for a coded Experience Template,
8
+ * `components` otherwise. Everything else (slot props, merge precedence,
9
+ * design resolution) is identical across the two kinds.
5
10
  -->
6
11
  <script lang="ts">
7
12
  import type { Snippet } from 'svelte';
8
13
 
9
14
  import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
10
15
 
11
- import { setContentfulComponent, setResolvedDesign } from './context.js';
16
+ import {
17
+ setContentfulComponent,
18
+ setContentfulExperienceTemplate,
19
+ setResolvedDesign,
20
+ } from './context.js';
12
21
  import { selectResolvedDesign } from './design-utils.js';
13
22
  import type { RenderUnknown } from './component-props.js';
14
23
  import {
15
24
  normalizeComponentRegistration,
25
+ normalizeExperienceTemplateRegistration,
16
26
  type ContentfulComponent,
27
+ type ContentfulExperienceTemplate,
17
28
  type Config,
18
29
  type RenderContext,
19
30
  } from './types.js';
@@ -23,23 +34,49 @@
23
34
  config: Config;
24
35
  experience: RenderContext;
25
36
  renderUnknown: RenderUnknown;
26
- children: Snippet[];
37
+ /**
38
+ * Pre-rendered slot children keyed by slot name — one zero-arg Snippet per
39
+ * child node. Spread onto the customer component as named props, so a slot
40
+ * named `content` arrives as a `content` prop. `children` is not special.
41
+ */
42
+ slotSnippets: Record<string, Snippet[]>;
27
43
  }
28
44
 
29
- let { node, config, experience, renderUnknown, children }: NodeRendererProps = $props();
45
+ let { node, config, experience, renderUnknown, slotSnippets }: NodeRendererProps = $props();
46
+
47
+ const { kind, id } = node.registration;
48
+ const isExperienceTemplate = kind === 'experienceTemplate';
30
49
 
31
- const entry = $derived(config.components[node.registration.componentId]);
32
- const componentConfig = $derived(entry ? normalizeComponentRegistration(entry) : null);
50
+ const entry = $derived(
51
+ isExperienceTemplate ? config.experienceTemplates?.[id] : config.components[id]
52
+ );
53
+ const componentConfig = $derived.by(() => {
54
+ if (!entry) return null;
55
+ return isExperienceTemplate
56
+ ? normalizeExperienceTemplateRegistration(entry)
57
+ : normalizeComponentRegistration(entry);
58
+ });
33
59
 
34
- const contentful: ContentfulComponent = {
35
- componentId: node.registration.componentId,
36
- nodeId: node.nodeId,
37
- content: node.props.content,
38
- design: node.props.designRaw,
39
- resolved: node.props.resolved,
40
- slots: node.slots,
41
- };
42
- setContentfulComponent(contentful);
60
+ if (isExperienceTemplate) {
61
+ const contentful: ContentfulExperienceTemplate = {
62
+ experienceTemplateId: id,
63
+ nodeId: node.nodeId,
64
+ content: node.props.content,
65
+ design: node.props.designRaw,
66
+ resolved: node.props.resolved,
67
+ };
68
+ setContentfulExperienceTemplate(contentful);
69
+ } else {
70
+ const contentful: ContentfulComponent = {
71
+ componentId: id,
72
+ nodeId: node.nodeId,
73
+ content: node.props.content,
74
+ design: node.props.designRaw,
75
+ resolved: node.props.resolved,
76
+ slots: node.slots,
77
+ };
78
+ setContentfulComponent(contentful);
79
+ }
43
80
 
44
81
  const tokenResolvedDesign = $derived.by(() => {
45
82
  const { props, unresolved } = selectResolvedDesign(
@@ -51,7 +88,7 @@
51
88
  );
52
89
  if (unresolved.length && typeof console !== 'undefined') {
53
90
  console.warn(
54
- `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on "${node.registration.componentId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
91
+ `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on ${kind} "${id}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
55
92
  );
56
93
  }
57
94
  return props;
@@ -59,7 +96,7 @@
59
96
 
60
97
  setResolvedDesign(() => tokenResolvedDesign);
61
98
 
62
- // Merge precedence (last wins): defaults < design < content < resolveData < children.
99
+ // Merge precedence (last wins): defaults < design < content < resolveData < slots.
63
100
  const composed = $derived.by(() => {
64
101
  if (!componentConfig) return null;
65
102
  return {
@@ -67,18 +104,35 @@
67
104
  ...tokenResolvedDesign,
68
105
  ...node.props.content,
69
106
  ...node.props.resolved,
70
- children,
107
+ ...slotSnippets,
71
108
  };
72
109
  });
110
+
111
+ // An unregistered Experience Template would blank the page if we swapped it
112
+ // for the missing-component box, so warn and render its slot children
113
+ // unwrapped — the content survives, the diagnostic names what's missing.
114
+ const orphanedSnippets = $derived.by(() => {
115
+ if (componentConfig || !isExperienceTemplate) return null;
116
+ if (typeof console !== 'undefined') {
117
+ console.warn(
118
+ `[@contentful/experiences-svelte] No experience template registered for id "${id}". Rendering its slot children without the experience template wrapper.`
119
+ );
120
+ }
121
+ return Object.values(slotSnippets).flat();
122
+ });
73
123
  </script>
74
124
 
75
125
  {#if componentConfig && composed}
76
126
  {@const Cmp = componentConfig.component}
77
127
  <Cmp {...composed} />
128
+ {:else if orphanedSnippets}
129
+ {#each orphanedSnippets as childSnippet, index (index)}
130
+ {@render childSnippet()}
131
+ {/each}
78
132
  {:else}
79
133
  {@const Unknown = renderUnknown}
80
134
  <Unknown
81
- componentId={node.registration.componentId}
135
+ componentId={id}
82
136
  nodeId={node.nodeId}
83
137
  />
84
138
  {/if}
@@ -7,7 +7,12 @@ interface NodeRendererProps {
7
7
  config: Config;
8
8
  experience: RenderContext;
9
9
  renderUnknown: RenderUnknown;
10
- children: Snippet[];
10
+ /**
11
+ * Pre-rendered slot children keyed by slot name — one zero-arg Snippet per
12
+ * child node. Spread onto the customer component as named props, so a slot
13
+ * named `content` arrives as a `content` prop. `children` is not special.
14
+ */
15
+ slotSnippets: Record<string, Snippet[]>;
11
16
  }
12
17
  declare const NodeRenderer: import("svelte").Component<NodeRendererProps, {}, "">;
13
18
  type NodeRenderer = ReturnType<typeof NodeRenderer>;
@@ -1 +1 @@
1
- {"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAqEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAQ3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAKH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;CACzC;AA2GH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,17 +1,18 @@
1
1
  <!--
2
2
  * Recursive renderer over PortableRenderNodes. Customer components receive
3
- * the merged content / design / resolveData prop bag plus the default
4
- * `children` slot as an ARRAY of Snippets — one zero-arg Snippet per
5
- * pre-rendered child node — declared as `children?: Snippet[]`. A component
3
+ * the merged content / design / resolveData prop bag plus one prop per slot,
4
+ * each an ARRAY of Snippets — one zero-arg Snippet per pre-rendered child
5
+ * node. A slot named `content` arrives as `content?: Snippet[]`; the
6
+ * conventional default slot arrives as `children?: Snippet[]`. A component
6
7
  * renders them all with an each-block + @render, or maps/filters/wraps the
7
- * children individually. The array mirrors the React adapter's ReactNode[]
8
+ * children individually. The arrays mirror the React adapter's ReactNode[]
8
9
  * slot contract; see the README for the consuming-a-slot example.
9
10
  *
10
- * Only the default `children` slot is injected as a prop — Svelte 5 Snippets
11
- * are compile-time entities, so the renderer can't synthesize an unbounded
12
- * set of named Snippet props from runtime payload data. Additional named
13
- * slots are reachable through `getContentfulComponent().slots` and can be
14
- * rendered with the exported NodesRenderer (see README).
11
+ * This applies to every node kind — a coded Experience Template declaring a
12
+ * `content` slot gets a `content` prop exactly as a component would. The raw
13
+ * per-slot payload nodes are also reachable via
14
+ * `getContentfulComponent().slots` for callers that want to render them
15
+ * themselves with the exported NodesRenderer.
15
16
  *
16
17
  * The Experience runtime context and the raw Contentful payload are read
17
18
  * via `getExperience` / `getContentfulComponent`.
@@ -38,20 +39,42 @@
38
39
  let { nodes, config, experience, renderUnknown }: NodesRendererProps = $props();
39
40
 
40
41
  // Curry the parameterized `renderNode` snippet into a zero-arg Snippet bound
41
- // to one node. Svelte 5 invokes a snippet as `(anchor, ...argGetters)`, so we
42
- // forward the anchor and supply the node through a getter. This is the one
43
- // spot that leans on Svelte's snippet calling convention; pinned by the
44
- // `children`-array tests in server-renderer.test.ts.
42
+ // to one node: forward the anchor (the renderer, under SSR) and supply the
43
+ // node through a getter. This is the one spot that leans on Svelte's snippet
44
+ // calling convention; pinned by the slot-array tests in
45
+ // server-renderer.test.ts and nodes-renderer.ssr.test.ts.
45
46
  function toChildSnippet(node: PortableRenderNode): Snippet {
46
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- programmatic snippet construction; see note above.
47
48
  return ((anchor: unknown) => (renderNode as any)(anchor, () => node)) as Snippet;
48
49
  }
50
+
51
+ // Svelte's two compiler outputs disagree on how snippet arguments arrive: the
52
+ // client build passes each one as a getter, the server build passes it by
53
+ // value. Compiler-generated `{@render}` call sites match whichever build they
54
+ // are in, but `toChildSnippet` writes its call by hand, so the snippet body
55
+ // accepts either shape and normalizes here.
56
+ type NodeArg = PortableRenderNode | (() => PortableRenderNode);
57
+ function unwrapNode(node: NodeArg): PortableRenderNode {
58
+ return typeof node === 'function' ? node() : node;
59
+ }
60
+
61
+ // Build one Snippet[] per slot so every slot becomes a named prop. Snippets
62
+ // themselves are compile-time entities, but the *prop bag* is plain runtime
63
+ // data — so an unbounded set of payload-driven slot names is fine as long as
64
+ // each snippet comes from `toChildSnippet`.
65
+ function toSlotSnippets(node: PortableRenderNode): Record<string, Snippet[]> {
66
+ const slotSnippets: Record<string, Snippet[]> = {};
67
+ for (const [slotName, children] of Object.entries(node.slots)) {
68
+ slotSnippets[slotName] = children.map(toChildSnippet);
69
+ }
70
+ return slotSnippets;
71
+ }
49
72
  </script>
50
73
 
51
- {#snippet renderNode(node: PortableRenderNode)}
52
- {@const childNodes = (node.slots.children ?? []) as PortableRenderNode[]}
53
- {@const children = childNodes.map(toChildSnippet)}
54
- <NodeRenderer {node} {config} {experience} {renderUnknown} {children} />
74
+ {#snippet renderNode(nodeArg: NodeArg)}
75
+ {@const node = unwrapNode(nodeArg)}
76
+ {@const slotSnippets = toSlotSnippets(node)}
77
+ <NodeRenderer {node} {config} {experience} {renderUnknown} {slotSnippets} />
55
78
  {/snippet}
56
79
 
57
80
  {#each nodes as node, index (node.nodeId ?? index)}
@@ -1 +1 @@
1
- {"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AAqCH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AA4DH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -14,7 +14,6 @@
14
14
  import DebugExperience from './DebugExperience.svelte';
15
15
  import MissingComponent from './MissingComponent.svelte';
16
16
  import NodesRenderer from './NodesRenderer.svelte';
17
- import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
18
17
  import type { ServerExperienceRendererProps } from './component-props.js';
19
18
  import { setExperience } from './context.js';
20
19
  import type { RenderContext } from './types.js';
@@ -63,14 +62,10 @@
63
62
  {#if debug}
64
63
  <DebugExperience {experience} />
65
64
  {/if}
66
- <WrapWithExperienceTemplate experienceTemplate={experience.experienceTemplate} {config} experience={renderContext}>
67
- {#snippet children()}
68
- <NodesRenderer
69
- nodes={experience.nodes}
70
- {config}
71
- experience={renderContext}
72
- {renderUnknown}
73
- />
74
- {/snippet}
75
- </WrapWithExperienceTemplate>
65
+ <NodesRenderer
66
+ nodes={experience.nodes}
67
+ {config}
68
+ experience={renderContext}
69
+ {renderUnknown}
70
+ />
76
71
  {/if}
@@ -1 +1 @@
1
- {"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAsE1E,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
1
+ {"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAiE1E,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { defineComponent, defineExperienceTemplate, normalizeComponentRegistrati
15
15
  export type { ComponentConfig, Components, Config, ContentfulComponent, ContentfulExperienceTemplate, ExperienceTemplateConfig, ExperienceTemplateRegistration, ExperienceTemplates, Registration, RenderContext, ResolveToken, } from './types.js';
16
16
  export { resolveExperience } from '@contentful/experiences-sdk-core';
17
17
  export type { ResolverConfig, ResolveExperienceOptions } from '@contentful/experiences-sdk-core';
18
- export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ValuesByViewport, ViewportDef, } from '@contentful/experiences-sdk-core';
18
+ export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ValuesByViewport, ViewportDef, } from '@contentful/experiences-sdk-core';
19
19
  export { CSS_PROPERTIES, getValueForViewport, getViewportIndex, isCssProperty, resolveDesignProperties, toCssKey, toCssMediaQuery, } from '@contentful/experiences-design';
20
20
  export { ContentfulViewDelivery, ContentfulViewDeliveryClient, DELIVERY_HOST, NotFoundError, PREVIEW_HOST, createClient, fetchExperience, } from '@contentful/experiences-client';
21
21
  export type { ClientOptions, CreateClientOptions, ExperienceOptions, ResolveOptions, } from '@contentful/experiences-client';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAItE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAElE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG/E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,YAAY,EACV,6BAA6B,EAC7B,6BAA6B,IAAI,uBAAuB,EACxD,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,8BAA8B,EAC9B,uCAAuC,GACxC,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,UAAU,EACV,MAAM,EACN,mBAAmB,EACnB,4BAA4B,EAC5B,wBAAwB,EACxB,8BAA8B,EAC9B,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,YAAY,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAGjG,YAAY,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,0BAA0B,EAC1B,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,GACZ,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,QAAQ,EACR,eAAe,GAChB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKtE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAElE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG/E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,YAAY,EACV,6BAA6B,EAC7B,6BAA6B,IAAI,uBAAuB,EACxD,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,8BAA8B,EAC9B,uCAAuC,GACxC,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,UAAU,EACV,MAAM,EACN,mBAAmB,EACnB,4BAA4B,EAC5B,wBAAwB,EACxB,8BAA8B,EAC9B,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,YAAY,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAGjG,YAAY,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,GACZ,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,QAAQ,EACR,eAAe,GAChB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@ export { default as ExperienceRenderer } from './ClientExperienceRenderer.svelte
17
17
  export { default as ServerExperienceRenderer } from './ServerExperienceRenderer.svelte';
18
18
  export { default as MissingComponent } from './MissingComponent.svelte';
19
19
  export { default as DebugExperience } from './DebugExperience.svelte';
20
- // Exposed so advanced customers can render non-`children` slots manually:
20
+ // Every slot already arrives as a same-named `Snippet[]` prop; this is exposed so
21
+ // advanced customers can render a slot's raw nodes themselves instead:
21
22
  // e.g. `<NodesRenderer nodes={contentful.slots.header as PortableRenderNode[]} ... />`.
22
23
  export { default as NodesRenderer } from './NodesRenderer.svelte';
23
24
  export { useActiveViewport } from './use-active-viewport.svelte.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=nodes-renderer.ssr.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodes-renderer.ssr.test.d.ts","sourceRoot":"","sources":["../src/nodes-renderer.ssr.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,126 @@
1
+ /*
2
+ * True SSR coverage: renders through `svelte/server` so Svelte's *server*
3
+ * compiler output is exercised. Every other test in this package renders via
4
+ * `@testing-library/svelte`, i.e. the client build.
5
+ *
6
+ * The two builds differ in how a compiled snippet receives its arguments — by
7
+ * getter on the client, by value on the server — and `NodesRenderer` builds one
8
+ * snippet call by hand (`toChildSnippet`). These tests pin that nested nodes
9
+ * render under SSR for both node kinds; without the normalization in
10
+ * `NodesRenderer`, every child node throws.
11
+ *
12
+ * Runs under vitest.ssr.config.ts (environment: node).
13
+ */
14
+ import { describe, expect, it } from 'vitest';
15
+ import { render } from 'svelte/server';
16
+ import { resolveExperience } from '@contentful/experiences-sdk-core';
17
+ import ServerExperienceRenderer from './ServerExperienceRenderer.svelte';
18
+ import ButtonFixture from './test-fixtures/ButtonFixture.svelte';
19
+ import ContainerFixture from './test-fixtures/ContainerFixture.svelte';
20
+ import ExperienceTemplateFixture from './test-fixtures/ExperienceTemplateFixture.svelte';
21
+ const VIEWPORTS = [{ id: 'desktop', query: '*', displayName: 'Desktop', previewSize: '100%' }];
22
+ const componentNode = (typeId, rest = {}) => ({
23
+ component: {
24
+ sys: {
25
+ type: 'ResourceLink',
26
+ linkType: 'Contentful:Component',
27
+ urn: `crn:contentful:::experience:spaces/$self/environments/$self/components/${typeId}`,
28
+ },
29
+ },
30
+ ...rest,
31
+ });
32
+ const experienceTemplateNode = (typeId, rest = {}) => ({
33
+ experienceTemplate: {
34
+ sys: {
35
+ type: 'ResourceLink',
36
+ linkType: 'Contentful:ExperienceTemplate',
37
+ urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
38
+ },
39
+ },
40
+ ...rest,
41
+ });
42
+ const button = (label) => componentNode('contentful-button', {
43
+ id: `btn-${label}`,
44
+ contentProperties: { label },
45
+ designProperties: {},
46
+ slots: {},
47
+ });
48
+ const config = {
49
+ components: {
50
+ 'contentful-container': ContainerFixture,
51
+ 'contentful-button': ButtonFixture,
52
+ },
53
+ experienceTemplates: { page: ExperienceTemplateFixture },
54
+ };
55
+ const renderPlan = async (payload) => {
56
+ const plan = await resolveExperience(payload, config);
57
+ return render(ServerExperienceRenderer, {
58
+ props: { experience: plan, config },
59
+ }).body;
60
+ };
61
+ describe('NodesRenderer — SSR (server-compiled output)', () => {
62
+ it('renders a component node’s slot children', async () => {
63
+ const html = await renderPlan({
64
+ viewports: VIEWPORTS,
65
+ nodes: [
66
+ componentNode('contentful-container', {
67
+ id: 'container',
68
+ contentProperties: {},
69
+ designProperties: {},
70
+ slots: { children: [button('Get started')] },
71
+ }),
72
+ ],
73
+ });
74
+ expect(html).toContain('Get started');
75
+ expect(html.indexOf('<div')).toBeLessThan(html.indexOf('Get started'));
76
+ });
77
+ it('renders a coded Experience Template node’s named `content` slot', async () => {
78
+ const html = await renderPlan({
79
+ viewports: VIEWPORTS,
80
+ nodes: [
81
+ experienceTemplateNode('page', {
82
+ id: 'tpl',
83
+ contentProperties: { title: 'Coded page' },
84
+ designProperties: {},
85
+ slots: { content: [button('Read the blog')] },
86
+ }),
87
+ ],
88
+ });
89
+ expect(html).toContain('data-experience-template="page"');
90
+ expect(html).toContain('data-title="Coded page"');
91
+ expect(html).toContain('Read the blog');
92
+ // The button is inside the template chrome, not a sibling of it.
93
+ expect(html.split('<main')[1]).toContain('Read the blog');
94
+ });
95
+ it('renders deeply nested slot children', async () => {
96
+ const html = await renderPlan({
97
+ viewports: VIEWPORTS,
98
+ nodes: [
99
+ experienceTemplateNode('page', {
100
+ id: 'tpl',
101
+ contentProperties: {},
102
+ designProperties: {},
103
+ slots: {
104
+ content: [
105
+ componentNode('contentful-container', {
106
+ id: 'container',
107
+ contentProperties: {},
108
+ designProperties: {},
109
+ slots: { children: [button('Deep')] },
110
+ }),
111
+ ],
112
+ },
113
+ }),
114
+ ],
115
+ });
116
+ expect(html).toContain('Deep');
117
+ });
118
+ it('renders a composite experience unwrapped', async () => {
119
+ const html = await renderPlan({
120
+ viewports: VIEWPORTS,
121
+ nodes: [button('Alone')],
122
+ });
123
+ expect(html).toContain('Alone');
124
+ expect(html).not.toContain('<main');
125
+ });
126
+ });
@@ -35,6 +35,38 @@ function componentNode(typeId, rest = {}) {
35
35
  ...rest,
36
36
  };
37
37
  }
38
+ /**
39
+ * A coded Experience Template is an ordinary top-level node — the only
40
+ * difference from `componentNode` is which registry its id resolves against.
41
+ */
42
+ function experienceTemplateNode(typeId, rest = {}) {
43
+ return {
44
+ experienceTemplate: {
45
+ sys: {
46
+ type: 'ResourceLink',
47
+ linkType: 'Contentful:ExperienceTemplate',
48
+ urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
49
+ },
50
+ },
51
+ ...rest,
52
+ };
53
+ }
54
+ /**
55
+ * `sys.experienceTemplate` rides along on every Experience — coded and
56
+ * composite alike — so it must never influence rendering. Tests attach it to
57
+ * prove it is ignored.
58
+ */
59
+ function sysWithExperienceTemplate(typeId) {
60
+ return {
61
+ experienceTemplate: {
62
+ sys: {
63
+ type: 'ResourceLink',
64
+ linkType: 'Contentful:ExperienceTemplate',
65
+ urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
66
+ },
67
+ },
68
+ };
69
+ }
38
70
  beforeEach(() => {
39
71
  captureSink.splice(0);
40
72
  });
@@ -158,13 +190,13 @@ describe('ServerExperienceRenderer', () => {
158
190
  nodes: [
159
191
  {
160
192
  nodeId: 'root',
161
- registration: { componentId: 'contentful-container' },
193
+ registration: { kind: 'component', id: 'contentful-container' },
162
194
  props: { content: {}, design: {}, designRaw: {} },
163
195
  slots: {
164
196
  children: [
165
197
  {
166
198
  nodeId: 'ghost',
167
- registration: { componentId: 'NotRegistered' },
199
+ registration: { kind: 'component', id: 'NotRegistered' },
168
200
  props: { content: {}, design: {}, designRaw: {} },
169
201
  slots: {},
170
202
  },
@@ -247,7 +279,7 @@ describe('ServerExperienceRenderer', () => {
247
279
  nodes: [
248
280
  {
249
281
  nodeId: 'r',
250
- registration: { componentId: 'item' },
282
+ registration: { kind: 'component', id: 'item' },
251
283
  props: {
252
284
  content: { value: 'fromContent' },
253
285
  design: {},
@@ -263,25 +295,26 @@ describe('ServerExperienceRenderer', () => {
263
295
  });
264
296
  expect(container.innerHTML).toContain('data-value="fromResolveData"');
265
297
  });
266
- it('wraps rendered nodes with the registered experienceTemplate', async () => {
298
+ it('renders an experienceTemplate node from the experienceTemplates registry', async () => {
267
299
  const tplConfig = {
268
300
  components: { item: PrecedenceFixture },
269
301
  experienceTemplates: {
270
302
  page: { component: ExperienceTemplateFixture, defaults: { title: 'Default Title' } },
271
303
  },
272
304
  };
305
+ // The payload's `content` slot must arrive as a named `content` Snippet[]
306
+ // prop — the fixture renders it, so the child only appears if that holds.
273
307
  const tplPayload = {
274
- sys: {
275
- experienceTemplate: {
276
- sys: {
277
- type: 'ResourceLink',
278
- linkType: 'Contentful:ExperienceTemplate',
279
- urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/page',
280
- },
281
- },
282
- },
308
+ sys: sysWithExperienceTemplate('page'),
283
309
  viewports: VIEWPORTS,
284
- nodes: [componentNode('item', { id: 'i', contentProperties: { value: 'inside' } })],
310
+ nodes: [
311
+ experienceTemplateNode('page', {
312
+ id: 'tpl',
313
+ slots: {
314
+ content: [componentNode('item', { id: 'i', contentProperties: { value: 'inside' } })],
315
+ },
316
+ }),
317
+ ],
285
318
  };
286
319
  const plan = await resolveExperience(tplPayload, tplConfig);
287
320
  const { container } = render(ServerExperienceRenderer, {
@@ -291,26 +324,52 @@ describe('ServerExperienceRenderer', () => {
291
324
  expect(container.innerHTML).toContain('data-title="Default Title"');
292
325
  expect(container.innerHTML).toContain('inside');
293
326
  });
294
- it('renders nodes unwrapped + warns when the experienceTemplate is not registered', async () => {
327
+ it('renders a composite experience unwrapped — sys.experienceTemplate is ignored', async () => {
328
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
329
+ // `hero` is registered as a template, but the nodes are plain components,
330
+ // so nothing may wrap them.
331
+ const cfg = {
332
+ components: { item: PrecedenceFixture },
333
+ experienceTemplates: { hero: ExperienceTemplateFixture },
334
+ };
335
+ const plan = await resolveExperience({
336
+ sys: sysWithExperienceTemplate('hero'),
337
+ viewports: VIEWPORTS,
338
+ nodes: [
339
+ componentNode('item', { id: 'a', contentProperties: { value: 'one' } }),
340
+ componentNode('item', { id: 'b', contentProperties: { value: 'two' } }),
341
+ ],
342
+ }, cfg);
343
+ const { container } = render(ServerExperienceRenderer, {
344
+ props: { experience: plan, config: cfg },
345
+ });
346
+ expect(container.innerHTML).toContain('data-value="one"');
347
+ expect(container.innerHTML).toContain('data-value="two"');
348
+ expect(container.innerHTML).not.toContain('data-experience-template');
349
+ expect(warn).not.toHaveBeenCalled();
350
+ warn.mockRestore();
351
+ });
352
+ it('renders slot children unwrapped + warns when the experienceTemplate is not registered', async () => {
295
353
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
296
354
  const cfg = { components: { item: PrecedenceFixture } };
297
355
  const tplPayload = {
298
- sys: {
299
- experienceTemplate: {
300
- sys: {
301
- type: 'ResourceLink',
302
- linkType: 'Contentful:ExperienceTemplate',
303
- urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/missing-experienceTemplate',
304
- },
305
- },
306
- },
307
356
  viewports: VIEWPORTS,
308
- nodes: [componentNode('item', { id: 'i', contentProperties: { value: 'unwrapped' } })],
357
+ nodes: [
358
+ experienceTemplateNode('missing-experienceTemplate', {
359
+ id: 'tpl',
360
+ slots: {
361
+ content: [
362
+ componentNode('item', { id: 'i', contentProperties: { value: 'unwrapped' } }),
363
+ ],
364
+ },
365
+ }),
366
+ ],
309
367
  };
310
368
  const plan = await resolveExperience(tplPayload, cfg);
311
369
  const { container } = render(ServerExperienceRenderer, {
312
370
  props: { experience: plan, config: cfg },
313
371
  });
372
+ // The subtree survives — an unregistered template must not blank the page.
314
373
  expect(container.innerHTML).toContain('data-value="unwrapped"');
315
374
  expect(container.innerHTML).not.toContain('data-experience-template');
316
375
  expect(warn).toHaveBeenCalledWith(expect.stringContaining('missing-experienceTemplate'));
@@ -407,8 +466,9 @@ describe('ServerExperienceRenderer — bare-component registrations', () => {
407
466
  expect(keys).toContain('text');
408
467
  expect(keys).not.toContain('experience');
409
468
  expect(keys).not.toContain('contentful');
410
- // Children is always injected as a Snippet by the renderer.
411
- expect(keys).toContain('children');
469
+ // Slot props exist only for slots the payload actually carries — `children`
470
+ // is just a slot name, so a node without slots gets no `children` prop.
471
+ expect(keys).not.toContain('children');
412
472
  });
413
473
  });
414
474
  describe('ServerExperienceRenderer — getContentfulComponent()', () => {
@@ -518,32 +578,28 @@ describe('ServerExperienceRenderer — resolveToken', () => {
518
578
  // unchanged, so the customer can inspect it or resolve it themselves.
519
579
  expect(container.innerHTML).toContain('data-bg="[object Object]"');
520
580
  });
521
- it('runs on page-level experienceTemplate design props too', async () => {
581
+ it('runs on an experienceTemplate node design props too', async () => {
522
582
  const tplConfig = {
523
583
  components: { item: PrecedenceFixture },
524
584
  experienceTemplates: { page: ExperienceTemplateFixture },
525
585
  resolveToken: (ref) => (ref.value === 'brand/canvas' ? '#111827' : undefined),
526
586
  };
527
587
  const tplPayload = {
528
- sys: {
529
- experienceTemplate: {
530
- sys: {
531
- type: 'ResourceLink',
532
- linkType: 'Contentful:ExperienceTemplate',
533
- urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/page',
534
- },
535
- },
536
- },
588
+ sys: sysWithExperienceTemplate('page'),
537
589
  viewports: VIEWPORTS,
538
- nodes: [componentNode('item', { id: 'i', contentProperties: { value: 'ok' } })],
590
+ nodes: [
591
+ experienceTemplateNode('page', {
592
+ id: 'tpl',
593
+ designProperties: { cfBackground: dt('brand/canvas') },
594
+ slots: {
595
+ content: [componentNode('item', { id: 'i', contentProperties: { value: 'ok' } })],
596
+ },
597
+ }),
598
+ ],
539
599
  };
540
600
  const plan = await resolveExperience(tplPayload, tplConfig);
541
- // Experience Templates don't carry design props in the current XDA payload
542
- // shape; seed a raw token on the resolved plan so we exercise the
543
- // experienceTemplate path. Render at a non-fallback viewport (mobile ≠
544
- // fallback index 0) so the adapter recomputes from the seeded raw design
545
- // and runs resolveToken.
546
- plan.experienceTemplate.props.designRaw = { cfBackground: dt('brand/canvas') };
601
+ // Render at a non-fallback viewport (mobile ≠ fallback index 0) so the
602
+ // adapter recomputes from the raw design and runs resolveToken itself.
547
603
  const { container } = render(ServerExperienceRenderer, {
548
604
  props: { experience: plan, config: tplConfig, initialViewportId: 'mobile' },
549
605
  });
@@ -3,16 +3,21 @@
3
3
 
4
4
  import { getDesignValues } from '../get-design-values.js';
5
5
 
6
+ // A coded Experience Template's slots arrive as named `Snippet[]` props, so
7
+ // the payload's `content` slot lands on a `content` prop — no `children`
8
+ // special case.
6
9
  let {
7
10
  title,
8
- children,
11
+ content,
9
12
  }: {
10
13
  title?: string;
11
- children: Snippet;
14
+ content?: Snippet[];
12
15
  } = $props();
13
16
  const design = $derived(getDesignValues());
14
17
  </script>
15
18
 
16
19
  <main data-experience-template="page" data-title={title} data-bg={design.cfBackground as string}>
17
- {@render children()}
20
+ {#each content ?? [] as childSnippet, index (index)}
21
+ {@render childSnippet()}
22
+ {/each}
18
23
  </main>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  title?: string;
4
- children: Snippet;
4
+ content?: Snippet[];
5
5
  };
6
6
  declare const ExperienceTemplateFixture: import("svelte").Component<$$ComponentProps, {}, "">;
7
7
  type ExperienceTemplateFixture = ReturnType<typeof ExperienceTemplateFixture>;
@@ -1 +1 @@
1
- {"version":3,"file":"ExperienceTemplateFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ExperienceTemplateFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAmBJ,QAAA,MAAM,yBAAyB,sDAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
1
+ {"version":3,"file":"ExperienceTemplateFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ExperienceTemplateFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB,CAAC;AAwBJ,QAAA,MAAM,yBAAyB,sDAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { ResolveContext, ResolveToken };
7
7
  * node. Useful for custom design-property resolution outside the SDK's
8
8
  * cascade, branching by `componentId` in a generic wrapper, keying
9
9
  * analytics on `nodeId`, rendering a raw-payload panel in preview, or
10
- * rendering non-`children` slots through the exported `<NodesRenderer />`.
10
+ * re-rendering a slot's nodes yourself through the exported `<NodesRenderer />`.
11
11
  *
12
12
  * Design properties stay in their **raw discriminated form** here (the same
13
13
  * shape `ctx.design` carries inside `resolveData`). The viewport-cascaded,
@@ -20,20 +20,21 @@ export interface ContentfulComponent {
20
20
  design: Record<string, DesignPropValue>;
21
21
  resolved?: Record<string, unknown>;
22
22
  /**
23
- * Raw per-slot node arrays from the payload. The default slot named
24
- * `children` is rendered automatically and passed as a Snippet prop;
25
- * additional slots (if any) are reachable here and can be rendered with
26
- * `<NodesRenderer nodes={...} />`.
23
+ * Raw per-slot node arrays from the payload. Every slot is also rendered
24
+ * automatically and passed as a same-named `Snippet[]` prop (`children` is
25
+ * not special); these raw nodes are here for callers that want to render a
26
+ * slot themselves with `<NodesRenderer nodes={...} />`.
27
27
  */
28
28
  slots: Record<string, unknown>;
29
29
  }
30
30
  /**
31
- * Same shape as `ContentfulComponent`, but for the page-level Experience
32
- * Template.
33
- * Exposed via `getContentfulExperienceTemplate()` inside an Experience Template's component tree.
31
+ * Same shape as `ContentfulComponent`, but for an Experience Template node.
32
+ * Exposed via `getContentfulExperienceTemplate()` inside an Experience
33
+ * Template's component tree.
34
34
  */
35
35
  export interface ContentfulExperienceTemplate {
36
36
  experienceTemplateId: string;
37
+ nodeId?: string;
37
38
  content: Record<string, unknown>;
38
39
  design: Record<string, DesignPropValue>;
39
40
  resolved?: Record<string, unknown>;
@@ -56,8 +57,8 @@ export interface RenderContext extends ExperienceContext {
56
57
  }
57
58
  /**
58
59
  * Customer-supplied configuration for a single component type. The `component`
59
- * receives the merged props (design + content + resolveData + `children`
60
- * Snippet); resolved design values auto-fill matching props and are also
60
+ * receives the merged props (design + content + resolveData + one `Snippet[]`
61
+ * prop per slot); resolved design values auto-fill matching props and are also
61
62
  * readable via `getDesignValues()`. Runtime context and raw payload come from
62
63
  * `getExperience()` / `getContentfulComponent()`.
63
64
  */
@@ -76,6 +77,13 @@ export interface ComponentConfig<Props extends object = Record<string, unknown>>
76
77
  * card: defineComponent<CardProps>({ component: Card, resolveData: ... }),
77
78
  */
78
79
  export type Registration<Props extends object = Record<string, unknown>> = Component<any> | ComponentConfig<Props>;
80
+ /**
81
+ * Customer-supplied configuration for a coded Experience Template. Identical in
82
+ * shape and behavior to `ComponentConfig` — an Experience Template is just a
83
+ * node whose implementation lives in the `experienceTemplates` registry. Its
84
+ * slots arrive as named `Snippet[]` props (a `content` slot becomes a `content`
85
+ * prop), so there is no `children` special case.
86
+ */
79
87
  export interface ExperienceTemplateConfig<Props extends object = Record<string, unknown>> {
80
88
  defaults?: Partial<Props>;
81
89
  resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;
@@ -84,7 +92,17 @@ export interface ExperienceTemplateConfig<Props extends object = Record<string,
84
92
  export type ExperienceTemplateRegistration<Props extends object = Record<string, unknown>> = Component<any> | ExperienceTemplateConfig<Props>;
85
93
  export declare function defineExperienceTemplate<Props extends object = Record<string, unknown>>(config: ExperienceTemplateConfig<Props>): ExperienceTemplateConfig<Props>;
86
94
  export declare function defineComponent<Props extends object = Record<string, unknown>>(config: ComponentConfig<Props>): ComponentConfig<Props>;
95
+ /**
96
+ * Component registry — keyed by `componentId` (last slash-segment of
97
+ * `component.sys.urn`).
98
+ */
87
99
  export type Components = Record<string, Registration<any>>;
100
+ /**
101
+ * Experience Template registry — keyed by `experienceTemplateId` (last
102
+ * slash-segment of an Experience Template node's own
103
+ * `experienceTemplate.sys.urn`). Not read from `payload.sys`; templates are
104
+ * ordinary nodes.
105
+ */
88
106
  export type ExperienceTemplates = Record<string, ExperienceTemplateRegistration<any>>;
89
107
  export interface Config {
90
108
  components: Components;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-svelte",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Svelte adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,13 +31,13 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "svelte-package -i src -o dist && publint",
34
- "test": "vitest run",
34
+ "test": "vitest run && vitest run -c vitest.ssr.config.ts",
35
35
  "check": "svelte-check --tsconfig ./tsconfig.json"
36
36
  },
37
37
  "dependencies": {
38
- "@contentful/experiences-client": "0.3.4",
39
- "@contentful/experiences-sdk-core": "0.7.4",
40
- "@contentful/experiences-design": "0.7.4"
38
+ "@contentful/experiences-client": "0.3.5",
39
+ "@contentful/experiences-sdk-core": "0.7.5",
40
+ "@contentful/experiences-design": "0.7.5"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "svelte": "^5.55.7"
@@ -1,90 +0,0 @@
1
- <!--
2
- * Wraps the rendered Experience nodes with the page-level Experience Template when the
3
- * plan carries one and the customer registered an Experience Template config under that
4
- * id. When the Experience Template is referenced but unregistered, warns once and
5
- * renders children unwrapped — graceful degradation matches the
6
- * unknown-component fallback story.
7
- -->
8
- <script lang="ts">
9
- import type { Snippet } from 'svelte';
10
-
11
- import type { PortableExperienceTemplate } from '@contentful/experiences-sdk-core';
12
-
13
- import { setContentfulExperienceTemplate, setResolvedDesign } from './context.js';
14
- import { selectResolvedDesign } from './design-utils.js';
15
- import {
16
- normalizeExperienceTemplateRegistration,
17
- type ContentfulExperienceTemplate,
18
- type Config,
19
- type RenderContext,
20
- } from './types.js';
21
-
22
- interface WrapWithExperienceTemplateProps {
23
- experienceTemplate: PortableExperienceTemplate | undefined;
24
- config: Config;
25
- experience: RenderContext;
26
- children: Snippet;
27
- }
28
-
29
- let { experienceTemplate, config, experience, children }: WrapWithExperienceTemplateProps = $props();
30
-
31
- // setContext must run during synchronous component init — call once with
32
- // the initial value (Experience Templates do not toggle on/off mid-mount).
33
- if (experienceTemplate) {
34
- setContentfulExperienceTemplate({
35
- experienceTemplateId: experienceTemplate.experienceTemplateId,
36
- content: experienceTemplate.props.content,
37
- design: experienceTemplate.props.designRaw,
38
- resolved: experienceTemplate.props.resolved,
39
- } satisfies ContentfulExperienceTemplate);
40
- }
41
-
42
- const entry = $derived(experienceTemplate ? config.experienceTemplates?.[experienceTemplate.experienceTemplateId] : undefined);
43
- const experienceTemplateConfig = $derived(entry ? normalizeExperienceTemplateRegistration(entry) : null);
44
-
45
- const tokenResolvedDesign = $derived.by(() => {
46
- if (!experienceTemplate) return {};
47
- const { props, unresolved } = selectResolvedDesign(
48
- experienceTemplate.props,
49
- experience.viewports,
50
- experience.activeViewportIndex,
51
- experience.fallbackViewportIndex,
52
- config.resolveToken
53
- );
54
- if (unresolved.length && typeof console !== 'undefined') {
55
- console.warn(
56
- `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on experience template "${experienceTemplate.experienceTemplateId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
57
- );
58
- }
59
- return props;
60
- });
61
-
62
- // A getter (not a snapshot) so getDesignValues() stays reactive.
63
- setResolvedDesign(() => tokenResolvedDesign);
64
-
65
- // Same precedence as component nodes.
66
- const composed = $derived.by(() => {
67
- if (!experienceTemplate || !experienceTemplateConfig) return null;
68
- return {
69
- ...experienceTemplateConfig.defaults,
70
- ...tokenResolvedDesign,
71
- ...experienceTemplate.props.content,
72
- ...experienceTemplate.props.resolved,
73
- };
74
- });
75
-
76
- $effect(() => {
77
- if (experienceTemplate && !experienceTemplateConfig && typeof console !== 'undefined') {
78
- console.warn(
79
- `[@contentful/experiences-svelte] No experience template registered for id "${experienceTemplate.experienceTemplateId}". Rendering nodes without the experience template wrapper.`
80
- );
81
- }
82
- });
83
- </script>
84
-
85
- {#if experienceTemplate && experienceTemplateConfig && composed}
86
- {@const Tpl = experienceTemplateConfig.component}
87
- <Tpl {...composed} {children} />
88
- {:else}
89
- {@render children()}
90
- {/if}
@@ -1,13 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { PortableExperienceTemplate } from '@contentful/experiences-sdk-core';
3
- import { type Config, type RenderContext } from './types.js';
4
- interface WrapWithExperienceTemplateProps {
5
- experienceTemplate: PortableExperienceTemplate | undefined;
6
- config: Config;
7
- experience: RenderContext;
8
- children: Snippet;
9
- }
10
- declare const WrapWithExperienceTemplate: import("svelte").Component<WrapWithExperienceTemplateProps, {}, "">;
11
- type WrapWithExperienceTemplate = ReturnType<typeof WrapWithExperienceTemplate>;
12
- export default WrapWithExperienceTemplate;
13
- //# sourceMappingURL=WrapWithExperienceTemplate.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WrapWithExperienceTemplate.svelte.d.ts","sourceRoot":"","sources":["../src/WrapWithExperienceTemplate.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAInF,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,+BAA+B;IACvC,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6EH,QAAA,MAAM,0BAA0B,qEAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}