@elytracms/next 0.0.81 → 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,53 +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>;
378
-
379
- /**
380
- * The locale ⇄ URL mapping, owned in ONE place (EC-486).
381
- *
382
- * The delivery host reads a request URL and derives `{ locale, path }`; the
383
- * sitemap goes the other way, turning an enumerated `{ locale, path }` back
384
- * into the URL a crawler will request. Those are inverse functions, and while
385
- * they lived in two files they disagreed: a page whose top-level slug happens
386
- * to equal a **non-default locale code** (`/de` in a project whose locales are
387
- * `['en','de']`, default `en`) was published to `sitemap.xml` as
388
- * `https://site/de`, which the host then split as *locale `de`, path `/`* and
389
- * 404'd. Measured — see EC-486.
390
- *
391
- * Keeping both directions here makes the round-trip checkable, which is what
392
- * {@link localeUrlPath} reports: some `{ locale, path }` pairs simply have **no
393
- * URL that addresses them**, and a caller that publishes URLs must be able to
394
- * tell rather than emit a link nothing serves.
395
- */
396
- /**
397
- * Derive the request locale from a leading URL segment (`/de/about` → locale
398
- * `de`, path `/about`) and return the remaining path. No prefix (or the
399
- * default locale's own content at root) falls back to the default locale.
400
- */
401
- declare function splitLocalePath(url: string, locales: LocaleConfig): {
402
- locale: Locale;
403
- path: string;
404
- };
405
- interface LocaleUrlPath {
406
- /** The request path a client uses to reach `path` in `locale`. */
407
- urlPath: string;
408
- /**
409
- * Whether {@link splitLocalePath} recovers exactly the `{ locale, path }`
410
- * this was built from. `false` means the pair is **unaddressable**: every URL
411
- * that could denote it is read by the host as a different locale/path pair,
412
- * so the content is unreachable and the URL must not be published.
413
- */
414
- addressable: boolean;
415
- }
416
- /**
417
- * The inverse of {@link splitLocalePath}: default locale serves the root,
418
- * other locales get a `/<locale>` prefix — then the result is round-tripped
419
- * back through `splitLocalePath` so an unaddressable pair is reported rather
420
- * than silently emitted (AGENTS.md §4: never a fallback that hides a broken
421
- * state).
422
- */
423
- declare function localeUrlPath(path: string, locale: Locale, locales: LocaleConfig): LocaleUrlPath;
442
+ declare function createContentSnapshot(adapter: PersistenceAdapter, projectId: string, options: ContentSnapshotOptions): Promise<ContentSnapshot>;
424
443
 
425
444
  /**
426
445
  * The pure half of the catch-all route helper (EC-144): URL → outcome, with
@@ -429,6 +448,43 @@ declare function localeUrlPath(path: string, locale: Locale, locales: LocaleConf
429
448
  * each outcome onto `redirect()` / `permanentRedirect()` / `notFound()` /
430
449
  * `<CanvasRenderer />`.
431
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
+ }
432
488
  /** What the host's payload factory sees for one request. */
