@contentful/experiences-svelte 0.4.1 → 0.5.1

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +39 -8
  3. package/dist/ClientExperienceRenderer.svelte +3 -3
  4. package/dist/DebugExperience.svelte +3 -1
  5. package/dist/DebugExperience.svelte.d.ts.map +1 -1
  6. package/dist/DebugExperience.test.js +6 -6
  7. package/dist/MissingComponent.svelte +6 -6
  8. package/dist/NodeRenderer.svelte +5 -5
  9. package/dist/NodeRenderer.svelte.d.ts +1 -1
  10. package/dist/NodeRenderer.svelte.d.ts.map +1 -1
  11. package/dist/NodesRenderer.svelte +29 -21
  12. package/dist/NodesRenderer.svelte.d.ts +0 -1
  13. package/dist/NodesRenderer.svelte.d.ts.map +1 -1
  14. package/dist/ServerExperienceRenderer.svelte +3 -3
  15. package/dist/WrapWithExperienceTemplate.svelte +87 -0
  16. package/dist/WrapWithExperienceTemplate.svelte.d.ts +13 -0
  17. package/dist/WrapWithExperienceTemplate.svelte.d.ts.map +1 -0
  18. package/dist/component-props.d.ts +1 -1
  19. package/dist/component-props.d.ts.map +1 -1
  20. package/dist/context.d.ts +6 -5
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +8 -7
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -3
  26. package/dist/server-renderer.test.js +90 -29
  27. package/dist/test-fixtures/ContainerFixture.svelte +4 -2
  28. package/dist/test-fixtures/ContainerFixture.svelte.d.ts +1 -1
  29. package/dist/test-fixtures/ContainerFixture.svelte.d.ts.map +1 -1
  30. package/dist/test-fixtures/{TemplateFixture.svelte → ExperienceTemplateFixture.svelte} +1 -1
  31. package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts +9 -0
  32. package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts.map +1 -0
  33. package/dist/test-fixtures/WrappingContainerFixture.svelte +27 -0
  34. package/dist/test-fixtures/WrappingContainerFixture.svelte.d.ts +8 -0
  35. package/dist/test-fixtures/WrappingContainerFixture.svelte.d.ts.map +1 -0
  36. package/dist/types.d.ts +13 -12
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +2 -2
  39. package/package.json +5 -5
  40. package/dist/WrapWithTemplate.svelte +0 -87
  41. package/dist/WrapWithTemplate.svelte.d.ts +0 -13
  42. package/dist/WrapWithTemplate.svelte.d.ts.map +0 -1
  43. package/dist/test-fixtures/TemplateFixture.svelte.d.ts +0 -9
  44. package/dist/test-fixtures/TemplateFixture.svelte.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,102 @@
