@elytracms/next 0.0.91 → 0.0.93

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
@@ -3,11 +3,10 @@ import { ComponentRegistry, ComponentManifest } from '@elytracms/core/component-
3
3
  export { ComponentManifest, PropField, SlotSpec, defineComponent } from '@elytracms/core/component-registry';
4
4
  import { ComponentImplementations, RelationTarget, DocumentBodyItem } from '@elytracms/core/runtime-renderer';
5
5
  export { ComponentImplementations, RenderAsset } from '@elytracms/core/runtime-renderer';
6
- import { HostComponentEntry } from '@elytracms/core/studio-core';
7
- import { ContentQueryError, ResolvePageResult, ResolvedAsset, Perspective, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
8
- export { Perspective } from '@elytracms/core/content';
9
- import { LocaleConfig, Locale, RouteRecord, CollectionDef, RedirectRecord, LocaleConfigInput } from '@elytracms/core/cms-core';
10
- export { CmsDocument, CollectionDef, FieldDef, Locale, LocaleConfig, LocaleConfigInput, LocaleRequest, LocaleStrategy, LocaleUrlPath, RedirectRecord, RouteRecord, documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
6
+ import { HostComponentEntry, ResolvedProject, WorkspaceProjectEntry } from '@elytracms/core/studio-core';
7
+ import { ContentQueryError, ResolvePageResult, ResolvedAsset, ContentClient, CacheTags, ContentDocumentSource, ResolvedDocument } from '@elytracms/core/content';
8
+ import { LocaleConfig, Perspective, Locale, RouteRecord, CollectionDef, RedirectRecord, LocaleConfigInput, CmsValidationIssue } from '@elytracms/core/cms-core';
9
+ export { CmsDocument, CollectionDef, DRAFT_PERSPECTIVE, FieldDef, Locale, LocaleConfig, LocaleConfigInput, LocaleRequest, LocaleStrategy, LocaleUrlPath, Perspective, RedirectRecord, RouteRecord, documentSchema, localeConfigSchema, localeUrlPath, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection, variantOf } from '@elytracms/core/cms-core';
11
10
  import { AssetRecord, PersistenceAdapter } from '@elytracms/core/persistence';
12
11
  export { AssetRecord } from '@elytracms/core/persistence';
13
12
  import { Clock } from '@elytracms/core/determinism';
@@ -252,6 +251,15 @@ declare function mergeCacheTags(first: CacheTags, ...rest: readonly CacheTags[])
252
251
  * API — assembles the exact same `ContentSource` without faking an adapter.
253
252
  */
254
253
  interface ContentSnapshotOptions {
254
+ /**
255
+ * The project's collection definitions — **required** (EC-583). The snapshot's
256
+ * lookup resolves localized fields and a collection's declared `presentation`
257
+ * through them, and the persistence contract holds no schema any more (this used to
258
+ * read `adapter.cms.findSchema`, which in a seeded host was the engine sample's JSON
259
+ * projection unless the host re-saved its own). Hand it the config's collections —
260
+ * the same ones the studio and the seed read.
261
+ */
262
+ collections: readonly CollectionDef[];
255
263
  /** Route records served by `resolvePage` (EC-019). */
256
264
  routes?: readonly RouteRecord[];
257
265
  /** Redirect records served by `resolvePage` (EC-019/020). */
@@ -302,8 +310,13 @@ interface StaticContentData {
302
310
  * explicit unresolved-placeholder scheme (never a silent empty string).
303
311
  */
304
312
  assetUrl?: (asset: AssetRecord) => string;
313
+ /**
314
+ * EC-585 — the stored rows the host could not read (`parseDeliveredDocuments`), handed
315
+ * to every client of this source as its `sourceIssues` (and in the results they
316
+ * concern). A malformed row is not delivered, and never silently.
317
+ */
318
+ sourceIssues?: readonly CmsValidationIssue[];
305
319
  }
306
-
307
320
  /**
308
321
  * Assemble a `ContentSource` from plain prefetched data — the pure half of
309
322
  * {@link createContentSnapshot}, shared with hosts that fetched the data
@@ -471,28 +484,164 @@ interface ResolveCanvasPageOptions {
471
484
  */
472
485
  declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
473
486
 
474
- interface CanvasCacheOptions {
475
- /**
476
- * Project scope prefix for every tag (`p:<scope>:…`) — must be the SAME scope
477
- * the revalidate receiver derives its tags with (`createRevalidateRoute`'s
478
- * `scope`; the delivery kit hands its one `contentScope` to both, EC-579).
479
- */
480
- scope?: string;
481
- /**
482
- * Which content world `loadContent` reads — the delivery kit passes its
483
- * `contentMode` (`'live' | 'fallback' | 'empty'`), so entries written in one
484
- * world can never be served in another (EC-543, see `canvasCacheKey`). A host
485
- * driving this helper directly with a single world may omit it.
486
- */
487
- mode?: string;
487
+ /**
488
+ * Cache-tag pipeline (EC-146, vision AD-5) — the pure half.
489
+ *
490
+ * This module defines (a) the **canonical Next.js tag string format** mapping
491
+ * the accessor-level `CacheTags` (EC-143) onto `cacheTag` / `revalidateTag` keys,
492
+ * (b) the **tag derivation for a revalidation event** ({@link revalidationTags},
493
+ * EC-579 — the receiver's half of the one emitter), and (c) the **pure half of a
494
+ * cached resolution**: its key ({@link canvasCacheKey}) and the `cacheTag` calls that
495
+ * declare its tags ({@link entryTagCalls}). No Next.js imports — everything here is
496
+ * unit-testable.
497
+ *
498
+ * **Caching is the host's (EC-620).** The engine's part is exactly three things: the
499
+ * tags on everything the accessors return, the invalidation signal (the Convex
500
+ * trigger's event → the kit receiver → `revalidateTag` for every derived tag), and a
501
+ * default in the scaffold. Whether a page caches, under which scope, for how long and
502
+ * with which Next flag is the host's: the kit holds no `'use cache'` scope and no
503
+ * `cacheLife`. The host's own `'use cache'` function declares a result's tags through
504
+ * the canvas route's `applyCacheTags` (`route.tsx`), which makes the
505
+ * {@link entryTagCalls} calls with the host's one scope.
506
+ *
507
+ * This is the ONE tag module: the entries are tagged here and the invalidations
508
+ * are derived here, from the same formatters and the same scope. Before EC-579 the
509
+ * format was mirrored twice more (the studio's browser dispatcher and an inline
510
+ * literal in the Convex emitter), and each emitter brought its own scope.
511
+ *
512
+ * ## Tag format
513
+ *
514
+ * - `doc:<collection>:<id>` — one per document whose content was actually in
515
+ * the response (fetch-time tagging: includes depth-1 populated relation
516
+ * targets, and documents inside filtered/dynamic lists *after* the filter
517
+ * ran).
518
+ * - `collection:<name>:<locale>` — the coarse membership tag: every response
519
+ * that read from a collection carries it, so create/delete/newly-matching
520
+ * documents sweep cached lists without read-set tracking.
521
+ * - `route:<pattern>:<locale>` — the resolved URL path plus every hop of a
522
+ * redirect chain.
523
+ * - `asset:<id>` — one per asset whose delivery shape the page *baked*
524
+ * (page-scoped images + `seoOgImage`, EC-227); editing that asset record
525
+ * (alt/dimensions/re-upload) drops exactly the pages that baked it. Not
526
+ * locale-scoped — an asset record is locale-agnostic.
527
+ * - `project` — the whole-project sweep tag attached to **every** cached
528
+ * entry; derived for the events whose effect no narrower tag names (a redirect
529
+ * change, an asset edit — see {@link revalidationTags}).
530
+ *
531
+ * ## Project scope
532
+ *
533
+ * Every tag is prefixed `p:<projectId>:` when a scope is configured (e.g.
534
+ * `p:prj_aurora:doc:post:post-1`). The cost is a few bytes per tag and it
535
+ * keeps multi-project hosts (one Next app serving several builder projects)
536
+ * from cross-invalidating, so the prefix is **on whenever the host knows its
537
+ * project id** — decision: include it. Since EC-579 the scope has ONE owner: the
538
+ * host. The webhook carries an event, not tags, and the receiver derives the
539
+ * tags with the same scope its cache entries are written under (the delivery
540
+ * kit hands both the one `contentScope`), so emitter and host can no longer
541
+ * disagree about it.
542
+ */
543
+ /** Apply the optional multi-project scope prefix to one tag. */
544
+ declare function scopeCacheTag(tag: string, scope?: string): string;
545
+ /**
546
+ * Tag for one document identity. `docKey` is the EC-143 `documentKey` string
547
+ * (`<collection>:<id>`), exactly as found in `CacheTags.docs`.
548
+ */
549
+ declare function docCacheTag(docKey: string, scope?: string): string;
550
+ /** The coarse membership tag of one collection in one locale. */
551
+ declare function collectionCacheTag(collection: string, locale: string, scope?: string): string;
552
+ /** Tag for one resolved route path (or redirect-chain hop) in one locale. */
553
+ declare function routeCacheTag(pattern: string, locale: string, scope?: string): string;
554
+ /**
555
+ * The whole-project sweep tag. Attached to every cached page entry; derived for
556
+ * the events no narrower tag covers ({@link revalidationTags}).
557
+ */
558
+ declare function projectSweepTag(scope?: string): string;
559
+ /**
560
+ * Map an accessor-level `CacheTags` (what one request actually read) onto the
561
+ * full, deterministic (sorted, deduplicated) set of Next tag strings for the
562
+ * cache entry — including the project sweep tag.
563
+ */
564
+ declare function nextCacheTags(tags: CacheTags, scope?: string): string[];
565
+ /** The receiver's own project — what its cached entries are tagged with. */
566
+ interface RevalidationProject {
567
+ /** The tag scope of this host's cache entries (`p:<scope>:…`), if any. */
568
+ scope?: string | undefined;
569
+ /** Every locale this host serves — one `collection:` tag per locale. */
570
+ locales: readonly string[];
488
571
  }
572
+ /**
573
+ * The tags one revalidation event invalidates on this host — sorted, deduplicated,
574
+ * scoped with the receiver's OWN scope, so the emitter never needs to know it.
575
+ *
576
+ * - `publish` / `unpublish` / `delete` → `doc:<collection>:<id>` (its detail page and
577
+ * every cached list that contained it) + `collection:<collection>:<locale>` for
578
+ * EVERY locale this host serves (list membership may have changed, and a URL
579
+ * cached as not-found for a document that now exists carries it too). **No
580
+ * sweep** — a publish drops exactly the entries that read the document or its
581
+ * collection (EC-515);
582
+ * - `redirects` → the sweep (routing can change any URL; see the `redirects` trigger
583
+ * in the studio's `convex/triggers.ts` for why a targeted `route:` tag is wrong);
584
+ * - `asset` → `asset:<id>` + the sweep (a page reading the asset through a document
585
+ * asset FIELD carries no `asset:` tag — EC-566 D16, EC-589);
586
+ * - `probe` → nothing (the doctor's no-op ping).
587
+ *
588
+ * Why the precise tags are enough (EC-515 option D, kept by EC-620): a cached
589
+ * resolution declares its tags with `cacheTag` from INSIDE the cache function that
590
+ * loaded it (the host's, through `applyCacheTags`), and Next decides an entry's
591
+ * staleness from the tags it was STORED with — so a `doc:` tag drops the entries that
592
+ * read the document, on a self-hosted host as on any other. Until EC-515 the read
593
+ * named only the baseline sweep (`unstable_cache` judged the reading wrapper's tags),
594
+ * every precise tag was inert on a self-hosted host, and every publish carried the
595
+ * sweep to compensate (wave-0091). Measured both ways in EC-515's harness, and again
596
+ * with the host-owned scope in EC-620's.
597
+ */
598
+ declare function revalidationTags(event: RevalidationEvent, project: RevalidationProject): string[];
599
+ declare const canvasCacheKeyBrand: unique symbol;
600
+ /**
601
+ * What identifies one published canvas resolution — the argument the canvas route
602
+ * hands the host's cache function (`CanvasRouteOptions.published`), which therefore
603
+ * IS that entry's key: Next keys a `'use cache'` entry by (build id, function id,
604
+ * serialized arguments), so whatever identifies an entry has to be an argument, and
605
+ * nothing else may be. It encodes three facts ({@link parseCanvasCacheKey}):
606
+ *
607
+ * - `scope` — the tag scope (the project id the studio publishes under; `''` if none).
608
+ * - `mode` — which content world the loader reads (the delivery kit's `contentMode`:
609
+ * `live` / `fallback` / `empty`; `''` for a host driving the route directly with
610
+ * one world).
611
+ * - `url` — the request path, locale prefix included.
612
+ *
613
+ * **An opaque string, not an object — measured (EC-620).** Next wraps every
614
+ * `'use cache'` function in React's per-request `cache()`, which memoizes by argument
615
+ * IDENTITY. `Page` and `generateMetadata` each build the key for the same URL; as two
616
+ * equal objects they missed that memo and a cold URL ran the loader twice (page and
617
+ * metadata filling the entry concurrently), as one string they share the in-flight
618
+ * miss — one load, exactly as the kit-owned entry did. A string is also trivially
619
+ * stable for Next's own key serialization. The brand keeps a hand-built string out.
620
+ *
621
+ * The mode is in the key because an entry can outlive the world that wrote it (a
622
+ * shared or remote cache handler, a host whose env changed under a warm cache) and a
623
+ * hit never asks the loader whether it still applies. Before EC-543 the key was
624
+ * `(scope, url)` only, and since `ELYTRA_PROJECT_ID` *is* the config slug, a live host
625
+ * and an empty one keyed the same URL identically — measured on 0.0.85: a warm cache
626
+ * written in live mode kept answering `200` with the live world's bytes after both env
627
+ * vars were blanked and the host itself reported `contentMode: 'empty'`. Keying on the
628
+ * mode makes that cross-read structurally impossible rather than something to detect —
629
+ * a world change is a cold miss, never a stale hit. Since EC-620 the cache function is
630
+ * the host's, so the property holds as long as the host keys on this value — which
631
+ * it does by passing it through unchanged (the canvas route refuses a key of another
632
+ * world).
633
+ */
634
+ type CanvasCacheKey = string & {
635
+ readonly [canvasCacheKeyBrand]: true;
636
+ };
637
+
489
638
  /**
490
639
  * What the route helper tells the content loader about the request it is
491
640
  * loading for (EC-156). Loaders that serve both perspectives from one
492
641
  * prefetched source (the fixture snapshot) can ignore it; a live loader uses
493
642
  * it to fetch the draft-perspective snapshot only for draft-mode requests —
494
- * it must NOT call request APIs like `draftMode()` itself, because on the
495
- * cached published path the loader runs inside a `'use cache'` function, where
643
+ * it must NOT call request APIs like `draftMode()` itself, because on a cached
644
+ * published path the loader runs inside the host's `'use cache'` function, where
496
645
  * request APIs are unavailable.
497
646
  */
498
647
  interface CanvasContentRequest {
@@ -503,8 +652,8 @@ interface CanvasRouteOptions {
503
652
  * Async content loader — the seam EC-145/146 plug Convex into. For the
504
653
  * fixture-backed example this memoizes a `createContentSnapshot` call.
505
654
  * Invoked per request; cache/memoize inside the loader as appropriate.
506
- * With `cache` configured it only runs on a cache miss (once per
507
- * revalidation cycle) — cache hits never touch the content backend.
655
+ * Behind a host cache function ({@link CanvasRouteOptions.published}) it runs
656
+ * only on a cache miss — cache hits never touch the content backend.
508
657
  * Receives the request's perspective (see {@link CanvasContentRequest});
509
658
  * zero-arg loaders remain valid.
510
659
  */
@@ -512,18 +661,38 @@ interface CanvasRouteOptions {
512
661
  /** Host component surface from `defineHostComponents`. */
513
662
  components: HostComponents;
514
663
  /**
515
- * EC-146 instant publish: cache the published-perspective resolution as a
516
- * `'use cache'` entry tagged with the response's own fetch-time tags,
517
- * invalidated by the `/api/revalidate` webhook (EC-515). Draft mode and
518
- * `next dev` always bypass it. Tag-driven only — no time-based revalidation
519
- * (the AD-5 model, `CANVAS_CACHE_LIFE`). Requires `cacheComponents: true` in
520
- * the host's `next.config`.
664
+ * The tag scope of everything this route resolves (`p:<scope>:…`) — what
665
+ * {@link CanvasRoute.applyCacheTags} scopes with and what every
666
+ * {@link CanvasCacheKey} carries. It must be the SAME scope the revalidate receiver
667
+ * derives its tags with (`createRevalidateRoute`'s `scope`); the delivery kit hands
668
+ * its one `contentScope` to both (EC-579) — a disagreement would drop nothing,
669
+ * silently.
670
+ */
671
+ scope?: string;
672
+ /**
673
+ * Which content world `loadContent` reads — the delivery kit passes its
674
+ * `contentMode` (`'live' | 'fallback' | 'empty'`). It rides in every
675
+ * {@link CanvasCacheKey}, so an entry written in one world can never be served in
676
+ * another (EC-543). A host driving this helper directly with one world may omit it.
677
+ */
678
+ mode?: string;
679
+ /**
680
+ * How a PUBLISHED request gets its outcome — the host's cache seam (EC-620). Pass
681
+ * your own `'use cache'` function: it receives the request's {@link CanvasCacheKey}
682
+ * (keep that its only argument — it is the entry's key), calls
683
+ * {@link CanvasRoute.resolve} with it, declares the result's tags with
684
+ * {@link CanvasRoute.applyCacheTags}, picks its `cacheLife`, and returns the
685
+ * outcome (module example above; the scaffold's page ships it as the default).
686
+ *
687
+ * Omit it and every published request resolves fresh — dynamic and uncached. Draft
688
+ * mode never calls it. `Page` and `generateMetadata` call it with the same key.
521
689
  */
522
- cache?: CanvasCacheOptions;
690
+ published?: (key: CanvasCacheKey) => Promise<CanvasPageOutcome>;
523
691
  /**
524
692
  * EC-146 hook: receives the merged `CacheTags` of everything this request
525
693
  * read (the `resolvePage` tags plus the baked relations/listings/links). On a
526
- * cache hit these are the stored tags of the run that wrote the entry.
694
+ * cache hit (through a host {@link CanvasRouteOptions.published} function) these
695
+ * are the stored tags of the run that wrote the entry.
527
696
  */
528
697
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
529
698
  /** Override the generated `<head>` metadata for a resolved page. */
@@ -583,14 +752,47 @@ interface CanvasRoute {
583
752
  */
584
753
  generateMetadata: (props: CanvasRouteProps, parent: ResolvingMetadata) => Promise<Metadata>;
585
754
  /**
586
- * `export const generateStaticParams = route.generateStaticParams` — REQUIRED in
587
- * the catch-all page file under `cacheComponents` (EC-515, measured on Next
588
- * 16.2.10): without it, awaiting the catch-all's `params` fails `next build`
589
- * ("Uncached data was accessed outside of <Suspense>"). Wrapping the page in
590
- * `<Suspense>` (or a root `loading.tsx`) instead also builds, but answers every
591
- * 404 and every redirect with HTTP 200 — the status is committed before the
592
- * content resolves. Declaring params keeps the page blocking, so `notFound()` is a
593
- * real 404 and a redirect a real 308/307.
755
+ * Resolve one PUBLISHED request — with no cache scope of its own (EC-620). Call it
756
+ * from the `'use cache'` function you pass as {@link CanvasRouteOptions.published},
757
+ * with the key the route handed that function. A key of another scope or content
758
+ * mode (a cache function shared with a route of another world) is refused loudly:
759
+ * the entry would be keyed under one world and resolved in another (EC-543).
760
+ *
761
+ * The outcome carries everything a cache decision needs: `tags` (what the
762
+ * resolution read — hand them to {@link applyCacheTags}) and, on a `render`
763
+ * outcome, `readsNow` (a listing on the page compared against the clock, so the
764
+ * page changes as time passes — bound that entry's life).
765
+ */
766
+ resolve: (key: CanvasCacheKey) => Promise<CanvasPageOutcome>;
767
+ /**
768
+ * Declare a result's tags on the CURRENT `'use cache'` entry, scoped with this
769
+ * route's scope — exactly the strings the revalidate receiver revalidates (EC-620).
770
+ * Any accessor result works (`{ tags }`): a resolution from {@link resolve}, or a
771
+ * `listDocuments` result in a route of your own.
772
+ *
773
+ * It makes the {@link entryTagCalls} calls: the project sweep first and alone, the
774
+ * rest in chunks of at most 128. Next keeps 128 tags per `cacheTag()` call and drops
775
+ * the rest with only a warning, and the sweep sorts last — one `cacheTag(...tags)`
776
+ * call loses it on a page that read ~125 documents, and with it every redirect and
777
+ * asset invalidation (measured, EC-515/EC-620).
778
+ *
779
+ * Callable ONLY inside a `'use cache'` function: anywhere else Next's own `cacheTag`
780
+ * throws (E819 during a request or a prerender; E886 when neither `cacheComponents`
781
+ * nor `experimental.useCache` is enabled) — a loud error, never a silent no-op.
782
+ */
783
+ applyCacheTags: (result: {
784
+ tags: CacheTags;
785
+ }) => void;
786
+ /**
787
+ * `export const generateStaticParams = route.generateStaticParams` — needed in the
788
+ * catch-all page file only when the HOST enables `cacheComponents` (that flag's
789
+ * rule, not the kit's; measured on Next 16.2.10, EC-515): without it, awaiting the
790
+ * catch-all's `params` fails `next build` ("Uncached data was accessed outside of
791
+ * <Suspense>"). Wrapping the page in `<Suspense>` (or a root `loading.tsx`) instead
792
+ * also builds, but answers every 404 and every redirect with HTTP 200 — the status
793
+ * is committed before the content resolves. Declaring params keeps the page
794
+ * blocking, so `notFound()` is a real 404 and a redirect a real 308/307. A host
795
+ * without `cacheComponents` needs none of it.
594
796
  *
595
797
  * It names ONE param set: {@link CANVAS_VALIDATION_SLUG}, a sample that Next
596
798
  * prerenders to validate the route and that resolves to not-found without
@@ -609,9 +811,71 @@ interface CanvasRoute {
609
811
  * route answers it with not-found and never resolves it against content.
610
812
  */
611
813
  declare const CANVAS_VALIDATION_SLUG: readonly string[];
612
- /** Create the `{ Page, generateMetadata }` pair for `app/[[...slug]]/page.tsx`. */
814
+ /**
815
+ * THE edge mapping of Next's draft mode onto the delivery perspective (EC-595 binding
816
+ * point 1): draft mode reads the draft variant, everything else the published snapshot.
817
+ * The kit's other request-time read (`pageOpensFullBleed`) imports this one, and the
818
+ * package exports it for a host's own server components, so the mapping is declared
819
+ * once. A request API: never call it inside a cache scope.
820
+ */
821
+ declare function requestPerspective(): Promise<Perspective>;
822
+ /**
823
+ * Create the canvas route for `app/[[...slug]]/page.tsx` — `{ Page, generateMetadata,
824
+ * generateStaticParams }` to export, plus `resolve` / `applyCacheTags` for the host's
825
+ * own cache function ({@link CanvasRouteOptions.published}).
826
+ */
613
827
  declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
614
828
 
829
+ /**
830
+ * A resolved document → Next.js `Metadata` (EC-171, reshaped by EC-187, and by EC-590 —
831
+ * EC-565 Round 3 "3g, extensibility"): **`seoMetadata(doc, ctx)`**, the kit's pure,
832
+ * exported SEO reader. The route's `generateMetadata` calls it (the default), so a
833
+ * project WRAPS it (`{ ...seoMetadata(doc, ctx), other }` for its own sub-fields),
834
+ * REPLACES it, or ignores it.
835
+ *
836
+ * What it reads — never a dictated field name:
837
+ * - the document's **SEO object**: the field under its collection's SEO ROLE
838
+ * (`seoField`, default a field named `seo`; the engine's `seoFields()` declares it).
839
+ * Its sub-field names are the contract ({@link SEO_SUBFIELDS}); a sub-field the object
840
+ * omits emits nothing, a sub-field the project added is theirs to read.
841
+ * - the document's **display title**: the ONE `resolveDocumentTitle` over its TITLE ROLE
842
+ * (`titleField`, default `title` → `name`) — the resolve result carries it
843
+ * (`result.title`), so a function `titleField` titles the page too.
844
+ *
845
+ * Mapping:
846
+ * - `title`: `seo.title` → the display title.
847
+ * - `description`: `seo.description`.
848
+ * - `openGraph`: title (`seo.ogTitle` → the effective title), description
849
+ * (`seo.ogDescription` → `seo.description`), image (`seo.image`) — and `locale` from the
850
+ * page's resolved locale when, and only when, the code carries a territory (EC-531,
851
+ * EC-610). The site-wide half of Open Graph (`type`, `siteName`, the site image) is the
852
+ * LAYOUT's; the canvas route merges this object over the layout's
853
+ * (`mergeMetadataOverParent`), so a page states only what is its own. Site-level SEO
854
+ * defaults live on the project's own settings document and are read by its layout —
855
+ * this reader needs none (EC-590 (b): nothing here ever read a settings document).
856
+ * - `robots`: `seo.noindex` → `{ index: false }`.
857
+ * - `alternates.canonical`: `seo.canonical` (EC-228), verbatim.
858
+ *
859
+ * Image resolution (EC-202/EC-266): `seo.image` is an asset field, delivered as a resolved
860
+ * asset object (`{ id, url, … }`) → its `url`; a string is a passthrough URL or a raw asset
861
+ * id resolved against the page's baked `assets`; anything unresolvable is dropped — an
862
+ * `og:image` must carry a real URL, never an internal id.
863
+ *
864
+ * `hreflang` alternates are intentionally NOT derived here (a per-page translation map is
865
+ * the sitemap's — EC-517).
866
+ */
867
+ interface SeoMetadataContext {
868
+ /** The SEO role field name — `result.seoField`. Default: the conventional `seo`. */
869
+ seoField?: string | null;
870
+ /** The display title — `result.title` (the title role, resolved). */
871
+ title?: string | null;
872
+ /** The page's resolved locale (`result.tags.locale`) — for `og:locale`. */
873
+ locale?: string;
874
+ /** The page-scoped baked assets (a raw asset-id `image` resolves against them). */
875
+ assets?: readonly ResolvedAsset[];
876
+ }
877
+ /** The {@link SeoMetadataContext} of a resolve result — the default route's one line. */
878
+ declare function seoContextOf(result: ResolvePageResult, assets?: readonly ResolvedAsset[]): SeoMetadataContext;
615
879
  /**
616
880
  * EC-531, narrowed by EC-610 (owner ruling 3, 2026-10-23): `og:locale` ONLY in the
617
881
  * shape Open Graph specifies — `language_TERRITORY` with an underscore (`de_DE`),
@@ -623,7 +887,10 @@ declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
623
887
  * Brazil, Latin America), and a wrong valid value looks deliberate.
624
888
  */
625
889
  declare function openGraphLocale(locale: string): string | null;
626
- declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly ResolvedAsset[]): Metadata;
890
+ /** The document's SEO object (the field under its SEO role), or an empty record. */
891
+ declare function seoObjectOf(doc: Record<string, unknown> | null | undefined, seoField?: string | null | undefined): Record<string, unknown>;
892
+ /** The kit's SEO reader — see the module docs. Pure: data in, `Metadata` out. */
893
+ declare function seoMetadata(doc: Record<string, unknown> | null | undefined, ctx?: SeoMetadataContext): Metadata;
627
894
  /**
628
895
  * EC-610 (owner ruling 1, 2026-10-13): **the site provides the default tags, and
629
896
  * the page's tags are merged on top** — a value the page states overrides the
@@ -650,72 +917,39 @@ declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly
650
917
  declare function mergeMetadataOverParent(parent: ResolvedMetadata | null | undefined, page: Metadata): Metadata;
651
918
 
652
919
  /**
653
- * `sitemap.xml` from published routes (EC-171): walk the same route records
654
- * `resolvePage` resolves against, resolve every candidate URL in the
655
- * **published** perspective, and emit `MetadataRoute.Sitemap` entries per
656
- * locale — Next's `app/sitemap.ts` convention. Pages marked `noindex` (and
657
- * URLs that do not resolve, e.g. a page that was never published) are
658
- * omitted.
659
- *
660
- * Dynamic routes (`/blog/:slug`): a route record does not declare which
661
- * collection feeds its params, so enumeration is delegated to the host's
662
- * {@link SitemapParamsProvider} — it gets a published-perspective accessor
663
- * client and returns one param record per concrete URL (typically from
664
- * `listDocuments`). **Honest limit:** parameterized routes without a provider
665
- * (or provider entries missing a param) are skipped and reported in
666
- * `skipped`, never guessed. `lastModified` is omitted — the delivery shape
667
- * carries no per-page timestamps today.
668
- *
669
- * Hierarchy mounts (`/:path*`, EC-218): a page-tree mount self-describes its
670
- * URLs, so it is enumerated automatically from the collection's published
671
- * documents (composing each page's nested path from its parent chain) — no
672
- * params provider entry needed.
673
- *
674
- * **Enumeration is a candidate list, not the output (EC-486).** Every branch
675
- * above — static, provider-fed, and mount — only proposes paths; each one is
676
- * then put back through `resolvePage`, and a path the resolver refuses is
677
- * dropped. So the sitemap cannot claim a URL the delivery host would 404, even
678
- * though the mount branch enumerates its collection independently of route
679
- * precedence. Do not read the enumeration in isolation: the gate below it is
680
- * what makes the two agree. What a mount enumerated and the resolver then
681
- * refused is reported in {@link CanvasSitemapResult.skipped}, never dropped in
682
- * silence.
920
+ * `sitemap.xml` from published routes (EC-171, reshaped by EC-590): the URL of every
921
+ * published document of every collection a route names, per locale — Next's
922
+ * `app/sitemap.ts` convention. Documents marked `noindex` in their SEO object, and URLs
923
+ * that do not resolve, are omitted.
924
+ *
925
+ * **The router enumerates, no host provider (EC-590; EC-565 Round 3, 3e).** A route record
926
+ * names the collection it serves (`document.collection`), so the router's `urlsFor` —
927
+ * the same reverse direction the studio's live link and every rendered link read —
928
+ * yields one URL per published document: template routes (`/blog/:slug`), hierarchy
929
+ * mounts (`/:path*`, composed through the parent and slug roles) and literal-id routes
930
+ * alike. The former `SitemapParamsProvider` existed because "a route record does not
931
+ * declare which collection feeds its params" — false for every document-bound route
932
+ * (F6c RESTATED). A document-less static route (`/contact`, no `document`) is listed by
933
+ * its pattern.
934
+ *
935
+ * **Enumeration is a candidate list, not the output (EC-486).** Every candidate is put
936
+ * back through `resolvePage`, and a path the resolver refuses is dropped — so the sitemap
937
+ * cannot claim a URL the delivery host would 404. What was enumerated and then refused is
938
+ * reported in {@link CanvasSitemapResult.skipped}, never dropped in silence. `lastModified`
939
+ * is omitted — the delivery shape carries no per-page timestamps today.
683
940
  *
684
941
  * Host usage (`app/sitemap.ts`):
685
942
  *
686
943
  * ```ts
687
944
  * import { createCanvasSitemap } from '@elytracms/next'
688
- * import { loadContent } from '../lib/content'
689
- *
690
- * export default createCanvasSitemap({
691
- * loadContent,
692
- * baseUrl: 'https://example.com',
693
- * params: ({ route, client }) =>
694
- * route.id === 'r-post'
695
- * ? (client.listDocuments('post').ok ? … : []) // slug per published post
696
- * : null,
697
- * })
945
+ * import { elytra } from '../lib/elytra'
946
+ *
947
+ * export default createCanvasSitemap({ loadContent: elytra.loadContent, baseUrl: elytra.frontendUrl! })
698
948
  * ```
699
949
  */
700
- interface SitemapRouteContext {
701
- route: RouteRecord;
702
- locale: Locale;
703
- /** Published-perspective accessor client bound to `locale`. */
704
- client: ContentClient;
705
- }
706
- /**
707
- * Enumerate the param sets of one dynamic route — one record per concrete
708
- * URL (e.g. `[{ slug: 'hello-world' }, …]`). Return `null`/`undefined` to
709
- * skip the route (reported in `skipped`).
710
- */
711
- type SitemapParamsProvider = (ctx: SitemapRouteContext) => readonly Record<string, string>[] | null | undefined;
712
950
  interface CanvasSitemapOptions {
713
951
  /** Absolute site origin, e.g. `https://example.com` (no trailing slash needed). */
714
952
  baseUrl: string;
715
- /** Route records to enumerate; defaults to the source's own route table. */
716
- routes?: readonly RouteRecord[];
717
- /** Param enumeration for dynamic routes (see {@link SitemapParamsProvider}). */
718
- params?: SitemapParamsProvider;
719
953
  }
720
954
  interface SitemapSkippedRoute {
721
955
  routeId: string;
@@ -724,13 +958,12 @@ interface SitemapSkippedRoute {
724
958
  }
725
959
  interface CanvasSitemapResult {
726
960
  entries: MetadataRoute.Sitemap;
727
- /** Routes that could not be enumerated — explicit, never silent. */
961
+ /** Routes whose candidates could not be published — explicit, never silent. */
728
962
  skipped: SitemapSkippedRoute[];
729
963
  }
730
964
  /**
731
- * Enumerate the sitemap entries of one content source (published
732
- * perspective). Pure over the prefetched source — synchronous accessors,
733
- * deterministic ordering (sorted by URL).
965
+ * Enumerate the sitemap entries of one content source (published perspective). Pure over
966
+ * the prefetched source — synchronous accessors, deterministic ordering (sorted by URL).
734
967
  */
735
968
  declare function canvasSitemapEntries(source: ContentSource, options: CanvasSitemapOptions): CanvasSitemapResult;
736
969
  interface CreateCanvasSitemapOptions extends CanvasSitemapOptions {
@@ -747,112 +980,6 @@ interface CreateCanvasSitemapOptions extends CanvasSitemapOptions {
747
980
  */
748
981
  declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () => Promise<MetadataRoute.Sitemap>;
749
982
 
750
- /**
751
- * Cache-tag pipeline (EC-146, vision AD-5) — the pure half.
752
- *
753
- * This module defines (a) the **canonical Next.js tag string format** mapping
754
- * the accessor-level `CacheTags` (EC-143) onto `cacheTag` / `revalidateTag` keys,
755
- * (b) the **tag derivation for a revalidation event** ({@link revalidationTags},
756
- * EC-579 — the receiver's half of the one emitter), and (c) the **pure half of the
757
- * route's cache entry** (EC-515): its key ({@link canvasCacheKey}), its `cacheTag`
758
- * calls ({@link entryTagCalls}), its life ({@link CANVAS_CACHE_LIFE}) and the
759
- * resolver registry the module-level `'use cache'` function reads its loader from
760
- * ({@link registerCanvasResolver}). No Next.js imports — everything here is
761
- * unit-testable; `route.tsx` holds the one `'use cache'` function that calls
762
- * `cacheTag` / `cacheLife` with these values.
763
- *
764
- * This is the ONE tag module: the entries are tagged here and the invalidations
765
- * are derived here, from the same formatters and the same scope. Before EC-579 the
766
- * format was mirrored twice more (the studio's browser dispatcher and an inline
767
- * literal in the Convex emitter), and each emitter brought its own scope.
768
- *
769
- * ## Tag format
770
- *
771
- * - `doc:<collection>:<id>` — one per document whose content was actually in
772
- * the response (fetch-time tagging: includes depth-1 populated relation
773
- * targets, and documents inside filtered/dynamic lists *after* the filter
774
- * ran).
775
- * - `collection:<name>:<locale>` — the coarse membership tag: every response
776
- * that read from a collection carries it, so create/delete/newly-matching
777
- * documents sweep cached lists without read-set tracking.
778
- * - `route:<pattern>:<locale>` — the resolved URL path plus every hop of a
779
- * redirect chain.
780
- * - `asset:<id>` — one per asset whose delivery shape the page *baked*
781
- * (page-scoped images + `seoOgImage`, EC-227); editing that asset record
782
- * (alt/dimensions/re-upload) drops exactly the pages that baked it. Not
783
- * locale-scoped — an asset record is locale-agnostic.
784
- * - `project` — the whole-project sweep tag attached to **every** cached
785
- * entry; derived for the events whose effect no narrower tag names (a redirect
786
- * change, an asset edit — see {@link revalidationTags}).
787
- *
788
- * ## Project scope
789
- *
790
- * Every tag is prefixed `p:<projectId>:` when a scope is configured (e.g.
791
- * `p:prj_aurora:doc:post:post-1`). The cost is a few bytes per tag and it
792
- * keeps multi-project hosts (one Next app serving several builder projects)
793
- * from cross-invalidating, so the prefix is **on whenever the host knows its
794
- * project id** — decision: include it. Since EC-579 the scope has ONE owner: the
795
- * host. The webhook carries an event, not tags, and the receiver derives the
796
- * tags with the same scope its cache entries are written under (the delivery
797
- * kit hands both the one `contentScope`), so emitter and host can no longer
798
- * disagree about it.
799
- */
800
- /** Apply the optional multi-project scope prefix to one tag. */
801
- declare function scopeCacheTag(tag: string, scope?: string): string;
802
- /**
803
- * Tag for one document identity. `docKey` is the EC-143 `documentKey` string
804
- * (`<collection>:<id>`), exactly as found in `CacheTags.docs`.
805
- */
806
- declare function docCacheTag(docKey: string, scope?: string): string;
807
- /** The coarse membership tag of one collection in one locale. */
808
- declare function collectionCacheTag(collection: string, locale: string, scope?: string): string;
809
- /** Tag for one resolved route path (or redirect-chain hop) in one locale. */
810
- declare function routeCacheTag(pattern: string, locale: string, scope?: string): string;
811
- /**
812
- * The whole-project sweep tag. Attached to every cached page entry; derived for
813
- * the events no narrower tag covers ({@link revalidationTags}).
814
- */
815
- declare function projectSweepTag(scope?: string): string;
816
- /**
817
- * Map an accessor-level `CacheTags` (what one request actually read) onto the
818
- * full, deterministic (sorted, deduplicated) set of Next tag strings for the
819
- * cache entry — including the project sweep tag.
820
- */
821
- declare function nextCacheTags(tags: CacheTags, scope?: string): string[];
822
- /** The receiver's own project — what its cached entries are tagged with. */
823
- interface RevalidationProject {
824
- /** The tag scope of this host's cache entries (`p:<scope>:…`), if any. */
825
- scope?: string | undefined;
826
- /** Every locale this host serves — one `collection:` tag per locale. */
827
- locales: readonly string[];
828
- }
829
- /**
830
- * The tags one revalidation event invalidates on this host — sorted, deduplicated,
831
- * scoped with the receiver's OWN scope, so the emitter never needs to know it.
832
- *
833
- * - `publish` / `unpublish` / `delete` → `doc:<collection>:<id>` (its detail page and
834
- * every cached list that contained it) + `collection:<collection>:<locale>` for
835
- * EVERY locale this host serves (list membership may have changed, and a URL
836
- * cached as not-found for a document that now exists carries it too). **No
837
- * sweep** — a publish drops exactly the entries that read the document or its
838
- * collection (EC-515);
839
- * - `redirects` → the sweep (routing can change any URL; see the `redirects` trigger
840
- * in the studio's `convex/triggers.ts` for why a targeted `route:` tag is wrong);
841
- * - `asset` → `asset:<id>` + the sweep (a page reading the asset through a document
842
- * asset FIELD carries no `asset:` tag — EC-566 D16, EC-589);
843
- * - `probe` → nothing (the doctor's no-op ping).
844
- *
845
- * Why the precise tags are enough now (EC-515, option D): each route entry declares
846
- * its tags with `cacheTag` from INSIDE the cached function, and Next decides an
847
- * entry's staleness from the tags it was STORED with — so a `doc:` tag drops the
848
- * entries that read the document, on a self-hosted host as on any other. Until
849
- * then the read named only the baseline sweep (`unstable_cache` judged the
850
- * reading wrapper's tags), every precise tag was inert on a self-hosted host, and
851
- * every publish carried the sweep to compensate (wave-0091). Measured both ways
852
- * in EC-515's harness.
853
- */
854
- declare function revalidationTags(event: RevalidationEvent, project: RevalidationProject): string[];
855
-
856
983
  /**
857
984
  * Pure request logic of the revalidation webhook receiver (EC-146), kept free of
858
985
  * Next.js imports so it is unit-testable. `revalidate.ts` wires it into a route
@@ -1310,7 +1437,8 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1310
1437
  * const route = elytra.canvasRoute()
1311
1438
  * export default route.Page
1312
1439
  * export const generateMetadata = route.generateMetadata
1313
- * export const generateStaticParams = route.generateStaticParams // cacheComponents (EC-515)
1440
+ * export const generateStaticParams = route.generateStaticParams // only if YOU enable cacheComponents
1441
+ * // (to cache: pass your own 'use cache' function as `published` — see `route.tsx`, EC-620)
1314
1442
  *
1315
1443
  * // app/api/revalidate/route.ts
1316
1444
  * export const { POST } = elytra.revalidateRoute()
@@ -1334,7 +1462,13 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1334
1462
  * project declared with `defineCollection` keeps its literal types through the host and
1335
1463
  * `InferDocument<typeof host, 'post'>` (`@elytracms/core/cms-core`) is a delivered post.
1336
1464
  */
1337
- interface ElytraHostOptions<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> {
1465
+ interface ElytraHostOptions<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> extends Omit<WorkspaceProjectEntry, 'slug' | 'locales' | 'collections' | 'routes' | 'components' | 'frontendUrl'> {
1466
+ /**
1467
+ * The project's display name (the entry's own `name` — `{ ...site }` carries it).
1468
+ * EC-583: the options ARE the config's project entry plus the host-only keys below,
1469
+ * and the kit resolves them through the one resolver the studio boots through.
1470
+ */
1471
+ name: string;
1338
1472
  /**
1339
1473
  * The project slug (config-as-code identity, mirrors `elytra.config.ts`).
1340
1474
  * Used as the content scope when no live backend is configured.
@@ -1400,8 +1534,9 @@ interface ElytraHostOptions<Collections extends readonly CollectionDef[] = reado
1400
1534
  */
1401
1535
  components?: readonly HostComponentEntry[];
1402
1536
  /**
1403
- * The public base URL of the delivery site (mirrors the config's
1404
- * `frontendUrl`) — exposed on the handle for sitemap/canonical use.
1537
+ * The public base URL of the delivery site (the config's project-level
1538
+ * `frontendUrl`). The handle's {@link ElytraHost.frontendUrl} is the RESOLVED one —
1539
+ * the bound environment's own URL when it declares one (EC-583).
1405
1540
  */
1406
1541
  frontendUrl?: string;
1407
1542
  /**
@@ -1438,18 +1573,18 @@ interface HostCanvasRouteOptions {
1438
1573
  */
1439
1574
  wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
1440
1575
  /**
1441
- * Tagged cache entry (instant publish): one `'use cache'` entry per URL, tagged
1442
- * with what its resolution read (EC-515; the host enables `cacheComponents`).
1443
- * Always on and always scoped to the kit's {@link ElytraHost.contentScope}, with
1444
- * the kit's {@link ElytraHost.contentMode} in the entry key (EC-543). `false`
1445
- * disables caching entirely.
1576
+ * The host's cache function for published requests (EC-620 — see
1577
+ * `CanvasRouteOptions.published`): your own `'use cache'` function that calls
1578
+ * `route.resolve(key)`, `route.applyCacheTags(outcome)` and picks its `cacheLife`.
1579
+ * Omit it and the route resolves every request fresh (dynamic, uncached).
1446
1580
  *
1447
- * There is no scope override (EC-579): the kit's revalidate receiver derives its
1448
- * tags with that same `contentScope`, so one value scopes both the entries and
1449
- * their invalidation. An override here was the one way left for the two to
1450
- * disagree — and a disagreement drops nothing, silently.
1581
+ * The route it builds is always scoped to the kit's {@link ElytraHost.contentScope}
1582
+ * with the kit's {@link ElytraHost.contentMode} in every cache key (EC-543), and
1583
+ * there is no override (EC-579): the kit's revalidate receiver derives its tags with
1584
+ * that same `contentScope`, so one value scopes both the entries and their
1585
+ * invalidation — a disagreement would drop nothing, silently.
1451
1586
  */
1452
- cache?: false;
1587
+ published?: CanvasRouteOptions['published'];
1453
1588
  }
1454
1589
  interface LoadGlobalsOptions {
1455
1590
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
@@ -1506,19 +1641,37 @@ interface ElytraHost<Collections extends readonly CollectionDef[] = readonly Col
1506
1641
  contentMode: ElytraContentMode;
1507
1642
  /** The resolved env bag (documented names, trimmed, empty = unset). */
1508
1643
  env: ElytraHostEnv;
1509
- /** Materialized route records (a derive is run once, at factory time). */
1644
+ /**
1645
+ * The ONE resolved project (EC-583) — the options resolved through
1646
+ * `resolveProject`, the same rules the studio boots through: composed, defaulted,
1647
+ * validation slots normalized, routes materialized, every boot rule applied. The
1648
+ * kit reads its locales, collections, routes and frontend URL from here.
1649
+ */
1650
+ project: ResolvedProject;
1651
+ /**
1652
+ * The project's route issue set at boot (EC-590 — the ONE router's: route conflicts,
1653
+ * mount conflicts, reachability). Surfaced on the console at build and in dev; the
1654
+ * editorial redirects (data) are judged per snapshot by the content client's
1655
+ * `routerIssues` and in the studio.
1656
+ */
1657
+ routeIssues: readonly CmsValidationIssue[];
1658
+ /** Materialized route records (the resolved project's; a derive ran once, at factory time). */
1510
1659
  routes: RouteRecord[];
1511
1660
  /** The registered component surface — exactly the `components` option's list (EC-591). */
1512
1661
  components: HostComponents;
1513
- /** The public base URL of the delivery site, when configured. */
1662
+ /**
1663
+ * The public base URL of the delivery site, when configured — the resolved project's
1664
+ * (`projectFrontendUrl`): the bound environment's own `frontendUrl`, else the
1665
+ * project-level one. The same answer the studio's live links read (EC-583).
1666
+ */
1514
1667
  frontendUrl?: string;
1515
1668
  /**
1516
1669
  * The content loader (the seam every kit surface reads through): live
1517
1670
  * delivery snapshot when env is configured — published, or the token-gated
1518
1671
  * draft for draft-mode requests — else the fallback source.
1519
1672
  *
1520
- * An UNCACHED read (EC-515): only the canvas route's entry caches. Under
1521
- * `cacheComponents`, live-mode I/O in a layout or page outside `<Suspense>` fails
1673
+ * An UNCACHED read: the kit caches nothing itself (EC-620 — caching is the host's).
1674
+ * Under `cacheComponents`, live-mode I/O in a layout or page outside `<Suspense>` fails
1522
1675
  * `next build` — call it inside a `<Suspense>`'d component (route handlers and
1523
1676
  * `sitemap.ts` just render dynamically).
1524
1677
  */
@@ -1580,10 +1733,10 @@ declare function createElytraHost<const Collections extends readonly CollectionD
1580
1733
  * `DRAFT_PREVIEW_TOKEN` — the same single secret that gates the kit's
1581
1734
  * preview-enable route (see `host-env.ts`).
1582
1735
  *
1583
- * Routes: config-sourced route records (AD-3/AD-11, EC-207 — the host repo's
1584
- * own `cms/`, passed via options) and the implicit per-locale home fallback are
1585
- * MERGED (EC-177 gap 2), never XOR'd — config records win per pattern+locale.
1586
- * The backend never carries route data.
1736
+ * Routes: the config's route records (AD-3/AD-11, EC-207), exactly as resolved —
1737
+ * the backend never carries route data, and the engine derives none (EC-590: the
1738
+ * implicit "`/` → the lowest-id `page`" home fallback is gone with every other
1739
+ * engine-dictated name; a project declares its `/`, the scaffold ships one).
1587
1740
  */
1588
1741
  interface LiveContentOptions {
1589
1742
  /** Convex deployment URL, e.g. `https://your-deployment-123.convex.cloud`. */
@@ -1596,7 +1749,7 @@ interface LiveContentOptions {
1596
1749
  locales: LocaleConfigInput;
1597
1750
  /** CMS collection schema — config-owned (AD-11); drives depth-1 relation population. */
1598
1751
  collections: readonly CollectionDef[];
1599
- /** Route records — config-owned (AD-3/AD-11); merged with the home fallback. */
1752
+ /** Route records — config-owned (AD-3/AD-11), the resolved project's. */
1600
1753
  routes: readonly RouteRecord[];
1601
1754
  /** `DRAFT_PREVIEW_TOKEN` — required for draft-perspective loads. */
1602
1755
  draftToken?: string;
@@ -1616,7 +1769,6 @@ interface DeliverySnapshotPayload {
1616
1769
  documents: unknown[];
1617
1770
  assets: unknown[];
1618
1771
  }
1619
-
1620
1772
  /**
1621
1773
  * Assemble a fetched delivery-snapshot payload into a `ContentSource` — the
1622
1774
  * pure half of {@link loadLiveContent}, exported so the parsing/assembly rules
@@ -1718,4 +1870,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1718
1870
  */
1719
1871
  declare const PACKAGE = "@elytracms/next";
1720
1872
 
1721
- export { type AssetImageLoaderOptions, type BakedListing, CANVAS_VALIDATION_SLUG, type CanvasCacheOptions, type CanvasContentRequest, type CanvasPageOutcome, 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 DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, 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 NextImageOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RevalidationProject, type SiteGlobals, type SitemapParamsProvider, type SitemapRouteContext, type SitemapSkippedRoute, type StaticContentData, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, withNextImage };
1873
+ export { type AssetImageLoaderOptions, type BakedListing, CANVAS_VALIDATION_SLUG, type CanvasCacheKey, type CanvasContentRequest, type CanvasPageOutcome, 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 DeliveryPresentation, ELYTRA_HOST_ENV_KEYS, type ElytraContentMode, 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 NextImageOptions, PACKAGE, type PreviewEvaluation, type PreviewRouteHandlers, type PreviewRouteOptions, type ResolveCanvasPageOptions, type ResolveGlobalsOptions, type ResolvedLink, type ResolvedNavEntry, type ResolvedNavLink, type RevalidateEvaluation, type RevalidateRouteHandlers, type RevalidateRouteOptions, type RevalidationProject, type SeoMetadataContext, type SiteGlobals, type SitemapSkippedRoute, type StaticContentData, assembleLiveSource, bodyItemsOf, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, requestPerspective, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, seoContextOf, seoMetadata, seoObjectOf, withNextImage };