@elytracms/next 0.0.80 → 0.0.82

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/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
2
  import { ComponentRegistry, ComponentIssue, ComponentManifest } from '@elytracms/core/component-registry';
3
3
  export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
4
- import { ComponentImplementations, ResolveBinding, RelationTarget } from '@elytracms/core/runtime-renderer';
4
+ import { ComponentImplementations, ResolveBinding, RelationTarget, DocumentBodyItem } from '@elytracms/core/runtime-renderer';
5
5
  export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
6
6
  import { ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
7
7
  export { Perspective } from '@elytracms/core/content';
8
- import { LocaleConfig, Locale, RouteRecord, RedirectRecord, CollectionDef } from '@elytracms/core/cms-core';
9
- export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, mergeRouteRecords, redirectRecordSchema, routeRecordSchema } from '@elytracms/core/cms-core';
8
+ import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord } from '@elytracms/core/cms-core';
9
+ export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, routeRecordSchema, splitLocalePath } from '@elytracms/core/cms-core';
10
10
  import { ComponentNode } from '@elytracms/core/project-graph';
11
11
  export { ComponentNode } from '@elytracms/core/project-graph';
12
12
  import { AssetRecord, PersistenceAdapter } from '@elytracms/core/persistence';
@@ -169,6 +169,29 @@ interface CanvasRendererProps {
169
169
  * issue (EC-290), never a silent `#`.
170
170
  */
171
171
  links?: Record<string, string>;
172
+ /**
173
+ * The resolved document's collection-declared delivery presentation (EC-499,
174
+ * from the render outcome): which field(s) hold the body. Absent → the EC-458
175
+ * shape picker selects the body by VALUE, exactly as before.
176
+ */
177
+ presentation?: DeliveryPresentation;
178
+ }
179
+ /**
180
+ * A collection's `presentation` declaration (EC-499), resolved for delivery: the
181
+ * body field list normalized from `string | string[]`. Plain data — it rides the
182
+ * (serializable) render outcome and the RSC boundary like the baked
183
+ * assets/relations/links do.
184
+ *
185
+ * EC-524: this renderer applies no CHROME. It carried a `wrapper` component id
186
+ * briefly (never released); a detail wrapper is a page template, templates are
187
+ * code, and the host now supplies one from its own route file via
188
+ * `createCanvasRoute`'s `wrap` option. What stays here is content selection —
189
+ * which is exactly what makes a canvas body and a rich-text body render
190
+ * agnostically through the one entry point.
191
+ */
192
+ interface DeliveryPresentation {
193
+ /** Field names whose values form the body, in render order. */
194
+ body?: readonly string[];
172
195
  }
173
196
  /** Default payloads: the route-bound document and the dynamic route params. */
174
197
  declare function defaultBindingPayloads(result: ResolvePageResult): BindingPayloads;
@@ -178,6 +201,15 @@ declare function defaultBindingPayloads(result: ResolvePageResult): BindingPaylo
178
201
  * the `ContentSource`).
179
202
  */
180
203
  declare function CanvasRenderer(props: CanvasRendererProps): Promise<ReactNode>;
204
+ /**
205
+ * The ordered body items of a resolved document (EC-499): the values of the
206
+ * collection's declared `presentation.body` fields concatenated in declaration
207
+ * order, or — when the collection declares nothing — the EC-458 value-shape
208
+ * pick, unchanged. A declared field the document has no value for contributes
209
+ * nothing (an empty body is legitimate); a declared field holding something that
210
+ * is neither composition nor rich text contributes a visible fallback.
211
+ */
212
+ declare function bodyItemsOf(document: Record<string, unknown> | null | undefined, presentation?: DeliveryPresentation): DocumentBodyItem[];
181
213
 
182
214
  /**
183
215
  * The content seam of the embedded runtime (EC-144, vision AD-1/AD-4): the
@@ -224,6 +256,16 @@ interface ContentSource {
224
256
  * explicit routes.
225
257
  */
226
258
  readonly routes?: readonly RouteRecord[];