1
+ ## 0.5.1 (2026-08-07)
2
+
3
+ ### 🚀 Features
4
+
5
+ - expose slot children as an array instead of a single wrapping node ([#122](https://github.com/contentful/experiences/pull/122))
6
+
7
+ ## 0.5.0 (2026-08-07)
8
+
9
+ ### 🩹 Fixes
10
+
11
+ - ⚠️ bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
12
+
13
+ ### ⚠️ Breaking Changes
14
+
15
+ - bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
16
+ the public API uses the component / experienceTemplate vocabulary.
17
+ - `registration.componentTypeId` -> `registration.componentId`
18
+ - `plan.template` -> `plan.experienceTemplate`
19
+ - `PortableTemplate` -> `PortableExperienceTemplate` (`templateId` ->
20
+ `experienceTemplateId`)
21
+ - `Config.templates` -> `Config.experienceTemplates`; `Templates` ->
22
+ `ExperienceTemplates`
23
+ - `defineTemplate` -> `defineExperienceTemplate`; `TemplateConfig` ->
24
+ `ExperienceTemplateConfig`; `TemplateRegistration` ->
25
+ `ExperienceTemplateRegistration`; `normalizeTemplateRegistration` ->
26
+ `normalizeExperienceTemplateRegistration`
27
+ - `useContentfulTemplate()` / `getContentfulTemplate()` ->
28
+ `useContentfulExperienceTemplate()` / `getContentfulExperienceTemplate()`;
29
+ `ContentfulTemplate` -> `ContentfulExperienceTemplate`
30
+ - `MissingComponentProps.componentTypeId` -> `componentId`
31
+ - Core payload types: `ComponentTypeNode`/`ComponentTypeRef` ->
32
+ `ComponentNode`/`ComponentRef`; `TemplateNode`/`TemplateRef` ->
33
+ `ExperienceTemplateNode`/`ExperienceTemplateRef`
34
+ `packages/core` stays zero-dep, so its payload types are hand-mirrored from the
35
+ delivery package rather than imported; each carries a doc comment naming its
36
+ upstream counterpart, and `client:typecheck` catches drift when the delivery SDK
37
+ regenerates. Documented in AGENTS.md.
38
+ `examples/scripts/` is intentionally unchanged: it targets the management SDK
39
+ (`contentful-management`), which has its own entity-type surface, and adopting it
40
+ would require changing that dependency. The seed script therefore still
41
+ provisions the shapes that SDK produces.
42
+ Verified: build, typecheck, and 157 tests pass across all five packages.
43
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
44
+ * docs: describe current state only in comments and docs
45
+ Drop ticket ids, tracker links, and before/after narrative from doc comments and
46
+ prose so they explain what the code does now rather than how it got here.
47
+ - `alpha-feature.ts`: state what the header selects and why it is required,
48
+ without the two-shapes-in-transition framing or the removal timeline.
49
+ - `core/src/types.ts`: keep each type's upstream counterpart in
50
+ `@contentful/experience-delivery` (load-bearing for maintenance) but drop the
51
+ "formerly known as" and old-link-type asides.
52
+ - README: replace the rename migration table with a description of the
53
+ alpha-feature header and when a caller needs to send it.
54
+ - AGENTS.md: reframe the rationale entries that opened on prior designs to state
55
+ the current design and its reason.
56
+ - Drop stale option-shape asides ("was nested under ...") and a test name that
57
+ described superseded behaviour.
58
+ Generated CHANGELOG.md files are untouched — they are release history.
59
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60
+ * feat!: migrate bootstrap fixture script to component/experienceTemplate entities
61
+ contentful-management@12.14.0 exposes the renamed ExO entities (Component,
62
+ ExperienceTemplate) alongside the deprecated ComponentType/Template ones. Adopt
63
+ the renamed shapes throughout examples/scripts so a freshly bootstrapped demo
64
+ Experience uses the same entity vocabulary the delivery-consuming packages now
65
+ read.
66
+ - Bump contentful-management to ^12.14.0 (existing dependency, version only —
67
+ no new packages).
68
+ - fixture/component-types.ts -> fixture/components.ts: ComponentTypeFixture ->
69
+ ComponentFixture.
70
+ - fixture/templates.ts -> fixture/experience-templates.ts:
71
+ TemplateFixture -> ExperienceTemplateFixture, TemplateTreeNode ->
72
+ ExperienceTemplateTreeNode.
73
+ - fixture/types.ts, fixture/experience.ts: ExperienceFixture.templateId ->
74
+ experienceTemplateId; node nodeType 'InlineFragment' ->
75
+ 'InlineExperienceFragment' with componentTypeId -> componentId, matching
76
+ contentful-management's InlineExperienceFragmentNode.
77
+ - fixture/data-assemblies.ts: dataAssemblyComponentTypeLinks ->
78
+ dataAssemblyComponentLinks (componentTypeId -> componentId).
79
+ - bootstrap-example.ts: cma.componentType -> cma.component,
80
+ cma.template -> cma.experienceTemplate; sys.type 'ComponentType' ->
81
+ 'Component', 'Template' -> 'ExperienceTemplate'; resource-link types
82
+ Contentful:ComponentType -> Contentful:Component and Contentful:Template ->
83
+ Contentful:ExperienceTemplate; URN segments components/ and
84
+ experienceTemplates/; seedComponentType -> seedComponent, seedTemplate ->
85
+ seedExperienceTemplate, linkDataAssembliesToComponentTypes ->
86
+ linkDataAssembliesToComponents.
87
+ The metadata.annotations.Template composed/coded-implementation marker is
88
+ unrelated to this rename (identical shape in 12.10.0 and 12.14.0) and is left
89
+ untouched.
90
+ Verified: examples/scripts typechecks clean; the main workspace's build,
91
+ typecheck, and 157 tests are unaffected.
92
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
93
+
94
+ ### 🧱 Updated Dependencies
95
+
96
+ - Updated client to 0.3.0
97
+ - Updated design to 0.7.0
98
+ - Updated core to 0.7.0
99
+
1
100
  ## 0.4.1 (2026-08-06)
2
101
 
3
102
  ### 🧱 Updated Dependencies
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
- defineTemplate<Props>(config); // Same shape, for page-level template wrappers
23
+ defineExperienceTemplate<Props>(config); // Same shape, for page-level Experience Template wrappers
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
38
+ NodesRenderer; // Exposed so you can render non-`children` slots manually (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>(); // Resolved design values for the current node; read in 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
- getContentfulTemplate(); // Same, for the page-level template
49
+ getContentfulExperienceTemplate(); // Same, for the page-level Experience Template
50
50
  type ToCssOptions;
51
51
  ```
52
52
 
@@ -56,12 +56,12 @@ Design is **not** injected as props; components read it via `getDesignValues()`
56
56
 
57
57
  ```ts
58
58
  // From core
59
- type Config, Components, Templates, Registration, TemplateRegistration,
60
- type ComponentConfig, TemplateConfig,
61
- type ContentfulComponent, ContentfulTemplate,
59
+ type Config, Components, ExperienceTemplates, Registration, ExperienceTemplateRegistration,
60
+ type ComponentConfig, ExperienceTemplateConfig,
61
+ type ContentfulComponent, ContentfulExperienceTemplate,
62
62
  type RenderContext, ResolveToken,
63
- type ExperiencePayload, ExperienceNode, ComponentTypeNode, TemplateNode,
64
- type PortableRenderPlan, PortableRenderNode, PortableTemplate,
63
+ type ExperiencePayload, ExperienceNode, ComponentNode, ExperienceTemplateNode,
64
+ type PortableRenderPlan, PortableRenderNode, PortableExperienceTemplate,
65
65
  type DesignPropValue, ManualDesignValue, DesignToken, ValuesByViewport,
66
66
  type ViewportDef, ExperienceContext, ResolveContext,
67
67
  type ResolverConfig, ResolveExperienceOptions
@@ -124,6 +124,37 @@ export const experienceConfig: Config = { components, resolveToken };
124
124
  <ServerExperienceRenderer experience={data.experience} config={experienceConfig} />
125
125
  ```
126
126
 
127
+ ### Slot children
128
+
129
+ 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.
130
+
131
+ ```svelte
132
+ <!-- Section.svelte -->
133
+ <script lang="ts">
134
+ import type { Snippet } from 'svelte';
135
+
136
+ let { children }: { children?: Snippet[] } = $props();
137
+ </script>
138
+
139
+ <!-- Common case — render them all: -->
140
+ <div>
141
+ {#each children ?? [] as child}
142
+ {@render child()}
143
+ {/each}
144
+ </div>
145
+
146
+ <!-- Or take control of each child: -->
147
+ <!--
148
+ <div>
149
+ {#each children ?? [] as child, i}
150
+ <div class="cell">{@render child()}</div>
151
+ {/each}
152
+ </div>
153
+ -->
154
+ ```
155
+
156
+ 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`.
157
+
127
158
  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).
128
159
 
129
160
  ---
@@ -14,7 +14,7 @@
14
14
  import DebugExperience from './DebugExperience.svelte';
15
15
  import MissingComponent from './MissingComponent.svelte';
16
16
  import NodesRenderer from './NodesRenderer.svelte';
17
- import WrapWithTemplate from './WrapWithTemplate.svelte';
17
+ import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
18
18
  import type { ClientExperienceRendererProps } from './component-props.js';
19
19
  import { setExperience } from './context.js';
20
20
  import type { RenderContext } from './types.js';
@@ -73,7 +73,7 @@
73
73
  {#if debug}
74
74
  <DebugExperience {experience} />
75
75
  {/if}
76
- <WrapWithTemplate template={experience.template} {config} experience={liveContext}>
76
+ <WrapWithExperienceTemplate experienceTemplate={experience.experienceTemplate} {config} experience={liveContext}>
77
77
  {#snippet children()}
78
78
  <NodesRenderer
79
79
  nodes={experience.nodes}
@@ -82,5 +82,5 @@
82
82
  {renderUnknown}
83
83
  />
84
84
  {/snippet}
85
- </WrapWithTemplate>
85
+ </WrapWithExperienceTemplate>
86
86
  {/if}
@@ -19,7 +19,9 @@
19
19
  const nodeCount = $derived(experience.nodes.length);
20
20
  const summary = $derived(
21
21
  `Experience debug — ${nodeCount} top-level node${nodeCount === 1 ? '' : 's'}${
22
- experience.template ? `, template: ${experience.template.templateId}` : ''
22
+ experience.experienceTemplate
23
+ ? `, experience template: ${experience.experienceTemplate.experienceTemplateId}`
24
+ : ''
23
25
  }`
24
26
  );
25
27
 
@@ -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;AAmDjE,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;AAqDjE,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -2,9 +2,9 @@ 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: [] };
5
- function node(componentTypeId, content = {}) {
5
+ function node(componentId, content = {}) {
6
6
  return {
7
- registration: { componentTypeId },
7
+ registration: { componentId },
8
8
  props: { content, design: {} },
9
9
  slots: {},
10
10
  };
@@ -29,14 +29,14 @@ describe('DebugExperience.svelte', () => {
29
29
  const { container } = render(DebugExperience, { props: { experience: one } });
30
30
  expect(container.innerHTML).toContain('Experience debug — 1 top-level node');
31
31
  });
32
- it('names the template in the summary when present', () => {
32
+ it('names the experienceTemplate in the summary when present', () => {
33
33
  const plan = {
34
34
  viewports: [],
35
35
  nodes: [],
36
- template: { templateId: 'page', props: { content: {}, design: {} } },
36
+ experienceTemplate: { experienceTemplateId: 'page', props: { content: {}, design: {} } },
37
37
  };
38
38
  const { container } = render(DebugExperience, { props: { experience: plan } });
39
- expect(container.innerHTML).toContain('template: page');
39
+ expect(container.innerHTML).toContain('experience template: page');
40
40
  });
41
41
  it('dumps the plan as pretty JSON', () => {
42
42
  const plan = {
@@ -44,7 +44,7 @@ describe('DebugExperience.svelte', () => {
44
44
  nodes: [node('button', { label: 'Go' })],
45
45
  };
46
46
  const { container } = render(DebugExperience, { props: { experience: plan } });
47
- expect(container.innerHTML).toContain('componentTypeId');
47
+ expect(container.innerHTML).toContain('componentId');
48
48
  expect(container.innerHTML).toContain('button');
49
49
  expect(container.innerHTML).toContain('Go');
50
50
  });
@@ -1,5 +1,5 @@
1
1
  <!--
2
- * Default fallback rendered when an instance references a component type
2
+ * Default fallback rendered when an instance references a component
3
3
  * not present in the Config.
4
4
  *
5
5
  * Behavior: a visible debug box when `debug` is on, silent null otherwise.
@@ -10,14 +10,14 @@
10
10
  import type { MissingComponentProps } from './component-props.js';
11
11
  import { getExperience } from './context.js';
12
12
 
13
- let { componentTypeId, nodeId }: MissingComponentProps = $props();
13
+ let { componentId, nodeId }: MissingComponentProps = $props();
14
14
  const experience = getExperience();
15
15
 
16
16
  $effect(() => {
17
17
  if (typeof console !== 'undefined') {
18
18
  const idLabel = nodeId ? ` (nodeId: ${nodeId})` : '';
19
19
  console.warn(
20
- `[@contentful/experiences] No component registered for type "${componentTypeId}"${idLabel}.`
20
+ `[@contentful/experiences] No component registered for id "${componentId}"${idLabel}.`
21
21
  );
22
22
  }
23
23
  });
@@ -26,15 +26,15 @@
26
26
  {#if experience.debug}
27
27
  <div
28
28
  style="border: 2px solid red; padding: 1rem; color: red; background: #fff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8125rem;"
29
- data-experiences-missing={componentTypeId}
29
+ data-experiences-missing={componentId}
30
30
  >
31
- <strong>Missing component &lsquo;{componentTypeId}&rsquo;</strong>
31
+ <strong>Missing component &lsquo;{componentId}&rsquo;</strong>
32
32
  <p style="margin: 0.5rem 0;">
33
33
  This component is referenced by the Experience payload but is not registered in the Config.
34
34
  Register it under this key in your <code>components</code> map:
35
35
  </p>
36
36
  <pre style="margin: 0; white-space: pre-wrap;">{JSON.stringify(
37
- { componentTypeId, nodeId: nodeId ?? null },
37
+ { componentId, nodeId: nodeId ?? null },
38
38
  null,
39
39
  2
40
40
  )}</pre>
@@ -23,16 +23,16 @@
23
23
  config: Config;
24
24
  experience: RenderContext;
25
25
  renderUnknown: RenderUnknown;
26
- children: Snippet;
26
+ children: Snippet[];
27
27
  }
28
28
 
29
29
  let { node, config, experience, renderUnknown, children }: NodeRendererProps = $props();
30
30
 
31
- const entry = $derived(config.components[node.registration.componentTypeId]);
31
+ const entry = $derived(config.components[node.registration.componentId]);
32
32
  const componentConfig = $derived(entry ? normalizeComponentRegistration(entry) : null);
33
33
 
34
34
  const contentful: ContentfulComponent = {
35
- componentTypeId: node.registration.componentTypeId,
35
+ componentId: node.registration.componentId,
36
36
  nodeId: node.nodeId,
37
37
  content: node.props.content,
38
38
  design: node.props.design,
@@ -52,7 +52,7 @@
52
52
  const { props, unresolved } = applyTokenResolver(resolvedDesign, config.resolveToken);
53
53
  if (unresolved.length && typeof console !== 'undefined') {
54
54
  console.warn(
55
- `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on "${node.registration.componentTypeId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
55
+ `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on "${node.registration.componentId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
56
56
  );
57
57
  }
58
58
  return props;
@@ -77,7 +77,7 @@
77
77
  {:else}
78
78
  {@const Unknown = renderUnknown}
79
79
  <Unknown
80
- componentTypeId={node.registration.componentTypeId}
80
+ componentId={node.registration.componentId}
81
81
  nodeId={node.nodeId}
82
82
  />
83
83
  {/if}
@@ -7,7 +7,7 @@ interface NodeRendererProps {
7
7
  config: Config;
8
8
  experience: RenderContext;
9
9
  renderUnknown: RenderUnknown;
10
- children: Snippet;
10
+ children: Snippet[];
11
11
  }
12
12
  declare const NodeRenderer: import("svelte").Component<NodeRendererProps, {}, "">;
13
13
  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,CAAC;CACnB;AAoEH,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;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;AAoEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,22 +1,17 @@
1
1
  <!--
2
2
  * Recursive renderer over PortableRenderNodes. Customer components receive
3
3
  * the merged content / design / resolveData prop bag plus the default
4
- * `children` slot as a named Snippet:
4
+ * `children` slot as an ARRAY of Snippets — one zero-arg Snippet per
5
+ * pre-rendered child node — declared as `children?: Snippet[]`. A component
6
+ * 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
+ * slot contract; see the README for the consuming-a-slot example.
5
9
  *
6
- * <script lang="ts">
7
- * import type { Snippet } from 'svelte';
8
- * let { text, children }: { text?: string; children?: Snippet } = $props();
9
- * </script>
10
- * <button>
11
- * {text}
12
- * {@render children?.()}
13
- * </button>
14
- *
15
- * Only the `children` slot is rendered automatically — Svelte 5 Snippets are
16
- * compile-time entities, so the renderer can't synthesize an unbounded set
17
- * of named Snippet props from runtime payload data. Additional named slots
18
- * are reachable through `getContentfulComponent().slots` and can be rendered
19
- * with the exported `<NodesRenderer />` (see README).
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).
20
15
  *
21
16
  * The Experience runtime context and the raw Contentful payload are read
22
17
  * via `getExperience` / `getContentfulComponent`.
@@ -25,10 +20,11 @@
25
20
  * the active viewport is sourced (initial seed vs reactive matchMedia).
26
21
  -->
27
22
  <script lang="ts">
23
+ import type { Snippet } from 'svelte';
24
+
28
25
  import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
29
26
 
30
27
  import NodeRenderer from './NodeRenderer.svelte';
31
- import NodesRenderer from './NodesRenderer.svelte';
32
28
  import type { RenderUnknown } from './component-props.js';
33
29
  import type { Config, RenderContext } from './types.js';
34
30
 
@@ -40,12 +36,24 @@
40
36
  }
41
37
 
42
38
  let { nodes, config, experience, renderUnknown }: NodesRendererProps = $props();
39
+
40
+ // 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.
45
+ function toChildSnippet(node: PortableRenderNode): Snippet {
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- programmatic snippet construction; see note above.
47
+ return ((anchor: unknown) => (renderNode as any)(anchor, () => node)) as Snippet;
48
+ }
43
49
  </script>
44
50
 
45
- {#each nodes as node, index (node.nodeId ?? index)}
46
- {#snippet children()}
47
- {@const childrenNodes = (node.slots.children ?? []) as PortableRenderNode[]}
48
- <NodesRenderer nodes={childrenNodes} {config} {experience} {renderUnknown} />
49
- {/snippet}
51
+ {#snippet renderNode(node: PortableRenderNode)}
52
+ {@const childNodes = (node.slots.children ?? []) as PortableRenderNode[]}
53
+ {@const children = childNodes.map(toChildSnippet)}
50
54
  <NodeRenderer {node} {config} {experience} {renderUnknown} {children} />
55
+ {/snippet}
56
+
57
+ {#each nodes as node, index (node.nodeId ?? index)}
58
+ {@render renderNode(node)}
51
59
  {/each}
@@ -1,5 +1,4 @@
1
1
  import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
2
- import NodesRenderer from './NodesRenderer.svelte';
3
2
  import type { RenderUnknown } from './component-props.js';
4
3
  import type { Config, RenderContext } from './types.js';
5
4
  interface NodesRendererProps {
@@ -1 +1 @@
1
- {"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,aAAa,MAAM,wBAAwB,CAAC;AACnD,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;AAwBH,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;AAqCH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -14,7 +14,7 @@
14
14
  import DebugExperience from './DebugExperience.svelte';
15
15
  import MissingComponent from './MissingComponent.svelte';
16
16
  import NodesRenderer from './NodesRenderer.svelte';
17
- import WrapWithTemplate from './WrapWithTemplate.svelte';
17
+ import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
18
18
  import type { ServerExperienceRendererProps } from './component-props.js';
19
19
  import { setExperience } from './context.js';
20
20
  import type { RenderContext } from './types.js';
@@ -62,7 +62,7 @@
62
62
  {#if debug}
63
63
  <DebugExperience {experience} />
64
64
  {/if}
65
- <WrapWithTemplate template={experience.template} {config} experience={renderContext}>
65
+ <WrapWithExperienceTemplate experienceTemplate={experience.experienceTemplate} {config} experience={renderContext}>
66
66
  {#snippet children()}
67
67
  <NodesRenderer
68
68
  nodes={experience.nodes}
@@ -71,5 +71,5 @@
71
71
  {renderUnknown}
72
72
  />
73
73
  {/snippet}
74
- </WrapWithTemplate>
74
+ </WrapWithExperienceTemplate>
75
75
  {/if}
@@ -0,0 +1,87 @@
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
+ import { applyTokenResolver, resolveDesignProperties } from '@contentful/experiences-design';
13
+
14
+ import { setContentfulExperienceTemplate, setResolvedDesign } from './context.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.design,
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 resolvedDesign = resolveDesignProperties(
48
+ experienceTemplate.props.design,
49
+ experience.viewports,
50
+ experience.activeViewportIndex
51
+ );
52
+ const { props, unresolved } = applyTokenResolver(resolvedDesign, config.resolveToken);
53
+ if (unresolved.length && typeof console !== 'undefined') {
54
+ console.warn(
55
+ `[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on experience template "${experienceTemplate.experienceTemplateId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
56
+ );
57
+ }
58
+ return props;
59
+ });
60
+
61
+ // A getter (not a snapshot) so getDesignValues() stays reactive; not merged into props.
62
+ setResolvedDesign(() => tokenResolvedDesign);
63
+
64
+ const composed = $derived.by(() => {
65
+ if (!experienceTemplate || !experienceTemplateConfig) return null;
66
+ return {
67
+ ...experienceTemplateConfig.defaults,
68
+ ...experienceTemplate.props.content,
69
+ ...experienceTemplate.props.resolved,
70
+ };
71
+ });
72
+
73
+ $effect(() => {
74
+ if (experienceTemplate && !experienceTemplateConfig && typeof console !== 'undefined') {
75
+ console.warn(
76
+ `[@contentful/experiences-svelte] No experience template registered for id "${experienceTemplate.experienceTemplateId}". Rendering nodes without the experience template wrapper.`
77
+ );
78
+ }
79
+ });
80
+ </script>
81
+
82
+ {#if experienceTemplate && experienceTemplateConfig && composed}
83
+ {@const Tpl = experienceTemplateConfig.component}
84
+ <Tpl {...composed} {children} />
85
+ {:else}
86
+ {@render children()}
87
+ {/if}
@@ -0,0 +1,13 @@
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
@@ -0,0 +1 @@
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;AA0EH,QAAA,MAAM,0BAA0B,qEAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}
@@ -20,7 +20,7 @@ export interface ServerExperienceRendererProps {
20
20
  }
21
21
  export type ClientExperienceRendererProps = ServerExperienceRendererProps;
22
22
  export interface MissingComponentProps {
23
- componentTypeId: string;
23
+ componentId: string;
24
24
  /** Optional — only present when the payload supplied an id for this node. */
25
25
  nodeId?: string;
26
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component-props.d.ts","sourceRoot":"","sources":["../src/component-props.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,MAAM,6BAA6B,GAAG,6BAA6B,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,qBAAqB,CAAC,CAAC;AAE7D,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,UAAU,EAAE,kBAAkB,CAAC;IAC/B,oEAAoE;IACpE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
1
+ {"version":3,"file":"component-props.d.ts","sourceRoot":"","sources":["../src/component-props.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,MAAM,6BAA6B,GAAG,6BAA6B,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,qBAAqB,CAAC,CAAC;AAE7D,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,UAAU,EAAE,kBAAkB,CAAC;IAC/B,oEAAoE;IACpE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
package/dist/context.d.ts CHANGED
@@ -1,19 +1,20 @@
1
- import type { ContentfulComponent, ContentfulTemplate, RenderContext } from './types.js';
1
+ import type { ContentfulComponent, ContentfulExperienceTemplate, RenderContext } from './types.js';
2
2
  export declare function setExperience(ctx: RenderContext): void;
3
3
  export declare function setContentfulComponent(node: ContentfulComponent): void;
4
- export declare function setContentfulTemplate(tpl: ContentfulTemplate): void;
4
+ export declare function setContentfulExperienceTemplate(tpl: ContentfulExperienceTemplate): void;
5
5
  /**
6
- * Publish the resolved design values for the enclosing node/template. Takes a
6
+ * Publish the resolved design values for the enclosing node or experience template. Takes a
7
7
  * getter (not a snapshot) so callers reading it inside a `$derived` stay
8
8
  * reactive across viewport changes.
9
9
  */
10
10
  export declare function setResolvedDesign(getDesign: () => Record<string, unknown>): void;
11
11
  export declare function getExperience(): RenderContext;
12
12
  export declare function getContentfulComponent(): ContentfulComponent | undefined;
13
- export declare function getContentfulTemplate(): ContentfulTemplate | undefined;
13
+ export declare function getContentfulExperienceTemplate(): ContentfulExperienceTemplate | undefined;
14
14
  /**
15
15
  * Read the design values the renderer resolved for the enclosing node or
16
- * template. Returns `undefined` outside a rendered node/template, which
16
+ * experience template. Returns `undefined` outside a rendered node or
17
+ * experience template, which
17
18
  * `getDesignValues` treats as "nothing to read."
18
19
  */
19
20
  export declare function getResolvedDesign(): Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAOzF,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEtD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAEtE;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEhF;AAED,wBAAgB,aAAa,IAAI,aAAa,CAQ7C;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,GAAG,SAAS,CAExE;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,GAAG,SAAS,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAGvE"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,4BAA4B,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AASnG,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEtD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAEtE;AAED,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,4BAA4B,GAAG,IAAI,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEhF;AAED,wBAAgB,aAAa,IAAI,aAAa,CAQ7C;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,GAAG,SAAS,CAExE;AAED,wBAAgB,+BAA+B,IAAI,4BAA4B,GAAG,SAAS,CAE1F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAGvE"}