433
489
  interface CanvasPayloadContext {
434
490
  /** Accessor client bound to this request's perspective + locale. */
@@ -500,6 +556,15 @@ type CanvasPageOutcome = {
500
556
  * embedding page's href.
501
557
  */
502
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;
503
568
  tags: CacheTags;
504
569
  };
505
570
  /** Build the request path from a Next catch-all `slug` param. */
@@ -525,28 +590,6 @@ interface ResolveCanvasPageOptions {
525
590
  */
526
591
  declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
527
592
 
528
- /**
529
- * Catch-all route helper (EC-144): the factory a host app calls once in
530
- * `app/[[...slug]]/page.tsx` to wire `resolvePage` → `<CanvasRenderer />`,
531
- * with redirects answered by Next's `redirect()`/`permanentRedirect()` (per
532
- * the route's `permanent` flag), unknown URLs answered by `notFound()` (the
533
- * host's 404 renders), and the perspective chosen by Next `draftMode()` —
534
- * draft when enabled (via the token-gated preview route), published
535
- * otherwise.
536
- *
537
- * With `cache` configured (EC-146), the published-perspective resolve+render
538
- * data work runs inside `unstable_cache`, keyed by URL and tagged with the
539
- * fetch-time tag set of what the response actually contained (see
540
- * `cache.ts`). Publishing in the studio then invalidates exactly the affected
541
- * entries via the signed `/api/revalidate` webhook — and while the content
542
- * backend is unreachable, cached entries keep serving because a cache hit
543
- * never invokes `loadContent`.
544
- */
545
- interface CanvasRequestInfo {
546
- /** The raw request path (including any locale prefix). */
547
- url: string;
548
- perspective: Perspective;
549
- }
550
593
  interface CanvasCacheOptions {
551
594
  /**
552
595
  * Project scope prefix for every tag (`p:<scope>:…`) — must be the project
@@ -600,6 +643,38 @@ interface CanvasRouteOptions {
600
643
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
601
644
  /** Override the generated `<head>` metadata for a resolved page. */
602
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;
603
678
  }
604
679
  interface CanvasRouteProps {
605
680
  params: Promise<{
@@ -750,14 +825,14 @@ declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () =>
750
825
  * keeps multi-project hosts (one Next app serving several builder projects)
751
826
  * from cross-invalidating, so the prefix is **on whenever the host knows its
752
827
  * project id** — decision: include it. The emitter (the studio's webhook
753
- * dispatcher, `apps/builder/src/lib/webhooks`) always scopes with the
828
+ * dispatcher, `packages/studio/src/lib/webhooks`) always scopes with the
754
829
  * operation's `projectId`; a host that omits `cache.scope` therefore will not
755
830
  * match studio-emitted tags. Configure `cache.scope` with the same project id
756
831
  * the studio publishes under.
757
832
  *
758
833
  * The studio-side emitter cannot depend on this package (it would drag the
759
- * `next` peer dependency into the builder), so
760
- * `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
761
836
  * both sides pin the exact literal strings to keep them in lock-step.
762
837
  */
763
838
  /** Apply the optional multi-project scope prefix to one tag. */
@@ -832,14 +907,67 @@ interface CachedEntry<T> {
832
907
  * entry and the next request repeats the cycle with fresh data.
833
908
  *
834
909
  * The loader runs **once per revalidation cycle** (per key), never once per
835
- * request. Time-based revalidation is intentionally not exposed: a
836
- * stale-while-revalidate background refresh would re-run the probe callback
837
- * (whose bail aborts the refresh), so this design is tag-driven only —
838
- * `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.
839
927
  *
840
928
  * Concurrency: wrappers and the captured closure are per-request, so there is
841
929
  * no cross-request mutation; two racing first requests both resolve and the
842
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.
843
971
  */
844
972
  declare function loadCachedEntry<T>(cache: CacheWrapper, keyParts: string[], baselineTags: readonly string[], load: () => Promise<CachedEntry<T>>): Promise<CachedEntry<T>>;
845
973
 
@@ -1298,16 +1426,13 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1298
1426
  * ```ts
1299
1427
  * // frontend/lib/elytra.ts — the ONE seam file
1300
1428
  * import { createElytraHost } from '@elytracms/next'
1301
- * import { collections, routes } from '../../cms'
1302
- * import { siteComponents } from '../../components'
1303
- *
1304
- * export const elytra = createElytraHost({
1305
- * slug: 'my-site',
1306
- * locales: { default: 'en', locales: ['en'] },
1307
- * collections,
1308
- * routes,
1309
- * components: siteComponents,
1310
- * })
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 })
1311
1436
  * ```
1312
1437
  *
1313
1438
  * ```ts
@@ -1339,7 +1464,23 @@ interface ElytraHostOptions {
1339
1464
  * Used as the content scope when no live backend is configured.
1340
1465
  */
1341
1466
  slug: string;
1342
- /** 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
+ */
1343
1484
  locales: LocaleConfig;
1344
1485
  /** CMS collection schema — config-owned (AD-11). */
1345
1486
  collections: readonly CollectionDef[];
@@ -1390,6 +1531,12 @@ interface HostCanvasRouteOptions {
1390
1531
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
1391
1532
  /** Metadata override (see `CanvasRouteOptions.metadata`). */
1392
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;
1393
1540
  /**
1394
1541
  * Tagged data cache (instant publish). Default: enabled, scoped to the
1395
1542
  * kit's content scope so the studio's webhook tags match. `false` disables
@@ -1533,6 +1680,12 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
1533
1680
  * document, empty composition, unknown component, or an undeclared hint all
1534
1681
  * answer `false` — the safe default is clearance.
1535
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
+ *
1536
1689
  * ```tsx
1537
1690
  * const outcome = client.resolvePage(path)
1538
1691
  * const fullBleed = firstBlockFullBleed(outcome.document, components.registry)
@@ -1541,7 +1694,7 @@ declare function loadLiveContent(options: LiveContentOptions, perspective: Persp
1541
1694
  *
1542
1695
  * The host kit wires the resolution half for you: `elytra.pageOpensFullBleed(slug)`.
1543
1696
  */
1544
- 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;
1545
1698
 
1546
1699
  /**
1547
1700
  * Pure token/target logic of the draft-preview routes (EC-144), kept free of
@@ -1589,4 +1742,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1589
1742
  */
1590
1743
  declare const PACKAGE = "@elytracms/next";
1591
1744
 
1592
- 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 LocaleUrlPath, 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, localeUrlPath, 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 };