@amplifyup/sdk 0.1.62 → 0.1.63
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/README.md +401 -12
- package/dist/{AmplifyRenderer-BQ8-H2H4.d.ts → AmplifyRenderer-Bwob7ICJ.d.ts} +23 -4
- package/dist/{AmplifyRenderer-W7eqltSj.d.mts → AmplifyRenderer-TawbUIKZ.d.mts} +23 -4
- package/dist/AmplifyRenderer.d.mts +1 -1
- package/dist/AmplifyRenderer.d.ts +1 -1
- package/dist/AmplifyRenderer.js +70 -0
- package/dist/AmplifyRenderer.js.map +1 -1
- package/dist/AmplifyRenderer.mjs +70 -0
- package/dist/AmplifyRenderer.mjs.map +1 -1
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -0
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +45 -11
- package/dist/react.d.ts +45 -11
- package/dist/react.js +310 -75
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +310 -76
- package/dist/react.mjs.map +1 -1
- package/dist/server.js +37 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +37 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,10 +61,10 @@ export default function Page() {
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
### 3.
|
|
64
|
+
### 3. Write a component
|
|
65
65
|
|
|
66
66
|
```tsx
|
|
67
|
-
import { Field
|
|
67
|
+
import { Field } from '@amplifyup/sdk/react';
|
|
68
68
|
import type { Fields } from '@amplifyup/sdk/react';
|
|
69
69
|
|
|
70
70
|
export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
|
|
@@ -78,13 +78,400 @@ export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
|
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
Connect content in Composer, then **Deploy** so the live site can serve the page.
|
|
82
|
+
|
|
83
|
+
That's the whole loop. The rest of this section is the detail.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Working with `fields`
|
|
88
|
+
|
|
89
|
+
Every component receives a `fields` prop. It is not raw CMS data — every entry is a **field envelope**:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
fields.heading
|
|
93
|
+
// → { value: 'Welcome to AmplifyUp', name: 'heading' }
|
|
94
|
+
|
|
95
|
+
fields.hero.image
|
|
96
|
+
// → { value: { url: '…', alt: '…' }, name: 'hero.image' }
|
|
97
|
+
|
|
98
|
+
fields.subheading // empty on a new page
|
|
99
|
+
// → { value: null, name: 'subheading' }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`name` is the field's dot path. It is what lets `<Field>` know which field it is bound to without you telling it. `value` is the content, typed from your schema.
|
|
103
|
+
|
|
104
|
+
**The shape is identical on the live site, in a draft, and in Composer.** There is no mode where you get a bare string instead of an envelope, and no mode where an empty field is `undefined` instead of `{ value: null, name }`.
|
|
105
|
+
|
|
106
|
+
### Two ways to read a field
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// Editable — use an SDK component
|
|
110
|
+
<Field field={fields.heading} />
|
|
111
|
+
|
|
112
|
+
// Display only — read .value
|
|
113
|
+
<title>{fields.heading.value}</title>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use the component when the field should be clickable in Composer. Use `.value` when it shouldn't — meta tags, `aria-label`, an `href`, a condition, anything that isn't visible text.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
export function Card({ fields }: { fields: Fields<{ title: string; link: string; image: ImageValue }> }) {
|
|
120
|
+
return (
|
|
121
|
+
<a href={fields.link.value ?? '#'} aria-label={fields.title.value ?? undefined}>
|
|
122
|
+
<Image field={fields.image} />
|
|
123
|
+
<h3><Field field={fields.title} /></h3>
|
|
124
|
+
</a>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### SDK field components
|
|
130
|
+
|
|
131
|
+
| Component | Field type | Renders |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `<Field>` | `string`, `number` | text node — no wrapper element |
|
|
134
|
+
| `<RichText>` | markdown / rich text | rendered HTML |
|
|
135
|
+
| `<Image>` | `ImageValue` | `<img>` |
|
|
136
|
+
|
|
137
|
+
All three take a `field` prop and read `name` from it. Pass any extra props (`className`, `loading`, etc.) and they go on the rendered element.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Field field={fields.eyebrow} className="text-xs uppercase" />
|
|
141
|
+
<RichText field={fields.body} className="prose" />
|
|
142
|
+
<Image field={fields.cover} className="rounded-lg" loading="lazy" />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`<Field>` is scalar-only. Passing a list is a type error:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// ✗ Field<string[]> is not assignable to Field<string | number>
|
|
149
|
+
<Field field={fields.tags} />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Empty fields
|
|
153
|
+
|
|
154
|
+
An empty field renders **nothing** on the live site and a clickable placeholder in Composer. The SDK never invents default copy.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// A new page, `subheading` not yet filled in:
|
|
158
|
+
|
|
159
|
+
<Field field={fields.subheading} />
|
|
160
|
+
// live: (nothing)
|
|
161
|
+
// Composer: clickable empty placeholder
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If you want a default, it's yours to write, and it reads from `.value`:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// ✓ your default, in your code
|
|
168
|
+
<p>{fields.subheading.value ?? 'Built for teams that ship.'}</p>
|
|
169
|
+
|
|
170
|
+
// ✓ hide the whole component when the key field is empty
|
|
171
|
+
export function Banner({ fields }: { fields: Fields<{ message: string }> }) {
|
|
172
|
+
if (!fields.message.value) return null;
|
|
173
|
+
return <div className="banner"><Field field={fields.message} /></div>;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Why no `fallback` prop: a fallback turns "this field was never populated" into "this field says *Welcome*". Nobody notices until a customer does. Empty should look empty.
|
|
178
|
+
|
|
179
|
+
### Wrapper elements
|
|
180
|
+
|
|
181
|
+
`<Field>` renders a text node, not an element, so you supply the tag:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// ✓
|
|
185
|
+
<h1><Field field={fields.heading} /></h1>
|
|
186
|
+
<p className="lead"><Field field={fields.intro} /></p>
|
|
187
|
+
|
|
188
|
+
// ✗ — Field has no `as` prop; put the element around it
|
|
189
|
+
<Field as="h1" field={fields.heading} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If the wrapper should disappear when the field is empty, check `.value`:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
{fields.intro.value ? (
|
|
196
|
+
<p className="lead"><Field field={fields.intro} /></p>
|
|
197
|
+
) : null}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Computed values
|
|
201
|
+
|
|
202
|
+
Occasionally what you display isn't a field — it's derived from one. Then you pass `value` and must also pass `name` so Composer knows which field a click edits:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// Show a formatted price but edit the raw number
|
|
206
|
+
<Field
|
|
207
|
+
value={formatPrice(fields.price.value)}
|
|
208
|
+
name={fields.price.name}
|
|
209
|
+
/>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`name` is only accepted alongside `value`. This is the one place you write it.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// ✗ — redundant, and a type error
|
|
216
|
+
<Field field={fields.price} name="price" />
|
|
217
|
+
|
|
218
|
+
// ✗ — value without name has nothing to bind to
|
|
219
|
+
<Field value={formatPrice(fields.price.value)} />
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Rendering differently in Composer
|
|
223
|
+
|
|
224
|
+
Most components render the same everywhere. Sometimes the live markup can't host an editable field — the text is a CSS background, an SVG `<title>`, a `<meta>` tag, an attribute, or it's behind interaction (a closed accordion, an auto-playing carousel). In Composer you still need something the author can click.
|
|
225
|
+
|
|
226
|
+
`useInComposer()` tells you which mode you're in:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { Field, Image, useInComposer } from '@amplifyup/sdk/react';
|
|
230
|
+
|
|
231
|
+
export function HeroBackground({ fields }: { fields: Fields<{ heading: string; image: ImageValue }> }) {
|
|
232
|
+
const inComposer = useInComposer();
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<section
|
|
236
|
+
className="hero"
|
|
237
|
+
style={{ backgroundImage: `url(${fields.image.value?.url ?? ''})` }}
|
|
238
|
+
>
|
|
239
|
+
{/* Live: image is a CSS background. Composer: also render it so it's clickable. */}
|
|
240
|
+
{inComposer ? <Image field={fields.image} className="hidden" /> : null}
|
|
241
|
+
<h1><Field field={fields.heading} /></h1>
|
|
242
|
+
</section>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Other common cases:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// Attribute on the live site, editable in Composer
|
|
251
|
+
const inComposer = useInComposer();
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
<button aria-label={fields.label.value ?? undefined}>
|
|
255
|
+
<Icon />
|
|
256
|
+
{inComposer ? <Field field={fields.label} /> : null}
|
|
257
|
+
</button>
|
|
258
|
+
</>
|
|
259
|
+
);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// Component that hides itself when empty on live, but stays visible for authors
|
|
264
|
+
export function Announcement({ fields }: { fields: Fields<{ message: string }> }) {
|
|
265
|
+
const inComposer = useInComposer();
|
|
266
|
+
if (!fields.message.value && !inComposer) return null;
|
|
267
|
+
return <div className="announcement"><Field field={fields.message} /></div>;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
// Interaction that fights the canvas — turn it off in Composer
|
|
273
|
+
export function Carousel({ fields }) {
|
|
274
|
+
const inComposer = useInComposer();
|
|
275
|
+
return (
|
|
276
|
+
<Collection field={fields.slides}>
|
|
277
|
+
{(slide) => <Slide slide={slide} autoplay={!inComposer} />}
|
|
278
|
+
</Collection>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Rules of thumb:
|
|
284
|
+
|
|
285
|
+
- The **field component is the source of truth in Composer.** If a field is visible to authors, it must be rendered through `<Field>` / `<RichText>` / `<Image>` / `<Collection>` in Composer mode, even if the live site reads `.value`.
|
|
286
|
+
- **Don't fork the whole component.** Branch the one element that differs, not the entire return. Two divergent trees drift and the preview stops matching production.
|
|
287
|
+
- **Empty gating uses `inComposer`.** `if (!x.value) return null` is correct for live and wrong for authors — they can't click what isn't rendered. Add `&& !inComposer`.
|
|
288
|
+
- **Don't use it to hide editable text on live.** If the text is visible on the live site, render it with the field component in both modes. `useInComposer` is for content the live markup genuinely can't expose as a field.
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
// ✗ — heading is visible on live; no reason to fork
|
|
292
|
+
{inComposer ? <Field field={fields.heading} /> : fields.heading.value}
|
|
293
|
+
|
|
294
|
+
// ✓
|
|
295
|
+
<Field field={fields.heading} />
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Lists
|
|
301
|
+
|
|
302
|
+
There are two kinds of list field and they are handled differently. Check your schema to know which you have.
|
|
303
|
+
|
|
304
|
+
| Shape | Schema | What it holds | In your component | Edited in Composer via |
|
|
305
|
+
| --- | --- | --- | --- | --- |
|
|
306
|
+
| **Reference list** | `array` + `entity` `of` | Linked entries — a query or a curated set of documents | Map `fields.posts.value` — plain rows | Props panel (pick, reorder) |
|
|
307
|
+
| **Repeater** | `array` + `of: 'json'` | Inline-authored items (slides, cards, FAQs) | `<Collection>` + `<Field>` on each item | Canvas (add, remove, reorder, click to edit) |
|
|
308
|
+
|
|
309
|
+
Scalar lists (tags, multi-select) are plain `.value` arrays edited in the props panel — same as reference lists.
|
|
310
|
+
|
|
311
|
+
### Reference lists
|
|
312
|
+
|
|
313
|
+
Rows are plain objects, not envelopes. Read them directly:
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
export function LatestPosts({ fields }: { fields: Fields<{ heading: string; posts: Post[] }> }) {
|
|
317
|
+
const posts = fields.posts.value ?? [];
|
|
318
|
+
return (
|
|
319
|
+
<section>
|
|
320
|
+
<h2><Field field={fields.heading} /></h2>
|
|
321
|
+
<ul>
|
|
322
|
+
{posts.map((post) => (
|
|
323
|
+
<li key={post.id}>
|
|
324
|
+
<img src={post.mainImage?.url} alt={post.mainImage?.alt ?? ''} />
|
|
325
|
+
<a href={`/blog/${post.slug}`}>{post.title}</a>
|
|
326
|
+
</li>
|
|
327
|
+
))}
|
|
328
|
+
</ul>
|
|
329
|
+
</section>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
// ✗ — reference rows aren't editable inline; there's nothing for Field to bind to
|
|
336
|
+
{posts.map((post) => <Field field={post.title} />)}
|
|
337
|
+
|
|
338
|
+
// ✗ — Collection is for repeaters
|
|
339
|
+
<Collection field={fields.posts}>…</Collection>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The list itself (which posts, in what order) is edited in the props panel. When the component is selected on the canvas, the component outline is the affordance — you don't need to add one.
|
|
343
|
+
|
|
344
|
+
### Repeaters
|
|
345
|
+
|
|
346
|
+
Items **are** enveloped. `fields.slides.value[0].title` is `{ value, name: 'slides.0.title' }`. Render through `<Collection>`:
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { Collection, Field, Image } from '@amplifyup/sdk/react';
|
|
350
|
+
|
|
351
|
+
export function Slideshow({
|
|
352
|
+
fields,
|
|
353
|
+
}: {
|
|
354
|
+
fields: Fields<{ slides: Array<{ title: string; caption: string; image: ImageValue }> }>;
|
|
355
|
+
}) {
|
|
356
|
+
return (
|
|
357
|
+
<Collection field={fields.slides}>
|
|
358
|
+
{(slide, i) => (
|
|
359
|
+
<figure className="slide" data-index={i}>
|
|
360
|
+
<Image field={slide.image} />
|
|
361
|
+
<figcaption>
|
|
362
|
+
<h2><Field field={slide.title} /></h2>
|
|
363
|
+
<Field field={slide.caption} className="text-sm" />
|
|
364
|
+
</figcaption>
|
|
365
|
+
</figure>
|
|
366
|
+
)}
|
|
367
|
+
</Collection>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
On the live site `Collection` renders your children and nothing else — no wrapper element. In Composer it adds an empty-state placeholder and add / remove / reorder controls.
|
|
373
|
+
|
|
374
|
+
Need a wrapping `<ul>`? Put it outside:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
<ul className="faq">
|
|
378
|
+
<Collection field={fields.questions}>
|
|
379
|
+
{(q) => (
|
|
380
|
+
<li>
|
|
381
|
+
<h3><Field field={q.question} /></h3>
|
|
382
|
+
<RichText field={q.answer} />
|
|
383
|
+
</li>
|
|
384
|
+
)}
|
|
385
|
+
</Collection>
|
|
386
|
+
</ul>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// ✗ — mapping .value skips the canvas chrome; items won't be addable in Composer
|
|
391
|
+
{fields.slides.value.map((slide) => <Field field={slide.title} />)}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Mixing a list with a runtime query
|
|
395
|
+
|
|
396
|
+
Search or "load more" results come from `queryContent`, which returns plain rows. Your reference list is also plain rows, so they share a renderer:
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
const posts = hits ?? fields.posts.value ?? [];
|
|
400
|
+
return <PostGrid posts={posts} />;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
No SDK component is involved on either path.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Slots
|
|
408
|
+
|
|
409
|
+
A slot is a region where authors drop other components. Render it with `<Slot>`:
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
import { Field, Slot } from '@amplifyup/sdk/react';
|
|
413
|
+
|
|
414
|
+
export function TwoColumn({ fields, slots }) {
|
|
415
|
+
return (
|
|
416
|
+
<div className="grid grid-cols-2">
|
|
417
|
+
<div><Slot name="left" slots={slots} /></div>
|
|
418
|
+
<div><Slot name="right" slots={slots} /></div>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Slots hold components, not fields — nothing in a slot is read through `fields`.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Do / Don't
|
|
429
|
+
|
|
430
|
+
| Do | Don't |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| `<Field field={fields.heading} />` | `<Field field={fields.heading} name="heading" />` |
|
|
433
|
+
| `{fields.heading.value}` for non-visible uses | `{fields.heading}` as a JSX child (it's an object) |
|
|
434
|
+
| `{fields.x.value ?? 'default'}` in your code | expect the SDK to supply default copy |
|
|
435
|
+
| `if (!fields.x.value) return null` to hide a component | wrap `Field` in conditional logic that hides the field in Composer |
|
|
436
|
+
| `<h1><Field … /></h1>` | look for an `as` / `wrapper` prop |
|
|
437
|
+
| `fields.posts.value.map(…)` for reference lists | `<Field>` or `<Collection>` on reference rows |
|
|
438
|
+
| `<Collection>` for repeaters | `fields.slides.value.map(…)` for repeaters |
|
|
439
|
+
| `value` + `name` together for computed output | `value` alone, or `name` alone |
|
|
440
|
+
| `<Image field={fields.cover} />` | `<img src={fields.cover.value.url} />` when it should be editable |
|
|
441
|
+
| `queryContent` rows as plain data | treat query rows as `fields` |
|
|
442
|
+
| `useInComposer()` to expose a field the live markup can't (CSS background, attribute, hidden panel) | fork the whole component's return on `inComposer` |
|
|
443
|
+
| `if (!x.value && !inComposer) return null` | `if (!x.value) return null` — authors can't click what isn't rendered |
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Types
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import type { Field, Fields, ImageValue } from '@amplifyup/sdk/react';
|
|
451
|
+
|
|
452
|
+
type Field<T> = { value: T | null; name: string };
|
|
453
|
+
|
|
454
|
+
type Fields<T> = { [K in keyof T]: /* Field<T[K]>, recursing into objects and repeater arrays */ };
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Declare the schema shape once on the component and let inference do the rest:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
type HeroFields = Fields<{
|
|
461
|
+
eyebrow: string;
|
|
462
|
+
heading: string;
|
|
463
|
+
body: string; // markdown → use <RichText>
|
|
464
|
+
image: ImageValue; // → use <Image>
|
|
465
|
+
ctas: Array<{ label: string; href: string }>; // repeater
|
|
466
|
+
related: Post[]; // reference list — rows are plain Post
|
|
467
|
+
}>;
|
|
468
|
+
|
|
469
|
+
export function Hero({ fields }: { fields: HeroFields }) { … }
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`fields.heading.value` is `string | null`. `fields.image.value` is `ImageValue | null`. `fields.ctas.value[0].label` is `Field<string>`. `fields.related.value[0]` is `Post`.
|
|
473
|
+
|
|
474
|
+
---
|
|
88
475
|
|
|
89
476
|
## Content sources
|
|
90
477
|
|
|
@@ -95,6 +482,8 @@ site; in Composer it stays clickable. Default copy belongs in your component
|
|
|
95
482
|
| Composer preview | Orchestrator (Control Plane) layout provider | `?preview=true` / Composer iframe |
|
|
96
483
|
| Runtime queries | Edge `/v1/query` | `queryContent` — search, load more, pickers |
|
|
97
484
|
|
|
485
|
+
Field envelopes are attached once, when the SDK reads the Edge response. Edge's JSON is raw; you never see it that way if you go through `AmplifyPageContent` or `fetchPageConfigServer`. If you call `fetchEdgeResolve` directly you get raw props and must map them yourself — prefer the documented path.
|
|
486
|
+
|
|
98
487
|
## Pattern pages (From page)
|
|
99
488
|
|
|
100
489
|
Components using Composer **From page** require `pageContext` on the provider (or `init`) so the first paint and first page view include fields. Late `setPageContext` is fine for subsequent events, not for rendering those components.
|
|
@@ -116,7 +505,7 @@ Components using Composer **From page** require `pageContext` on the provider (o
|
|
|
116
505
|
|
|
117
506
|
Pass `pageContext` at provider/init for From-page components. Do not wait until after first paint. Fixed (static) page fields still attach to events when `pageContext` is omitted.
|
|
118
507
|
|
|
119
|
-
The SDK maps Edge
|
|
508
|
+
The SDK maps Edge's resolved tree into `layoutTree` and renders it. Edge already applies personalization and CMS projection — the site does not call CMS or Decision APIs for layout. After paint, the overlay calls `POST /v1/select` (or `selectVariants()`) then `/v1/resolve?variants=` so decisions stay on the edge (`clientPersonalization`).
|
|
120
509
|
|
|
121
510
|
## Runtime queries
|
|
122
511
|
|
|
@@ -143,7 +532,7 @@ const hits = await queryContent({
|
|
|
143
532
|
});
|
|
144
533
|
```
|
|
145
534
|
|
|
146
|
-
Only entities published with that route can be queried, and visitors only see published content. Results are plain rows
|
|
535
|
+
Only entities published with that route can be queried, and visitors only see published content. Results are plain rows — not `fields` envelopes, not canvas-editable. Also available from `@amplifyup/sdk/server`. See the [runtime queries guide](../../docs/sdk-site-setup.md#8-runtime-content-queries).
|
|
147
536
|
|
|
148
537
|
## Track events
|
|
149
538
|
|
|
@@ -167,4 +556,4 @@ React context: `useAmplifyUp()` → `{ pageConfig, loading, error, source, meta
|
|
|
167
556
|
|
|
168
557
|
## License
|
|
169
558
|
|
|
170
|
-
MIT
|
|
559
|
+
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import { P as PageConfig } from './types-Db_eLrfU.js';
|
|
@@ -19,11 +19,30 @@ type Field<T> = {
|
|
|
19
19
|
value: T;
|
|
20
20
|
name: string;
|
|
21
21
|
};
|
|
22
|
+
/** Portable entry ref after normalize (`id` required). */
|
|
23
|
+
type PortableRef = {
|
|
24
|
+
id: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Repeater envelope: each item is a `Fields` bag.
|
|
29
|
+
* Paths are dotted indexes (`slides.0.title`).
|
|
30
|
+
*/
|
|
31
|
+
type CollectionField<T extends Record<string, unknown>> = {
|
|
32
|
+
value: Array<Fields<T>>;
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
22
35
|
type IsPlainObject<T> = T extends object ? T extends readonly unknown[] ? false : T extends ImageValue ? false : true : false;
|
|
36
|
+
/** Resolved / authored entry refs carry `id` (see `stampPlatformIdentity`). */
|
|
37
|
+
type IsEntryRefItem<T> = T extends {
|
|
38
|
+
id: string;
|
|
39
|
+
} ? true : false;
|
|
40
|
+
type AsRecord<T> = T extends Record<string, unknown> ? T : Record<string, unknown>;
|
|
41
|
+
type FieldsProp<T> = NonNullable<T> extends ImageValue ? Field<T | null> : NonNullable<T> extends readonly (infer U)[] ? IsEntryRefItem<U> extends true ? Field<T> : IsPlainObject<U> extends true ? CollectionField<AsRecord<U>> : Field<T> : IsPlainObject<NonNullable<T>> extends true ? Fields<NonNullable<T>> : Field<T>;
|
|
23
42
|
type Fields<T> = {
|
|
24
43
|
readonly [FIELDS_BRAND]: true;
|
|
25
44
|
} & {
|
|
26
|
-
[K in keyof T]-?:
|
|
45
|
+
[K in keyof T]-?: FieldsProp<T[K]>;
|
|
27
46
|
};
|
|
28
47
|
/** Props passed into site components. Raw CMS bags are not assignable. */
|
|
29
48
|
type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -69,6 +88,6 @@ interface AmplifyRendererProps {
|
|
|
69
88
|
composerPreview?: boolean;
|
|
70
89
|
}
|
|
71
90
|
declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
72
|
-
declare const AmplifyRenderer:
|
|
91
|
+
declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
|
|
73
92
|
|
|
74
|
-
export { AmplifyRenderer as A, type Field as F, type ImageValue as I, type LayoutComponentProps as L,
|
|
93
|
+
export { AmplifyRenderer as A, type CollectionField as C, type Field as F, type ImageValue as I, type LayoutComponentProps as L, type PortableRef as P, type Fields as a, isBrandedFields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import { P as PageConfig } from './types-Db_eLrfU.mjs';
|
|
@@ -19,11 +19,30 @@ type Field<T> = {
|
|
|
19
19
|
value: T;
|
|
20
20
|
name: string;
|
|
21
21
|
};
|
|
22
|
+
/** Portable entry ref after normalize (`id` required). */
|
|
23
|
+
type PortableRef = {
|
|
24
|
+
id: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Repeater envelope: each item is a `Fields` bag.
|
|
29
|
+
* Paths are dotted indexes (`slides.0.title`).
|
|
30
|
+
*/
|
|
31
|
+
type CollectionField<T extends Record<string, unknown>> = {
|
|
32
|
+
value: Array<Fields<T>>;
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
22
35
|
type IsPlainObject<T> = T extends object ? T extends readonly unknown[] ? false : T extends ImageValue ? false : true : false;
|
|
36
|
+
/** Resolved / authored entry refs carry `id` (see `stampPlatformIdentity`). */
|
|
37
|
+
type IsEntryRefItem<T> = T extends {
|
|
38
|
+
id: string;
|
|
39
|
+
} ? true : false;
|
|
40
|
+
type AsRecord<T> = T extends Record<string, unknown> ? T : Record<string, unknown>;
|
|
41
|
+
type FieldsProp<T> = NonNullable<T> extends ImageValue ? Field<T | null> : NonNullable<T> extends readonly (infer U)[] ? IsEntryRefItem<U> extends true ? Field<T> : IsPlainObject<U> extends true ? CollectionField<AsRecord<U>> : Field<T> : IsPlainObject<NonNullable<T>> extends true ? Fields<NonNullable<T>> : Field<T>;
|
|
23
42
|
type Fields<T> = {
|
|
24
43
|
readonly [FIELDS_BRAND]: true;
|
|
25
44
|
} & {
|
|
26
|
-
[K in keyof T]-?:
|
|
45
|
+
[K in keyof T]-?: FieldsProp<T[K]>;
|
|
27
46
|
};
|
|
28
47
|
/** Props passed into site components. Raw CMS bags are not assignable. */
|
|
29
48
|
type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -69,6 +88,6 @@ interface AmplifyRendererProps {
|
|
|
69
88
|
composerPreview?: boolean;
|
|
70
89
|
}
|
|
71
90
|
declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
72
|
-
declare const AmplifyRenderer:
|
|
91
|
+
declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
|
|
73
92
|
|
|
74
|
-
export { AmplifyRenderer as A, type Field as F, type ImageValue as I, type LayoutComponentProps as L,
|
|
93
|
+
export { AmplifyRenderer as A, type CollectionField as C, type Field as F, type ImageValue as I, type LayoutComponentProps as L, type PortableRef as P, type Fields as a, isBrandedFields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
|
package/dist/AmplifyRenderer.js
CHANGED
|
@@ -430,6 +430,21 @@ function isPortableImageShape(value) {
|
|
|
430
430
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
431
431
|
return "url" in value;
|
|
432
432
|
}
|
|
433
|
+
function isEntryRefItem(value) {
|
|
434
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
435
|
+
const rec = value;
|
|
436
|
+
return typeof rec.id === "string" && rec.id.trim().length > 0 || typeof rec._id === "string" && rec._id.trim().length > 0 || typeof rec.docId === "string" && rec.docId.trim().length > 0;
|
|
437
|
+
}
|
|
438
|
+
function isRepeaterArray(value) {
|
|
439
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
440
|
+
const sample = value.find((item) => item != null);
|
|
441
|
+
if (sample === void 0) return false;
|
|
442
|
+
if (typeof sample !== "object" || Array.isArray(sample)) return false;
|
|
443
|
+
if (isPortableImageShape(sample)) return false;
|
|
444
|
+
if (isEntryRefItem(sample)) return false;
|
|
445
|
+
if (sample.$$typeof) return false;
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
433
448
|
function joinPath(prefix, key) {
|
|
434
449
|
return prefix ? `${prefix}.${key}` : key;
|
|
435
450
|
}
|
|
@@ -446,9 +461,21 @@ function toFieldEnvelopes(source, prefix = "") {
|
|
|
446
461
|
for (const [key, raw] of Object.entries(input)) {
|
|
447
462
|
const path = joinPath(prefix, key);
|
|
448
463
|
if (isFieldEnvelope(raw)) {
|
|
464
|
+
const inner = raw.value;
|
|
465
|
+
if (isRepeaterArray(inner)) {
|
|
466
|
+
out[key] = {
|
|
467
|
+
value: envelopeRepeaterItems(inner, raw.name || path),
|
|
468
|
+
name: raw.name || path
|
|
469
|
+
};
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
449
472
|
out[key] = { value: raw.value ?? null, name: raw.name || path };
|
|
450
473
|
continue;
|
|
451
474
|
}
|
|
475
|
+
if (isRepeaterArray(raw)) {
|
|
476
|
+
out[key] = { value: envelopeRepeaterItems(raw, path), name: path };
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
452
479
|
if (raw && typeof raw === "object" && !Array.isArray(raw) && !isPortableImageShape(raw) && !raw.$$typeof) {
|
|
453
480
|
out[key] = toFieldEnvelopes(raw, path);
|
|
454
481
|
continue;
|
|
@@ -457,6 +484,16 @@ function toFieldEnvelopes(source, prefix = "") {
|
|
|
457
484
|
}
|
|
458
485
|
return brandFields(out);
|
|
459
486
|
}
|
|
487
|
+
function envelopeRepeaterItems(items, prefix) {
|
|
488
|
+
return items.map((item, i) => {
|
|
489
|
+
const path = joinPath(prefix, String(i));
|
|
490
|
+
if (isBrandedFields(item)) return item;
|
|
491
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
492
|
+
return toFieldEnvelopes(item, path);
|
|
493
|
+
}
|
|
494
|
+
return toFieldEnvelopes({}, path);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
460
497
|
function attachFieldEnvelopes(props) {
|
|
461
498
|
const raw = props && typeof props === "object" ? { ...props } : {};
|
|
462
499
|
if (isBrandedFields(raw.fields)) return raw;
|
|
@@ -483,6 +520,14 @@ function envelopePageConfig(page) {
|
|
|
483
520
|
}
|
|
484
521
|
function collectFieldPatches(props) {
|
|
485
522
|
const out = [];
|
|
523
|
+
const walkRepeater = (items, prefix) => {
|
|
524
|
+
items.forEach((item, i) => {
|
|
525
|
+
const itemPath = joinPath(prefix, String(i));
|
|
526
|
+
if (isBrandedFields(item) || item && typeof item === "object" && !Array.isArray(item) && !isFieldEnvelope(item)) {
|
|
527
|
+
walk(item, itemPath);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
};
|
|
486
531
|
const walk = (obj, prefix) => {
|
|
487
532
|
for (const [key, raw] of Object.entries(obj)) {
|
|
488
533
|
if (key === "fields" && raw && typeof raw === "object" && !Array.isArray(raw) && !isFieldEnvelope(raw)) {
|
|
@@ -491,9 +536,17 @@ function collectFieldPatches(props) {
|
|
|
491
536
|
}
|
|
492
537
|
const path = joinPath(prefix, key);
|
|
493
538
|
if (isFieldEnvelope(raw)) {
|
|
539
|
+
if (isRepeaterArray(raw.value)) {
|
|
540
|
+
walkRepeater(raw.value, raw.name || path);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
494
543
|
out.push({ name: raw.name || path, value: raw.value });
|
|
495
544
|
continue;
|
|
496
545
|
}
|
|
546
|
+
if (isRepeaterArray(raw)) {
|
|
547
|
+
walkRepeater(raw, path);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
497
550
|
if (raw && typeof raw === "object" && !Array.isArray(raw) && !isPortableImageShape(raw) && !raw.$$typeof) {
|
|
498
551
|
walk(raw, path);
|
|
499
552
|
continue;
|
|
@@ -3234,6 +3287,16 @@ var ComposerFieldStore = class {
|
|
|
3234
3287
|
if (set.size === 0) this.listeners.delete(name);
|
|
3235
3288
|
};
|
|
3236
3289
|
}
|
|
3290
|
+
keys() {
|
|
3291
|
+
return [...this.values.keys()];
|
|
3292
|
+
}
|
|
3293
|
+
delete(name) {
|
|
3294
|
+
if (!name || !this.values.has(name)) return;
|
|
3295
|
+
this.values.delete(name);
|
|
3296
|
+
const set = this.listeners.get(name);
|
|
3297
|
+
if (!set) return;
|
|
3298
|
+
for (const fn of set) fn();
|
|
3299
|
+
}
|
|
3237
3300
|
};
|
|
3238
3301
|
var ComposerStoreContext = react.createContext(null);
|
|
3239
3302
|
function matchesNode(eventNodeId, layoutNodeId) {
|
|
@@ -3254,6 +3317,13 @@ function ComposerProvider({
|
|
|
3254
3317
|
if (!fieldName || typeof fieldName !== "string") return;
|
|
3255
3318
|
if (!matchesNode(nodeId, layoutNodeId)) return;
|
|
3256
3319
|
store.set(fieldName, value);
|
|
3320
|
+
if (value && typeof value === "object") {
|
|
3321
|
+
for (const patch of collectFieldPatches(
|
|
3322
|
+
toFieldEnvelopes({ [fieldName]: value })
|
|
3323
|
+
)) {
|
|
3324
|
+
store.set(patch.name, patch.value);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3257
3327
|
};
|
|
3258
3328
|
const applyProps = (nextProps, nodeId) => {
|
|
3259
3329
|
if (!nextProps || typeof nextProps !== "object" || Array.isArray(nextProps)) {
|