259
+ /**
260
+ * The project's collection definitions (EC-499, additive) — config-owned
261
+ * (AD-11), already prefetched by every source that reads a schema. The route
262
+ * helper reads the resolved document's collection from here to honour its
263
+ * declared `presentation` (which field(s) hold the delivery body, and the
264
+ * optional detail wrapper). Optional so existing custom sources stay valid;
265
+ * without it every collection is treated as undeclared and the EC-458 shape
266
+ * picker decides, exactly as before.
267
+ */
268
+ readonly collections?: readonly CollectionDef[];
227
269
  /**
228
270
  * Data-source definitions the graph's bindings reference (EC-166 delivery,
229
271
  * additive). When present, the route helper materializes per-section list
@@ -306,13 +348,36 @@ interface MaterializedSourcePayloads {
306
348
  */
307
349
  declare function materializeSourcePayloads(input: MaterializeSourcePayloadsInput): MaterializedSourcePayloads;
308
350
 
351
+ /**
352
+ * Bridge from a `PersistenceAdapter` to the synchronous `ContentLookup` the
353
+ * accessors (EC-143) read through: prefetch the project's collections,
354
+ * documents, assets, and graphs ONCE (async), then serve every accessor call
355
+ * synchronously from memory. This is how `apps/example-site` reads the seeded
356
+ * in-memory fixture project; a Convex-backed `ContentSource` (EC-145/146)
357
+ * replaces this bridge without touching the route helper.
358
+ *
359
+ * The assembly half is exposed separately as {@link createStaticContentSource}
360
+ * (EC-156): a host that already holds the prefetched data as plain values —
361
+ * e.g. a delivery-snapshot payload fetched over Convex's public HTTP query
362
+ * API — assembles the exact same `ContentSource` without faking an adapter.
363
+ */
309
364
  interface ContentSnapshotOptions {
310
365
  /** Route records served by `resolvePage` (EC-019). */
311
366
  routes?: readonly RouteRecord[];
312
367
  /** Redirect records served by `resolvePage` (EC-019/020). */
313
368
  redirects?: readonly RedirectRecord[];
314
- /** Locale configuration; defaults to English-only. */
315
- locales?: LocaleConfig;
369
+ /**
370
+ * Locale configuration — **required**, never defaulted (EC-530). The default
371
+ * locale is the storage key every localized value is written under, so a
372
+ * fabricated `en` is a data migration to undo, not a config edit: content
373
+ * authored under one locale and served under another, silently (EC-512
374
+ * measured exactly that). EC-490 made a config declaring no locales an error
375
+ * and EC-519 collapsed the two declarations into one; this was the last place
376
+ * in the chain still inventing the value, and it sits in the package that
377
+ * resolves the URLs. Hand it the project's `LocaleConfig` — the one the
378
+ * config declares.
379
+ */
380
+ locales: LocaleConfig;
316
381
  /**
317
382
  * Data-source definitions the graph's bindings reference (EC-166): section
318
383
  * sources (`cms.section.<nodeId>`) carry their stored where/sort/limit, and
@@ -332,8 +397,8 @@ interface ContentSnapshot extends ContentSource {
332
397
  */
333
398
  interface StaticContentData {
334
399
  projectId: string;
335
- /** Locale configuration; defaults to English-only. */
336
- locales?: LocaleConfig;
400
+ /** Locale configuration — **required**, never defaulted (see EC-530 above). */
401
+ locales: LocaleConfig;
337
402
  /** Route records served by `resolvePage` (EC-019). */
338
403
  routes?: readonly RouteRecord[];
339
404
  /** Redirect records served by `resolvePage` (EC-019/020). */
@@ -374,7 +439,7 @@ declare function createStaticContentSource(data: StaticContentData): ContentSnap
374
439
  * request (or memoize across requests for fixture-backed sites) — accessors
375
440
  * never reach back to the adapter.
376
441
  */
377
- declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options?: ContentSnapshotOptions): Promise<ContentSnapshot>;
442
+ declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options: ContentSnapshotOptions): Promise<ContentSnapshot>;
378
443
 
379
444
  /**
380
445
  * The pure half of the catch-all route helper (EC-144): URL → outcome, with
@@ -383,6 +448,43 @@ declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: s
383
448
  * each outcome onto `redirect()` / `permanentRedirect()` / `notFound()` /
384
449
  * `<CanvasRenderer />`.
385
450
  */
