@amplifyup/sdk 0.1.61 → 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 +404 -10
- package/dist/AmplifyRenderer-Bwob7ICJ.d.ts +93 -0
- package/dist/AmplifyRenderer-TawbUIKZ.d.mts +93 -0
- package/dist/AmplifyRenderer.d.mts +4 -40
- package/dist/AmplifyRenderer.d.ts +4 -40
- package/dist/AmplifyRenderer.js +327 -30
- package/dist/AmplifyRenderer.js.map +1 -1
- package/dist/AmplifyRenderer.mjs +328 -31
- package/dist/AmplifyRenderer.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +135 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +135 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{query-BqApXxQi.d.ts → query-3AXnQgzO.d.ts} +1 -0
- package/dist/{query-DaJlqU-Z.d.mts → query-DIZjc7st.d.mts} +1 -0
- package/dist/react.d.mts +74 -42
- package/dist/react.d.ts +74 -42
- package/dist/react.js +1613 -1253
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1690 -1334
- package/dist/react.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +134 -2
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +134 -2
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,25 +61,417 @@ 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
|
+
import type { Fields } from '@amplifyup/sdk/react';
|
|
68
69
|
|
|
69
|
-
export function Hero({
|
|
70
|
+
export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
|
|
70
71
|
return (
|
|
71
72
|
<section>
|
|
72
73
|
<h1>
|
|
73
|
-
<Field
|
|
74
|
+
<Field field={fields.heading} />
|
|
74
75
|
</h1>
|
|
75
76
|
</section>
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
---
|
|
83
475
|
|
|
84
476
|
## Content sources
|
|
85
477
|
|
|
@@ -90,6 +482,8 @@ then **Deploy** so Edge can serve the page.
|
|
|
90
482
|
| Composer preview | Orchestrator (Control Plane) layout provider | `?preview=true` / Composer iframe |
|
|
91
483
|
| Runtime queries | Edge `/v1/query` | `queryContent` — search, load more, pickers |
|
|
92
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
|
+
|
|
93
487
|
## Pattern pages (From page)
|
|
94
488
|
|
|
95
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.
|
|
@@ -111,7 +505,7 @@ Components using Composer **From page** require `pageContext` on the provider (o
|
|
|
111
505
|
|
|
112
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.
|
|
113
507
|
|
|
114
|
-
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`).
|
|
115
509
|
|
|
116
510
|
## Runtime queries
|
|
117
511
|
|
|
@@ -138,7 +532,7 @@ const hits = await queryContent({
|
|
|
138
532
|
});
|
|
139
533
|
```
|
|
140
534
|
|
|
141
|
-
Only entities published with that route can be queried, and visitors only see published content. Also available from `@amplifyup/sdk/server`. See the [runtime queries guide](../../docs/sdk-site-setup.md#8-runtime-content-queries).
|
|
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).
|
|
142
536
|
|
|
143
537
|
## Track events
|
|
144
538
|
|
|
@@ -162,4 +556,4 @@ React context: `useAmplifyUp()` → `{ pageConfig, loading, error, source, meta
|
|
|
162
556
|
|
|
163
557
|
## License
|
|
164
558
|
|
|
165
|
-
MIT
|
|
559
|
+
MIT
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { P as PageConfig } from './types-Db_eLrfU.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Field envelopes — same shape in published, draft, and Composer edit.
|
|
8
|
+
* `{ value, name }` where `name` is the full dot path (`hero.image`).
|
|
9
|
+
* Produced once at the Edge resolve boundary (`fetchEdgeResolve`).
|
|
10
|
+
*/
|
|
11
|
+
declare const FIELDS_BRAND: unique symbol;
|
|
12
|
+
type ImageValue = {
|
|
13
|
+
url: string;
|
|
14
|
+
alt?: string;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
};
|
|
18
|
+
type Field<T> = {
|
|
19
|
+
value: T;
|
|
20
|
+
name: string;
|
|
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
|
+
};
|
|
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>;
|
|
42
|
+
type Fields<T> = {
|
|
43
|
+
readonly [FIELDS_BRAND]: true;
|
|
44
|
+
} & {
|
|
45
|
+
[K in keyof T]-?: FieldsProp<T[K]>;
|
|
46
|
+
};
|
|
47
|
+
/** Props passed into site components. Raw CMS bags are not assignable. */
|
|
48
|
+
type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
49
|
+
fields: Fields<T>;
|
|
50
|
+
};
|
|
51
|
+
declare function isBrandedFields(value: unknown): value is Fields<Record<string, unknown>>;
|
|
52
|
+
declare function isFieldEnvelope(value: unknown): value is Field<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a props bag as envelopes. Already-enveloped bags are returned as-is.
|
|
55
|
+
* Nested plain objects become nested bags; portable images stay a leaf.
|
|
56
|
+
*/
|
|
57
|
+
declare function toFieldEnvelopes<T extends Record<string, unknown>>(source: T | null | undefined, prefix?: string): Fields<T>;
|
|
58
|
+
|
|
59
|
+
interface AmplifyRendererProps {
|
|
60
|
+
/**
|
|
61
|
+
* Component renderer function that maps componentId to React component
|
|
62
|
+
* This is site-specific and must be provided by the developer
|
|
63
|
+
*/
|
|
64
|
+
renderComponent: (componentId: string, props: LayoutComponentProps, slots?: Record<string, ReactNode>, context?: {
|
|
65
|
+
layoutNodeId: string;
|
|
66
|
+
}) => ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Optional page config for server-side rendering (SSR/SSG)
|
|
69
|
+
* If provided, the component will use this instead of fetching client-side
|
|
70
|
+
*/
|
|
71
|
+
pageConfig?: PageConfig;
|
|
72
|
+
/**
|
|
73
|
+
* Optional loading component
|
|
74
|
+
*/
|
|
75
|
+
loadingComponent?: ReactNode;
|
|
76
|
+
/**
|
|
77
|
+
* Optional error component
|
|
78
|
+
*/
|
|
79
|
+
errorComponent?: (error: string) => ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* Optional empty state component (when no content is configured)
|
|
82
|
+
*/
|
|
83
|
+
emptyComponent?: ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Force Composer page-slot wrapper (avoids SSR miss when window isn't available yet).
|
|
86
|
+
* When true, root components stay under [data-slot-source="page"] so hierarchy can climb to Page.
|
|
87
|
+
*/
|
|
88
|
+
composerPreview?: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
91
|
+
declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
|
|
92
|
+
|
|
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 };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { P as PageConfig } from './types-Db_eLrfU.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Field envelopes — same shape in published, draft, and Composer edit.
|
|
8
|
+
* `{ value, name }` where `name` is the full dot path (`hero.image`).
|
|
9
|
+
* Produced once at the Edge resolve boundary (`fetchEdgeResolve`).
|
|
10
|
+
*/
|
|
11
|
+
declare const FIELDS_BRAND: unique symbol;
|
|
12
|
+
type ImageValue = {
|
|
13
|
+
url: string;
|
|
14
|
+
alt?: string;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
};
|
|
18
|
+
type Field<T> = {
|
|
19
|
+
value: T;
|
|
20
|
+
name: string;
|
|
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
|
+
};
|
|
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>;
|
|
42
|
+
type Fields<T> = {
|
|
43
|
+
readonly [FIELDS_BRAND]: true;
|
|
44
|
+
} & {
|
|
45
|
+
[K in keyof T]-?: FieldsProp<T[K]>;
|
|
46
|
+
};
|
|
47
|
+
/** Props passed into site components. Raw CMS bags are not assignable. */
|
|
48
|
+
type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
49
|
+
fields: Fields<T>;
|
|
50
|
+
};
|
|
51
|
+
declare function isBrandedFields(value: unknown): value is Fields<Record<string, unknown>>;
|
|
52
|
+
declare function isFieldEnvelope(value: unknown): value is Field<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a props bag as envelopes. Already-enveloped bags are returned as-is.
|
|
55
|
+
* Nested plain objects become nested bags; portable images stay a leaf.
|
|
56
|
+
*/
|
|
57
|
+
declare function toFieldEnvelopes<T extends Record<string, unknown>>(source: T | null | undefined, prefix?: string): Fields<T>;
|
|
58
|
+
|
|
59
|
+
interface AmplifyRendererProps {
|
|
60
|
+
/**
|
|
61
|
+
* Component renderer function that maps componentId to React component
|
|
62
|
+
* This is site-specific and must be provided by the developer
|
|
63
|
+
*/
|
|
64
|
+
renderComponent: (componentId: string, props: LayoutComponentProps, slots?: Record<string, ReactNode>, context?: {
|
|
65
|
+
layoutNodeId: string;
|
|
66
|
+
}) => ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Optional page config for server-side rendering (SSR/SSG)
|
|
69
|
+
* If provided, the component will use this instead of fetching client-side
|
|
70
|
+
*/
|
|
71
|
+
pageConfig?: PageConfig;
|
|
72
|
+
/**
|
|
73
|
+
* Optional loading component
|
|
74
|
+
*/
|
|
75
|
+
loadingComponent?: ReactNode;
|
|
76
|
+
/**
|
|
77
|
+
* Optional error component
|
|
78
|
+
*/
|
|
79
|
+
errorComponent?: (error: string) => ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* Optional empty state component (when no content is configured)
|
|
82
|
+
*/
|
|
83
|
+
emptyComponent?: ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Force Composer page-slot wrapper (avoids SSR miss when window isn't available yet).
|
|
86
|
+
* When true, root components stay under [data-slot-source="page"] so hierarchy can climb to Page.
|
|
87
|
+
*/
|
|
88
|
+
composerPreview?: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
91
|
+
declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
|
|
92
|
+
|
|
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,40 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
interface AmplifyRendererProps {
|
|
7
|
-
/**
|
|
8
|
-
* Component renderer function that maps componentId to React component
|
|
9
|
-
* This is site-specific and must be provided by the developer
|
|
10
|
-
*/
|
|
11
|
-
renderComponent: (componentId: string, props: Record<string, any>, slots?: Record<string, ReactNode>, context?: {
|
|
12
|
-
layoutNodeId: string;
|
|
13
|
-
}) => ReactNode;
|
|
14
|
-
/**
|
|
15
|
-
* Optional page config for server-side rendering (SSR/SSG)
|
|
16
|
-
* If provided, the component will use this instead of fetching client-side
|
|
17
|
-
*/
|
|
18
|
-
pageConfig?: PageConfig;
|
|
19
|
-
/**
|
|
20
|
-
* Optional loading component
|
|
21
|
-
*/
|
|
22
|
-
loadingComponent?: ReactNode;
|
|
23
|
-
/**
|
|
24
|
-
* Optional error component
|
|
25
|
-
*/
|
|
26
|
-
errorComponent?: (error: string) => ReactNode;
|
|
27
|
-
/**
|
|
28
|
-
* Optional empty state component (when no content is configured)
|
|
29
|
-
*/
|
|
30
|
-
emptyComponent?: ReactNode;
|
|
31
|
-
/**
|
|
32
|
-
* Force Composer page-slot wrapper (avoids SSR miss when window isn't available yet).
|
|
33
|
-
* When true, root components stay under [data-slot-source="page"] so hierarchy can climb to Page.
|
|
34
|
-
*/
|
|
35
|
-
composerPreview?: boolean;
|
|
36
|
-
}
|
|
37
|
-
declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
|
|
38
|
-
declare const AmplifyRenderer: React.MemoExoticComponent<typeof AmplifyRendererInner>;
|
|
39
|
-
|
|
40
|
-
export { AmplifyRenderer };
|
|
1
|
+
import 'react';
|
|
2
|
+
import 'react/jsx-runtime';
|
|
3
|
+
export { A as AmplifyRenderer } from './AmplifyRenderer-TawbUIKZ.mjs';
|
|
4
|
+
import './types-Db_eLrfU.mjs';
|