451
+ /** What the route helper tells a host callback about the request it is serving. */
452
+ interface CanvasRequestInfo {
453
+ /** The raw request path (including any locale prefix). */
454
+ url: string;
455
+ perspective: Perspective;
456
+ }
457
+ /**
458
+ * What a host's `wrap` function is told about the request it is wrapping
459
+ * (EC-524 — see `CanvasRouteOptions.wrap`).
460
+ *
461
+ * Every field is something the route already had in hand at the wrap point:
462
+ * nothing is gathered for this seam, and nothing was invented for it.
463
+ */
464
+ interface CanvasWrapContext extends CanvasRequestInfo {
465
+ /**
466
+ * The collection of the document this URL resolved to — **the branch key**.
467
+ * `undefined` when the resolution carried no document, so a host branching on
468
+ * a collection id naturally wraps nothing there (which is what the removed
469
+ * `presentation.wrapper` declaration did too: it only applied with a document).
470
+ */
471
+ collection?: string;
472
+ /**
473
+ * The resolved document. Verbatim the `document` prop the removed wrapper
474
+ * component received, so a wrapper ported to this seam needs no other source
475
+ * for its field-level chrome (the title, a byline, a date).
476
+ */
477
+ document: ResolvedDocument | null;
478
+ /**
479
+ * The whole resolution — the same value the `metadata` callback already
480
+ * receives, so this is the established host-callback argument on this route
481
+ * rather than a new surface. It carries `dynamicParams`, `route`,
482
+ * `localeFallback` and `issues` for a wrapper that needs more than the
483
+ * document (EC-499 recorded wanting exactly that and could not have it across
484
+ * a serializable declaration).
485
+ */
486
+ result: ResolvePageResult;
487
+ }
386
488
  /** What the host's payload factory sees for one request. */
387
489
  interface CanvasPayloadContext {
388
490
  /** Accessor client bound to this request's perspective + locale. */
@@ -454,19 +556,20 @@ type CanvasPageOutcome = {
454
556
  * embedding page's href.
455
557
  */
456
558
  links: Record<string, string>;
559
+ /**
560
+ * The resolved document's collection-declared delivery presentation
561
+ * (EC-499): which field(s) are the render body. Plain, serializable data —
562
+ * handed to `<CanvasRenderer />` so ONE catch-all renders every
563
+ * collection's body, whatever its shape. Absent when the collection
564
+ * declares nothing (or the source exposes no collections), and the EC-458
565
+ * value-shape picker then decides exactly as before.
566
+ */
567
+ presentation?: DeliveryPresentation;
457
568
  tags: CacheTags;
458
569
  };
459
570
  /** Build the request path from a Next catch-all `slug` param. */
460
571
  declare function pathFromSlug(slug: readonly string[] | undefined): string;
461
- /**
462
- * Derive the request locale from a leading URL segment (`/de/about` → locale
463
- * `de`, path `/about`) and return the remaining path. No prefix (or the
464
- * default locale's own content at root) falls back to the default locale.
465
- */
466
- declare function splitLocalePath(url: string, locales: LocaleConfig): {
467
- locale: Locale;
468
- path: string;
469
- };
572
+
470
573
  interface ResolveCanvasPageOptions {
471
574
  payloads?: CanvasPayloadsFactory;
472
575
  /**
@@ -487,28 +590,6 @@ interface ResolveCanvasPageOptions {
487
590
  */
488
591
  declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
489
592
 
490
- /**
491
- * Catch-all route helper (EC-144): the factory a host app calls once in
492
- * `app/[[...slug]]/page.tsx` to wire `resolvePage` → `<CanvasRenderer />`,
493
- * with redirects answered by Next's `redirect()`/`permanentRedirect()` (per
494
- * the route's `permanent` flag), unknown URLs answered by `notFound()` (the
495
- * host's 404 renders), and the perspective chosen by Next `draftMode()` —
496
- * draft when enabled (via the token-gated preview route), published
497
- * otherwise.
498
- *
499
- * With `cache` configured (EC-146), the published-perspective resolve+render
500
- * data work runs inside `unstable_cache`, keyed by URL and tagged with the
501
- * fetch-time tag set of what the response actually contained (see
502
- * `cache.ts`). Publishing in the studio then invalidates exactly the affected
503
- * entries via the signed `/api/revalidate` webhook — and while the content
504
- * backend is unreachable, cached entries keep serving because a cache hit
505
- * never invokes `loadContent`.
506
- */
507
- interface CanvasRequestInfo {
508
- /** The raw request path (including any locale prefix). */
509
- url: string;
510
- perspective: Perspective;
511
- }
512
593
  interface CanvasCacheOptions {
513
594
  /**
514
595
  * Project scope prefix for every tag (`p:<scope>:…`) — must be the project
@@ -562,6 +643,38 @@ interface CanvasRouteOptions {
562
643
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
563
644
  /** Override the generated `<head>` metadata for a resolved page. */
564
645
  metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
646
+ /**
647
+ * Wrap the rendered page in the host's own chrome (EC-524) — a detail
648
+ * template, a per-collection layout, a draft ribbon. Receives the rendered
649
+ * canvas as `children` and returns whatever should stand in its place:
650
+ *
651
+ * ```tsx
652
+ * elytra.canvasRoute({
653
+ * wrap: (children, ctx) =>
654
+ * ctx.collection === 'post' ? <PostDetail document={ctx.document}>{children}</PostDetail> : children,
655
+ * })
656
+ * ```
657
+ *
658
+ * **This is deliberately host code, not config** (owner ruling 2026-08-01):
659
+ * *"this sounds very much like page-templates … templates belong in code, so
660
+ * the wrapper is something the developer has to care about … the page-content
661
+ * (Canvas or Rich-Text) should render agnostically."* A collection's
662
+ * `presentation.body` still names WHICH field is the content — that is content
663
+ * selection, and it is what lets one catch-all render a canvas page and a
664
+ * rich-text post through the same call. What surrounds that content is the
665
+ * developer's implementation decision, branched here in their own file.
666
+ *
667
+ * It replaced a `presentation.wrapper` component-id STRING resolved from the
668
+ * canvas registry, which forced a delivery-only component to register as a
669
+ * pseudo-block (EC-514). A function beats that on every axis that matters: it
670
+ * cannot dangle, the compiler checks it, and it can branch on anything in
671
+ * {@link CanvasWrapContext} rather than on collection identity alone.
672
+ *
673
+ * Applied to the `render` outcome only — a redirect or a 404 never reaches it.
674
+ * Returning `children` unchanged is the no-op, and omitting `wrap` entirely is
675
+ * the default.
676
+ */
677
+ wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
565
678
  }
566
679
  interface CanvasRouteProps {
567
680
  params: Promise<{
@@ -601,6 +714,16 @@ declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly
601
714
  * documents (composing each page's nested path from its parent chain) — no
602
715
  * params provider entry needed.
603
716
  *
717
+ * **Enumeration is a candidate list, not the output (EC-486).** Every branch
718
+ * above — static, provider-fed, and mount — only proposes paths; each one is
719
+ * then put back through `resolvePage`, and a path the resolver refuses is
720
+ * dropped. So the sitemap cannot claim a URL the delivery host would 404, even
721
+ * though the mount branch enumerates its collection independently of route
722
+ * precedence. Do not read the enumeration in isolation: the gate below it is
723
+ * what makes the two agree. What a mount enumerated and the resolver then
724
+ * refused is reported in {@link CanvasSitemapResult.skipped}, never dropped in
725
+ * silence.
726
+ *
604
727
  * Host usage (`app/sitemap.ts`):
605
728
  *
606
729
  * ```ts
@@ -702,14 +825,14 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
702
825
  * keeps multi-project hosts (one Next app serving several builder projects)
703
826
  * from cross-invalidating, so the prefix is **on whenever the host knows its
704
827
  * project id** — decision: include it. The emitter (the studio's webhook
705
- * dispatcher, `apps/builder/src/lib/webhooks`) always scopes with the
828
+ * dispatcher, `packages/studio/src/lib/webhooks`) always scopes with the
706
829
  * operation's `projectId`; a host that omits `cache.scope` therefore will not
707
830
  * match studio-emitted tags. Configure `cache.scope` with the same project id
708
831
  * the studio publishes under.
709
832
  *
710
833
  * The studio-side emitter cannot depend on this package (it would drag the
711
- * `next` peer dependency into the builder), so
712
- * `apps/builder/src/lib/webhooks/tags.ts` mirrors these formatters; tests on
834
+ * `next` peer dependency into the studio bundle), so
835
+ * `packages/studio/src/lib/webhooks/tags.ts` mirrors these formatters; tests on
713
836
  * both sides pin the exact literal strings to keep them in lock-step.
714
837
  */
715
838
  /** Apply the optional multi-project scope prefix to one tag. */
@@ -784,14 +907,67 @@ interface CachedEntry<T> {
784
907
  * entry and the next request repeats the cycle with fresh data.
785
908
  *
786
909
  * The loader runs **once per revalidation cycle** (per key), never once per
787
- * request. Time-based revalidation is intentionally not exposed: a
788
- * stale-while-revalidate background refresh would re-run the probe callback
789
- * (whose bail aborts the refresh), so this design is tag-driven only —
790
- * `revalidate: false` — which is exactly the AD-5 model.
910
+ * request. Time-based revalidation is intentionally not exposed, and the reason
911
+ * is a real hazard rather than a preference: a stale-while-revalidate background
912
+ * refresh re-runs the probe callback, whose bail aborts the refresh, so the
913
+ * entry is never rewritten. `revalidate: false` keeps *time* from ever putting
914
+ * an entry into that state — which is the AD-5 model.
915
+ *
916
+ * **What `revalidate: false` does NOT buy (EC-515, measured on Next 16.2.10).**
917
+ * The earlier version of this comment claimed `revalidate: false` made the SWR
918
+ * hazard unreachable. It does not: a **tag** can put an entry into the stale
919
+ * state too, and then exactly the aborted-refresh loop above happens. Whether it
920
+ * does is decided by how the invalidator stamps the tag —
921
+ * `revalidateTag(tag, 'max')` marks entries stale (365-day expiry) and triggers
922
+ * the loop; `revalidateTag(tag, { expire: 0 })` drops them outright and does
923
+ * not. `revalidate.ts` therefore sends `{ expire: 0 }`, and the reasoning lives
924
+ * at that call site. Keep the two in step: this design only survives an
925
+ * invalidation that **drops** the entry, never one that merely staleness-marks
926
+ * it.
791
927
  *
792
928
  * Concurrency: wrappers and the captured closure are per-request, so there is
793
929
  * no cross-request mutation; two racing first requests both resolve and the
794
930
  * last write wins (deterministic content ⇒ identical entries).
931
+ *
932
+ * ## ⚠️ Self-hosted hosts: only the BASELINE tags invalidate (EC-515)
933
+ *
934
+ * **Measured on a self-hosted `next start` (Next 16.2.10, default
935
+ * `FileSystemCache`, `.next/cache` on disk); NOT measured on Vercel.**
936
+ *
937
+ * Fetch-time tag discovery writes the entry with the tags the response actually
938
+ * read — but the default self-hosted cache handler decides staleness and expiry
939
+ * from the tags the **reading** wrapper names (`ctx.tags`, see
940
+ * `IncrementalCache.get` / `FileSystemCache.get` for `kind: FETCH`), not from
941
+ * the tags the entry was **stored** with. The stored tag set is only ever used
942
+ * to decide whether to re-write the entry with a superset.
943
+ *
944
+ * Every read in {@link loadCachedEntry} goes through the **probe** wrapper,
945
+ * whose tags are `baselineTags`. So on such a host:
946
+ *
947
+ * - revalidating a tag that is in `baselineTags` works (the route helper passes
948
+ * the project sweep tag, so `project` sweeps do invalidate);
949
+ * - revalidating a *discovered* `doc:` / `collection:` / `route:` / `asset:` tag
950
+ * invalidates **nothing** — silently. No error, no log line, no loader run.
951
+ *
952
+ * Measured 2×2 that isolates it (same host, same run): a probe handed the full
953
+ * discovered tag set up front DOES honour a `doc:` tag, and the same probe
954
+ * handed only the baseline does not. So the tags, their formatting and the
955
+ * webhook are all correct — the read simply never consults them.
956
+ *
957
+ * Consequence for a self-hosted deployment: invalidation granularity collapses
958
+ * to whatever is in `baselineTags`. The studio's Convex write chokepoint emits
959
+ * the project sweep on every publish, so instant publish still *works* there,
960
+ * coarsely; the browser dispatcher's targeted `doc:`/`collection:` tag set
961
+ * (`packages/studio/src/lib/webhooks/tags.ts`) does not reach a self-hosted host
962
+ * at all. Vercel-hosted deployments are unaffected in practice — both consumer
963
+ * sites were measured healthy on 2026-08-01 — which is consistent with Vercel's
964
+ * cache handler purging by the entry's stored tags, though that has **not** been
965
+ * measured here.
966
+ *
967
+ * Fixing this properly means the read must be able to name the entry's own tags,
968
+ * which no `unstable_cache` API allows without restructuring the probe/bail
969
+ * seam. That is a deliberate open decision — see EC-515, do not "fix" it here
970
+ * incidentally.
795
971
  */
796
972
  declare function loadCachedEntry<T>(cache: CacheWrapper, keyParts: string[], baselineTags: readonly string[], load: () => Promise<CachedEntry<T>>): Promise<CachedEntry<T>>;
797
973
 
@@ -1250,16 +1426,13 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1250
1426
  * ```ts
1251
1427
  * // frontend/lib/elytra.ts — the ONE seam file
1252
1428
  * import { createElytraHost } from '@elytracms/next'
1253
- * import { collections, routes } from '../../cms'
1254
- * import { siteComponents } from '../../components'
1255
- *
1256
- * export const elytra = createElytraHost({
1257
- * slug: 'my-site',
1258
- * locales: { default: 'en', locales: ['en'] },
1259
- * collections,
1260
- * routes,
1261
- * components: siteComponents,
1262
- * })
1429
+ * // Read the project out of the repo's own config — `elytra.config.ts` is the
1430
+ * // single contract surface, and importing it costs a delivery bundle nothing
1431
+ * // (EC-532). Any other module layout works just as well; this is an example,
1432
+ * // not a required path.
1433
+ * import { site } from '../../elytra.config'
1434
+ *
1435
+ * export const elytra = createElytraHost({ ...site })
1263
1436
  * ```
1264
1437
  *
1265
1438
  * ```ts
@@ -1291,7 +1464,23 @@ interface ElytraHostOptions {
1291
1464
  * Used as the content scope when no live backend is configured.
1292
1465
  */
1293
1466
  slug: string;
1294
- /** Locale config — config-owned (AD-11), never read from the snapshot. */
1467
+ /**
1468
+ * Locale config — config-owned (AD-11), never read from the snapshot.
1469
+ *
1470
+ * **Import it, do not restate it** (EC-519). This is the *same*
1471
+ * {@link LocaleConfig} `elytra.config.ts` takes, so a project declares its
1472
+ * locales once and hands that one binding to both mounts. It stays a host
1473
+ * option because the engine cannot know where a repo keeps the value — but it
1474
+ * is no longer an option you have to *retype*: since EC-532 the delivery
1475
+ * bundle can import `elytra.config.ts` itself for zero engine modules
1476
+ * (measured), so `createElytraHost({ ...site })` over a project exported from
1477
+ * the config is the shortest correct answer.
1478
+ *
1479
+ * This used to be `{ default, locales }` against a config spelling the same
1480
+ * fact `locales` + `defaultLocale`, and in our own demo the two drifted — the
1481
+ * host said `['en']`, the config said `['en','de']`, and every `/de` URL 404'd
1482
+ * silently for weeks (EC-512).
1483
+ */
1295
1484
  locales: LocaleConfig;
1296
1485
  /** CMS collection schema — config-owned (AD-11). */
1297
1486
  collections: readonly CollectionDef[];
@@ -1342,6 +1531,12 @@ interface HostCanvasRouteOptions {
1342
1531
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
1343
1532
  /** Metadata override (see `CanvasRouteOptions.metadata`). */
1344
1533
  metadata?: (result: ResolvePageResult, info: CanvasRequestInfo) => Metadata;
1534
+ /**
1535
+ * Host-owned chrome around the rendered page (EC-524) — the seam a per-
1536
+ * collection detail template lives on. Branch on `ctx.collection` in your own
1537
+ * route file (see `CanvasRouteOptions.wrap`).
1538
+ */
1539
+ wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
1345
1540
  /**
1346
1541
  * Tagged data cache (instant publish). Default: enabled, scoped to the
1347
1542
  * kit's content scope so the studio's webhook tags match. `false` disables
@@ -1485,6 +1680,12 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
1485
1680
  * document, empty composition, unknown component, or an undeclared hint all
1486
1681
  * answer `false` — the safe default is clearance.
1487
1682
  *
1683
+ * EC-499: pass the collection's resolved `presentation` (from the render
1684
+ * outcome) so this reads the SAME body the renderer will render when the
1685
+ * collection declares one — otherwise the value-shape pick could name a
1686
+ * different field than the declaration does. A body whose first item is prose
1687
+ * rather than a block answers `false`, which is right: rich text opens no hero.
1688
+ *
1488
1689
  * ```tsx
1489
1690
  * const outcome = client.resolvePage(path)
1490
1691
  * const fullBleed = firstBlockFullBleed(outcome.document, components.registry)
@@ -1493,7 +1694,7 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
1493
1694
  *
1494
1695
  * The host kit wires the resolution half for you: `elytra.pageOpensFullBleed(slug)`.
1495
1696
  */
1496
- declare function firstBlockFullBleed(document: Record<string, unknown> | null | undefined, registry: ComponentRegistry): boolean;
1697
+ declare function firstBlockFullBleed(document: Record<string, unknown> | null | undefined, registry: ComponentRegistry, presentation?: DeliveryPresentation): boolean;
1497
1698
 
1498
1699
  /**
1499
1700
  * Pure token/target logic of the draft-preview routes (EC-144), kept free of
@@ -1541,4 +1742,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1541
1742
  */
1542
1743
  declare const PACKAGE = "@elytracms/next";
1543
1744
 
1544
- export { type AssetImageLoaderOptions, type BindingPayloads, type CacheWrapper, type CacheWrapperOptions, type CachedEntry, type CanvasCacheOptions, type CanvasContentRequest, type CanvasDataSource, type CanvasPageOutcome, type CanvasPayloadContext, type CanvasPayloadsFactory, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, ELYTRA_HOST_ENV_KEYS, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type LiveContentOptions, type LoadGlobalsOptions, type MaterializeSourcePayloadsInput, type MaterializedSourcePayloads, type NextImagePrimitiveOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, REVALIDATE_SIGNATURE_HEADER, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidatePayload, type RevalidateRouteHandlers, type RevalidateRouteOptions, type SiteGlobals, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type SourcePayloadError, type StaticContentData, assembleLiveSource, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
1745
+ export { type AssetImageLoaderOptions, type BindingPayloads, type CacheWrapper, type CacheWrapperOptions, type CachedEntry, type CanvasCacheOptions, type CanvasContentRequest, type CanvasDataSource, type CanvasPageOutcome, type CanvasPayloadContext, type CanvasPayloadsFactory, CanvasRenderer, type CanvasRendererProps, type CanvasRequestInfo, type CanvasRoute, type CanvasRouteOptions, type CanvasRouteProps, type CanvasSitemapOptions, type CanvasSitemapResult, type CanvasWrapContext, type ContentSnapshot, type ContentSnapshotOptions, type ContentSource, type CreateCanvasSitemapOptions, type DefineHostComponentsOptions, type DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraHost, type ElytraHostEnv, type ElytraHostImageOptions, type ElytraHostOptions, ElytraImage, type ElytraImageAsset, type ElytraImageProps, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type LiveContentOptions, type LoadGlobalsOptions, type MaterializeSourcePayloadsInput, type MaterializedSourcePayloads, type NextImagePrimitiveOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, REVALIDATE_SIGNATURE_HEADER, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidatePayload, type RevalidateRouteHandlers, type RevalidateRouteOptions, type SiteGlobals, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type SourcePayloadError, type StaticContentData, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError };