@elytracms/next 0.0.91 → 0.0.92

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). */
@@ -303,7 +311,6 @@ interface StaticContentData {
303
311
  */
304
312
  assetUrl?: (asset: AssetRecord) => string;
305
313
  }
306
-
307
314
  /**
308
315
  * Assemble a `ContentSource` from plain prefetched data — the pure half of
309
316
  * {@link createContentSnapshot}, shared with hosts that fetched the data
@@ -471,28 +478,164 @@ interface ResolveCanvasPageOptions {
471
478
  */
472
479
  declare function resolveCanvasPage(source: ContentSource, url: string, perspective: Perspective, options?: ResolveCanvasPageOptions): Promise<CanvasPageOutcome>;
473
480
 
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;
481
+ /**
482
+ * Cache-tag pipeline (EC-146, vision AD-5) — the pure half.
483
+ *
484
+ * This module defines (a) the **canonical Next.js tag string format** mapping
485
+ * the accessor-level `CacheTags` (EC-143) onto `cacheTag` / `revalidateTag` keys,
486
+ * (b) the **tag derivation for a revalidation event** ({@link revalidationTags},
487
+ * EC-579 — the receiver's half of the one emitter), and (c) the **pure half of a
488
+ * cached resolution**: its key ({@link canvasCacheKey}) and the `cacheTag` calls that
489
+ * declare its tags ({@link entryTagCalls}). No Next.js imports — everything here is
490
+ * unit-testable.
491
+ *
492
+ * **Caching is the host's (EC-620).** The engine's part is exactly three things: the
493
+ * tags on everything the accessors return, the invalidation signal (the Convex
494
+ * trigger's event → the kit receiver → `revalidateTag` for every derived tag), and a
495
+ * default in the scaffold. Whether a page caches, under which scope, for how long and
496
+ * with which Next flag is the host's: the kit holds no `'use cache'` scope and no
497
+ * `cacheLife`. The host's own `'use cache'` function declares a result's tags through
498
+ * the canvas route's `applyCacheTags` (`route.tsx`), which makes the
499
+ * {@link entryTagCalls} calls with the host's one scope.
500
+ *
501
+ * This is the ONE tag module: the entries are tagged here and the invalidations
502
+ * are derived here, from the same formatters and the same scope. Before EC-579 the
503
+ * format was mirrored twice more (the studio's browser dispatcher and an inline
504
+ * literal in the Convex emitter), and each emitter brought its own scope.
505
+ *
506
+ * ## Tag format
507
+ *
508
+ * - `doc:<collection>:<id>` — one per document whose content was actually in
509
+ * the response (fetch-time tagging: includes depth-1 populated relation
510
+ * targets, and documents inside filtered/dynamic lists *after* the filter
511
+ * ran).
512
+ * - `collection:<name>:<locale>` — the coarse membership tag: every response
513
+ * that read from a collection carries it, so create/delete/newly-matching
514
+ * documents sweep cached lists without read-set tracking.
515
+ * - `route:<pattern>:<locale>` — the resolved URL path plus every hop of a
516
+ * redirect chain.
517
+ * - `asset:<id>` — one per asset whose delivery shape the page *baked*
518
+ * (page-scoped images + `seoOgImage`, EC-227); editing that asset record
519
+ * (alt/dimensions/re-upload) drops exactly the pages that baked it. Not
520
+ * locale-scoped — an asset record is locale-agnostic.
521
+ * - `project` — the whole-project sweep tag attached to **every** cached
522
+ * entry; derived for the events whose effect no narrower tag names (a redirect
523
+ * change, an asset edit — see {@link revalidationTags}).
524
+ *
525
+ * ## Project scope
526
+ *
527
+ * Every tag is prefixed `p:<projectId>:` when a scope is configured (e.g.
528
+ * `p:prj_aurora:doc:post:post-1`). The cost is a few bytes per tag and it
529
+ * keeps multi-project hosts (one Next app serving several builder projects)
530
+ * from cross-invalidating, so the prefix is **on whenever the host knows its
531
+ * project id** — decision: include it. Since EC-579 the scope has ONE owner: the
532
+ * host. The webhook carries an event, not tags, and the receiver derives the
533
+ * tags with the same scope its cache entries are written under (the delivery
534
+ * kit hands both the one `contentScope`), so emitter and host can no longer
535
+ * disagree about it.
536
+ */
537
+ /** Apply the optional multi-project scope prefix to one tag. */
538
+ declare function scopeCacheTag(tag: string, scope?: string): string;
539
+ /**
540
+ * Tag for one document identity. `docKey` is the EC-143 `documentKey` string
541
+ * (`<collection>:<id>`), exactly as found in `CacheTags.docs`.
542
+ */
543
+ declare function docCacheTag(docKey: string, scope?: string): string;
544
+ /** The coarse membership tag of one collection in one locale. */
545
+ declare function collectionCacheTag(collection: string, locale: string, scope?: string): string;
546
+ /** Tag for one resolved route path (or redirect-chain hop) in one locale. */
547
+ declare function routeCacheTag(pattern: string, locale: string, scope?: string): string;
548
+ /**
549
+ * The whole-project sweep tag. Attached to every cached page entry; derived for
550
+ * the events no narrower tag covers ({@link revalidationTags}).
551
+ */
552
+ declare function projectSweepTag(scope?: string): string;
553
+ /**
554
+ * Map an accessor-level `CacheTags` (what one request actually read) onto the
555
+ * full, deterministic (sorted, deduplicated) set of Next tag strings for the
556
+ * cache entry — including the project sweep tag.
557
+ */
558
+ declare function nextCacheTags(tags: CacheTags, scope?: string): string[];
559
+ /** The receiver's own project — what its cached entries are tagged with. */
560
+ interface RevalidationProject {
561
+ /** The tag scope of this host's cache entries (`p:<scope>:…`), if any. */
562
+ scope?: string | undefined;
563
+ /** Every locale this host serves — one `collection:` tag per locale. */
564
+ locales: readonly string[];
488
565
  }
566
+ /**
567
+ * The tags one revalidation event invalidates on this host — sorted, deduplicated,
568
+ * scoped with the receiver's OWN scope, so the emitter never needs to know it.
569
+ *
570
+ * - `publish` / `unpublish` / `delete` → `doc:<collection>:<id>` (its detail page and
571
+ * every cached list that contained it) + `collection:<collection>:<locale>` for
572
+ * EVERY locale this host serves (list membership may have changed, and a URL
573
+ * cached as not-found for a document that now exists carries it too). **No
574
+ * sweep** — a publish drops exactly the entries that read the document or its
575
+ * collection (EC-515);
576
+ * - `redirects` → the sweep (routing can change any URL; see the `redirects` trigger
577
+ * in the studio's `convex/triggers.ts` for why a targeted `route:` tag is wrong);
578
+ * - `asset` → `asset:<id>` + the sweep (a page reading the asset through a document
579
+ * asset FIELD carries no `asset:` tag — EC-566 D16, EC-589);
580
+ * - `probe` → nothing (the doctor's no-op ping).
581
+ *
582
+ * Why the precise tags are enough (EC-515 option D, kept by EC-620): a cached
583
+ * resolution declares its tags with `cacheTag` from INSIDE the cache function that
584
+ * loaded it (the host's, through `applyCacheTags`), and Next decides an entry's
585
+ * staleness from the tags it was STORED with — so a `doc:` tag drops the entries that
586
+ * read the document, on a self-hosted host as on any other. Until EC-515 the read
587
+ * named only the baseline sweep (`unstable_cache` judged the reading wrapper's tags),
588
+ * every precise tag was inert on a self-hosted host, and every publish carried the
589
+ * sweep to compensate (wave-0091). Measured both ways in EC-515's harness, and again
590
+ * with the host-owned scope in EC-620's.
591
+ */
592
+ declare function revalidationTags(event: RevalidationEvent, project: RevalidationProject): string[];
593
+ declare const canvasCacheKeyBrand: unique symbol;
594
+ /**
595
+ * What identifies one published canvas resolution — the argument the canvas route
596
+ * hands the host's cache function (`CanvasRouteOptions.published`), which therefore
597
+ * IS that entry's key: Next keys a `'use cache'` entry by (build id, function id,
598
+ * serialized arguments), so whatever identifies an entry has to be an argument, and
599
+ * nothing else may be. It encodes three facts ({@link parseCanvasCacheKey}):
600
+ *
601
+ * - `scope` — the tag scope (the project id the studio publishes under; `''` if none).
602
+ * - `mode` — which content world the loader reads (the delivery kit's `contentMode`:
603
+ * `live` / `fallback` / `empty`; `''` for a host driving the route directly with
604
+ * one world).
605
+ * - `url` — the request path, locale prefix included.
606
+ *
607
+ * **An opaque string, not an object — measured (EC-620).** Next wraps every
608
+ * `'use cache'` function in React's per-request `cache()`, which memoizes by argument
609
+ * IDENTITY. `Page` and `generateMetadata` each build the key for the same URL; as two
610
+ * equal objects they missed that memo and a cold URL ran the loader twice (page and
611
+ * metadata filling the entry concurrently), as one string they share the in-flight
612
+ * miss — one load, exactly as the kit-owned entry did. A string is also trivially
613
+ * stable for Next's own key serialization. The brand keeps a hand-built string out.
614
+ *
615
+ * The mode is in the key because an entry can outlive the world that wrote it (a
616
+ * shared or remote cache handler, a host whose env changed under a warm cache) and a
617
+ * hit never asks the loader whether it still applies. Before EC-543 the key was
618
+ * `(scope, url)` only, and since `ELYTRA_PROJECT_ID` *is* the config slug, a live host
619
+ * and an empty one keyed the same URL identically — measured on 0.0.85: a warm cache
620
+ * written in live mode kept answering `200` with the live world's bytes after both env
621
+ * vars were blanked and the host itself reported `contentMode: 'empty'`. Keying on the
622
+ * mode makes that cross-read structurally impossible rather than something to detect —
623
+ * a world change is a cold miss, never a stale hit. Since EC-620 the cache function is
624
+ * the host's, so the property holds as long as the host keys on this value — which
625
+ * it does by passing it through unchanged (the canvas route refuses a key of another
626
+ * world).
627
+ */
628
+ type CanvasCacheKey = string & {
629
+ readonly [canvasCacheKeyBrand]: true;
630
+ };
631
+
489
632
  /**
490
633
  * What the route helper tells the content loader about the request it is
491
634
  * loading for (EC-156). Loaders that serve both perspectives from one
492
635
  * prefetched source (the fixture snapshot) can ignore it; a live loader uses
493
636
  * 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
637
+ * it must NOT call request APIs like `draftMode()` itself, because on a cached
638
+ * published path the loader runs inside the host's `'use cache'` function, where
496
639
  * request APIs are unavailable.
497
640
  */
498
641
  interface CanvasContentRequest {
@@ -503,8 +646,8 @@ interface CanvasRouteOptions {
503
646
  * Async content loader — the seam EC-145/146 plug Convex into. For the
504
647
  * fixture-backed example this memoizes a `createContentSnapshot` call.
505
648
  * 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.
649
+ * Behind a host cache function ({@link CanvasRouteOptions.published}) it runs
650
+ * only on a cache miss — cache hits never touch the content backend.
508
651
  * Receives the request's perspective (see {@link CanvasContentRequest});
509
652
  * zero-arg loaders remain valid.
510
653
  */
@@ -512,18 +655,38 @@ interface CanvasRouteOptions {
512
655
  /** Host component surface from `defineHostComponents`. */
513
656
  components: HostComponents;
514
657
  /**
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`.
658
+ * The tag scope of everything this route resolves (`p:<scope>:…`) — what
659
+ * {@link CanvasRoute.applyCacheTags} scopes with and what every
660
+ * {@link CanvasCacheKey} carries. It must be the SAME scope the revalidate receiver
661
+ * derives its tags with (`createRevalidateRoute`'s `scope`); the delivery kit hands
662
+ * its one `contentScope` to both (EC-579) — a disagreement would drop nothing,
663
+ * silently.
664
+ */
665
+ scope?: string;
666
+ /**
667
+ * Which content world `loadContent` reads — the delivery kit passes its
668
+ * `contentMode` (`'live' | 'fallback' | 'empty'`). It rides in every
669
+ * {@link CanvasCacheKey}, so an entry written in one world can never be served in
670
+ * another (EC-543). A host driving this helper directly with one world may omit it.
671
+ */
672
+ mode?: string;
673
+ /**
674
+ * How a PUBLISHED request gets its outcome — the host's cache seam (EC-620). Pass
675
+ * your own `'use cache'` function: it receives the request's {@link CanvasCacheKey}
676
+ * (keep that its only argument — it is the entry's key), calls
677
+ * {@link CanvasRoute.resolve} with it, declares the result's tags with
678
+ * {@link CanvasRoute.applyCacheTags}, picks its `cacheLife`, and returns the
679
+ * outcome (module example above; the scaffold's page ships it as the default).
680
+ *
681
+ * Omit it and every published request resolves fresh — dynamic and uncached. Draft
682
+ * mode never calls it. `Page` and `generateMetadata` call it with the same key.
521
683
  */
522
- cache?: CanvasCacheOptions;
684
+ published?: (key: CanvasCacheKey) => Promise<CanvasPageOutcome>;
523
685
  /**
524
686
  * EC-146 hook: receives the merged `CacheTags` of everything this request
525
687
  * 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.
688
+ * cache hit (through a host {@link CanvasRouteOptions.published} function) these
689
+ * are the stored tags of the run that wrote the entry.
527
690
  */
528
691
  onTags?: (tags: CacheTags, info: CanvasRequestInfo) => void;
529
692
  /** Override the generated `<head>` metadata for a resolved page. */
@@ -583,14 +746,47 @@ interface CanvasRoute {
583
746
  */
584
747
  generateMetadata: (props: CanvasRouteProps, parent: ResolvingMetadata) => Promise<Metadata>;
585
748
  /**
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.
749
+ * Resolve one PUBLISHED request — with no cache scope of its own (EC-620). Call it
750
+ * from the `'use cache'` function you pass as {@link CanvasRouteOptions.published},
751
+ * with the key the route handed that function. A key of another scope or content
752
+ * mode (a cache function shared with a route of another world) is refused loudly:
753
+ * the entry would be keyed under one world and resolved in another (EC-543).
754
+ *
755
+ * The outcome carries everything a cache decision needs: `tags` (what the
756
+ * resolution read — hand them to {@link applyCacheTags}) and, on a `render`
757
+ * outcome, `readsNow` (a listing on the page compared against the clock, so the
758
+ * page changes as time passes — bound that entry's life).
759
+ */
760
+ resolve: (key: CanvasCacheKey) => Promise<CanvasPageOutcome>;
761
+ /**
762
+ * Declare a result's tags on the CURRENT `'use cache'` entry, scoped with this
763
+ * route's scope — exactly the strings the revalidate receiver revalidates (EC-620).
764
+ * Any accessor result works (`{ tags }`): a resolution from {@link resolve}, or a
765
+ * `listDocuments` result in a route of your own.
766
+ *
767
+ * It makes the {@link entryTagCalls} calls: the project sweep first and alone, the
768
+ * rest in chunks of at most 128. Next keeps 128 tags per `cacheTag()` call and drops
769
+ * the rest with only a warning, and the sweep sorts last — one `cacheTag(...tags)`
770
+ * call loses it on a page that read ~125 documents, and with it every redirect and
771
+ * asset invalidation (measured, EC-515/EC-620).
772
+ *
773
+ * Callable ONLY inside a `'use cache'` function: anywhere else Next's own `cacheTag`
774
+ * throws (E819 during a request or a prerender; E886 when neither `cacheComponents`
775
+ * nor `experimental.useCache` is enabled) — a loud error, never a silent no-op.
776
+ */
777
+ applyCacheTags: (result: {
778
+ tags: CacheTags;
779
+ }) => void;
780
+ /**
781
+ * `export const generateStaticParams = route.generateStaticParams` — needed in the
782
+ * catch-all page file only when the HOST enables `cacheComponents` (that flag's
783
+ * rule, not the kit's; measured on Next 16.2.10, EC-515): without it, awaiting the
784
+ * catch-all's `params` fails `next build` ("Uncached data was accessed outside of
785
+ * <Suspense>"). Wrapping the page in `<Suspense>` (or a root `loading.tsx`) instead
786
+ * also builds, but answers every 404 and every redirect with HTTP 200 — the status
787
+ * is committed before the content resolves. Declaring params keeps the page
788
+ * blocking, so `notFound()` is a real 404 and a redirect a real 308/307. A host
789
+ * without `cacheComponents` needs none of it.
594
790
  *
595
791
  * It names ONE param set: {@link CANVAS_VALIDATION_SLUG}, a sample that Next
596
792
  * prerenders to validate the route and that resolves to not-found without
@@ -609,9 +805,71 @@ interface CanvasRoute {
609
805
  * route answers it with not-found and never resolves it against content.
610
806
  */
611
807
  declare const CANVAS_VALIDATION_SLUG: readonly string[];
612
- /** Create the `{ Page, generateMetadata }` pair for `app/[[...slug]]/page.tsx`. */
808
+ /**
809
+ * THE edge mapping of Next's draft mode onto the delivery perspective (EC-595 binding
810
+ * point 1): draft mode reads the draft variant, everything else the published snapshot.
811
+ * The kit's other request-time read (`pageOpensFullBleed`) imports this one, and the
812
+ * package exports it for a host's own server components, so the mapping is declared
813
+ * once. A request API: never call it inside a cache scope.
814
+ */
815
+ declare function requestPerspective(): Promise<Perspective>;
816
+ /**
817
+ * Create the canvas route for `app/[[...slug]]/page.tsx` — `{ Page, generateMetadata,
818
+ * generateStaticParams }` to export, plus `resolve` / `applyCacheTags` for the host's
819
+ * own cache function ({@link CanvasRouteOptions.published}).
820
+ */
613
821
  declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
614
822
 
823
+ /**
824
+ * A resolved document → Next.js `Metadata` (EC-171, reshaped by EC-187, and by EC-590 —
825
+ * EC-565 Round 3 "3g, extensibility"): **`seoMetadata(doc, ctx)`**, the kit's pure,
826
+ * exported SEO reader. The route's `generateMetadata` calls it (the default), so a
827
+ * project WRAPS it (`{ ...seoMetadata(doc, ctx), other }` for its own sub-fields),
828
+ * REPLACES it, or ignores it.
829
+ *
830
+ * What it reads — never a dictated field name:
831
+ * - the document's **SEO object**: the field under its collection's SEO ROLE
832
+ * (`seoField`, default a field named `seo`; the engine's `seoFields()` declares it).
833
+ * Its sub-field names are the contract ({@link SEO_SUBFIELDS}); a sub-field the object
834
+ * omits emits nothing, a sub-field the project added is theirs to read.
835
+ * - the document's **display title**: the ONE `resolveDocumentTitle` over its TITLE ROLE
836
+ * (`titleField`, default `title` → `name`) — the resolve result carries it
837
+ * (`result.title`), so a function `titleField` titles the page too.
838
+ *
839
+ * Mapping:
840
+ * - `title`: `seo.title` → the display title.
841
+ * - `description`: `seo.description`.
842
+ * - `openGraph`: title (`seo.ogTitle` → the effective title), description
843
+ * (`seo.ogDescription` → `seo.description`), image (`seo.image`) — and `locale` from the
844
+ * page's resolved locale when, and only when, the code carries a territory (EC-531,
845
+ * EC-610). The site-wide half of Open Graph (`type`, `siteName`, the site image) is the
846
+ * LAYOUT's; the canvas route merges this object over the layout's
847
+ * (`mergeMetadataOverParent`), so a page states only what is its own. Site-level SEO
848
+ * defaults live on the project's own settings document and are read by its layout —
849
+ * this reader needs none (EC-590 (b): nothing here ever read a settings document).
850
+ * - `robots`: `seo.noindex` → `{ index: false }`.
851
+ * - `alternates.canonical`: `seo.canonical` (EC-228), verbatim.
852
+ *
853
+ * Image resolution (EC-202/EC-266): `seo.image` is an asset field, delivered as a resolved
854
+ * asset object (`{ id, url, … }`) → its `url`; a string is a passthrough URL or a raw asset
855
+ * id resolved against the page's baked `assets`; anything unresolvable is dropped — an
856
+ * `og:image` must carry a real URL, never an internal id.
857
+ *
858
+ * `hreflang` alternates are intentionally NOT derived here (a per-page translation map is
859
+ * the sitemap's — EC-517).
860
+ */
861
+ interface SeoMetadataContext {
862
+ /** The SEO role field name — `result.seoField`. Default: the conventional `seo`. */
863
+ seoField?: string | null;
864
+ /** The display title — `result.title` (the title role, resolved). */
865
+ title?: string | null;
866
+ /** The page's resolved locale (`result.tags.locale`) — for `og:locale`. */
867
+ locale?: string;
868
+ /** The page-scoped baked assets (a raw asset-id `image` resolves against them). */
869
+ assets?: readonly ResolvedAsset[];
870
+ }
871
+ /** The {@link SeoMetadataContext} of a resolve result — the default route's one line. */
872
+ declare function seoContextOf(result: ResolvePageResult, assets?: readonly ResolvedAsset[]): SeoMetadataContext;
615
873
  /**
616
874
  * EC-531, narrowed by EC-610 (owner ruling 3, 2026-10-23): `og:locale` ONLY in the
617
875
  * shape Open Graph specifies — `language_TERRITORY` with an underscore (`de_DE`),
@@ -623,7 +881,10 @@ declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
623
881
  * Brazil, Latin America), and a wrong valid value looks deliberate.
624
882
  */
625
883
  declare function openGraphLocale(locale: string): string | null;
626
- declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly ResolvedAsset[]): Metadata;
884
+ /** The document's SEO object (the field under its SEO role), or an empty record. */
885
+ declare function seoObjectOf(doc: Record<string, unknown> | null | undefined, seoField?: string | null | undefined): Record<string, unknown>;
886
+ /** The kit's SEO reader — see the module docs. Pure: data in, `Metadata` out. */
887
+ declare function seoMetadata(doc: Record<string, unknown> | null | undefined, ctx?: SeoMetadataContext): Metadata;
627
888
  /**
628
889
  * EC-610 (owner ruling 1, 2026-10-13): **the site provides the default tags, and
629
890
  * the page's tags are merged on top** — a value the page states overrides the
@@ -650,72 +911,39 @@ declare function canvasPageMetadata(result: ResolvePageResult, assets?: readonly
650
911
  declare function mergeMetadataOverParent(parent: ResolvedMetadata | null | undefined, page: Metadata): Metadata;
651
912
 
652
913
  /**
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.
914
+ * `sitemap.xml` from published routes (EC-171, reshaped by EC-590): the URL of every
915
+ * published document of every collection a route names, per locale — Next's
916
+ * `app/sitemap.ts` convention. Documents marked `noindex` in their SEO object, and URLs
917
+ * that do not resolve, are omitted.
918
+ *
919
+ * **The router enumerates, no host provider (EC-590; EC-565 Round 3, 3e).** A route record
920
+ * names the collection it serves (`document.collection`), so the router's `urlsFor` —
921
+ * the same reverse direction the studio's live link and every rendered link read —
922
+ * yields one URL per published document: template routes (`/blog/:slug`), hierarchy
923
+ * mounts (`/:path*`, composed through the parent and slug roles) and literal-id routes
924
+ * alike. The former `SitemapParamsProvider` existed because "a route record does not
925
+ * declare which collection feeds its params" — false for every document-bound route
926
+ * (F6c RESTATED). A document-less static route (`/contact`, no `document`) is listed by
927
+ * its pattern.
928
+ *
929
+ * **Enumeration is a candidate list, not the output (EC-486).** Every candidate is put
930
+ * back through `resolvePage`, and a path the resolver refuses is dropped — so the sitemap
931
+ * cannot claim a URL the delivery host would 404. What was enumerated and then refused is
932
+ * reported in {@link CanvasSitemapResult.skipped}, never dropped in silence. `lastModified`
933
+ * is omitted — the delivery shape carries no per-page timestamps today.
683
934
  *
684
935
  * Host usage (`app/sitemap.ts`):
685
936
  *
686
937
  * ```ts
687
938
  * 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
- * })
939
+ * import { elytra } from '../lib/elytra'
940
+ *
941
+ * export default createCanvasSitemap({ loadContent: elytra.loadContent, baseUrl: elytra.frontendUrl! })
698
942
  * ```
699
943
  */
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
944
  interface CanvasSitemapOptions {
713
945
  /** Absolute site origin, e.g. `https://example.com` (no trailing slash needed). */
714
946
  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
947
  }
720
948
  interface SitemapSkippedRoute {
721
949
  routeId: string;
@@ -724,13 +952,12 @@ interface SitemapSkippedRoute {
724
952
  }
725
953
  interface CanvasSitemapResult {
726
954
  entries: MetadataRoute.Sitemap;
727
- /** Routes that could not be enumerated — explicit, never silent. */
955
+ /** Routes whose candidates could not be published — explicit, never silent. */
728
956
  skipped: SitemapSkippedRoute[];
729
957
  }
730
958
  /**
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).
959
+ * Enumerate the sitemap entries of one content source (published perspective). Pure over
960
+ * the prefetched source — synchronous accessors, deterministic ordering (sorted by URL).
734
961
  */
735
962
  declare function canvasSitemapEntries(source: ContentSource, options: CanvasSitemapOptions): CanvasSitemapResult;
736
963
  interface CreateCanvasSitemapOptions extends CanvasSitemapOptions {
@@ -747,112 +974,6 @@ interface CreateCanvasSitemapOptions extends CanvasSitemapOptions {
747
974
  */
748
975
  declare function createCanvasSitemap(options: CreateCanvasSitemapOptions): () => Promise<MetadataRoute.Sitemap>;
749
976
 
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
977
  /**
857
978
  * Pure request logic of the revalidation webhook receiver (EC-146), kept free of
858
979
  * Next.js imports so it is unit-testable. `revalidate.ts` wires it into a route
@@ -1310,7 +1431,8 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1310
1431
  * const route = elytra.canvasRoute()
1311
1432
  * export default route.Page
1312
1433
  * export const generateMetadata = route.generateMetadata
1313
- * export const generateStaticParams = route.generateStaticParams // cacheComponents (EC-515)
1434
+ * export const generateStaticParams = route.generateStaticParams // only if YOU enable cacheComponents
1435
+ * // (to cache: pass your own 'use cache' function as `published` — see `route.tsx`, EC-620)
1314
1436
  *
1315
1437
  * // app/api/revalidate/route.ts
1316
1438
  * export const { POST } = elytra.revalidateRoute()
@@ -1334,7 +1456,13 @@ declare function createPreviewDisableRoute(): PreviewRouteHandlers;
1334
1456
  * project declared with `defineCollection` keeps its literal types through the host and
1335
1457
  * `InferDocument<typeof host, 'post'>` (`@elytracms/core/cms-core`) is a delivered post.
1336
1458
  */
1337
- interface ElytraHostOptions<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> {
1459
+ interface ElytraHostOptions<Collections extends readonly CollectionDef[] = readonly CollectionDef[]> extends Omit<WorkspaceProjectEntry, 'slug' | 'locales' | 'collections' | 'routes' | 'components' | 'frontendUrl'> {
1460
+ /**
1461
+ * The project's display name (the entry's own `name` — `{ ...site }` carries it).
1462
+ * EC-583: the options ARE the config's project entry plus the host-only keys below,
1463
+ * and the kit resolves them through the one resolver the studio boots through.
1464
+ */
1465
+ name: string;
1338
1466
  /**
1339
1467
  * The project slug (config-as-code identity, mirrors `elytra.config.ts`).
1340
1468
  * Used as the content scope when no live backend is configured.
@@ -1400,8 +1528,9 @@ interface ElytraHostOptions<Collections extends readonly CollectionDef[] = reado
1400
1528
  */
1401
1529
  components?: readonly HostComponentEntry[];
1402
1530
  /**
1403
- * The public base URL of the delivery site (mirrors the config's
1404
- * `frontendUrl`) — exposed on the handle for sitemap/canonical use.
1531
+ * The public base URL of the delivery site (the config's project-level
1532
+ * `frontendUrl`). The handle's {@link ElytraHost.frontendUrl} is the RESOLVED one —
1533
+ * the bound environment's own URL when it declares one (EC-583).
1405
1534
  */
1406
1535
  frontendUrl?: string;
1407
1536
  /**
@@ -1438,18 +1567,18 @@ interface HostCanvasRouteOptions {
1438
1567
  */
1439
1568
  wrap?: (children: ReactNode, ctx: CanvasWrapContext) => ReactNode;
1440
1569
  /**
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.
1570
+ * The host's cache function for published requests (EC-620 — see
1571
+ * `CanvasRouteOptions.published`): your own `'use cache'` function that calls
1572
+ * `route.resolve(key)`, `route.applyCacheTags(outcome)` and picks its `cacheLife`.
1573
+ * Omit it and the route resolves every request fresh (dynamic, uncached).
1446
1574
  *
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.
1575
+ * The route it builds is always scoped to the kit's {@link ElytraHost.contentScope}
1576
+ * with the kit's {@link ElytraHost.contentMode} in every cache key (EC-543), and
1577
+ * there is no override (EC-579): the kit's revalidate receiver derives its tags with
1578
+ * that same `contentScope`, so one value scopes both the entries and their
1579
+ * invalidation — a disagreement would drop nothing, silently.
1451
1580
  */
1452
- cache?: false;
1581
+ published?: CanvasRouteOptions['published'];
1453
1582
  }
1454
1583
  interface LoadGlobalsOptions {
1455
1584
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
@@ -1506,19 +1635,37 @@ interface ElytraHost<Collections extends readonly CollectionDef[] = readonly Col
1506
1635
  contentMode: ElytraContentMode;
1507
1636
  /** The resolved env bag (documented names, trimmed, empty = unset). */
1508
1637
  env: ElytraHostEnv;
1509
- /** Materialized route records (a derive is run once, at factory time). */
1638
+ /**
1639
+ * The ONE resolved project (EC-583) — the options resolved through
1640
+ * `resolveProject`, the same rules the studio boots through: composed, defaulted,
1641
+ * validation slots normalized, routes materialized, every boot rule applied. The
1642
+ * kit reads its locales, collections, routes and frontend URL from here.
1643
+ */
1644
+ project: ResolvedProject;
1645
+ /**
1646
+ * The project's route issue set at boot (EC-590 — the ONE router's: route conflicts,
1647
+ * mount conflicts, reachability). Surfaced on the console at build and in dev; the
1648
+ * editorial redirects (data) are judged per snapshot by the content client's
1649
+ * `routerIssues` and in the studio.
1650
+ */
1651
+ routeIssues: readonly CmsValidationIssue[];
1652
+ /** Materialized route records (the resolved project's; a derive ran once, at factory time). */
1510
1653
  routes: RouteRecord[];
1511
1654
  /** The registered component surface — exactly the `components` option's list (EC-591). */
1512
1655
  components: HostComponents;
1513
- /** The public base URL of the delivery site, when configured. */
1656
+ /**
1657
+ * The public base URL of the delivery site, when configured — the resolved project's
1658
+ * (`projectFrontendUrl`): the bound environment's own `frontendUrl`, else the
1659
+ * project-level one. The same answer the studio's live links read (EC-583).
1660
+ */
1514
1661
  frontendUrl?: string;
1515
1662
  /**
1516
1663
  * The content loader (the seam every kit surface reads through): live
1517
1664
  * delivery snapshot when env is configured — published, or the token-gated
1518
1665
  * draft for draft-mode requests — else the fallback source.
1519
1666
  *
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
1667
+ * An UNCACHED read: the kit caches nothing itself (EC-620 — caching is the host's).
1668
+ * Under `cacheComponents`, live-mode I/O in a layout or page outside `<Suspense>` fails
1522
1669
  * `next build` — call it inside a `<Suspense>`'d component (route handlers and
1523
1670
  * `sitemap.ts` just render dynamically).
1524
1671
  */
@@ -1580,10 +1727,10 @@ declare function createElytraHost<const Collections extends readonly CollectionD
1580
1727
  * `DRAFT_PREVIEW_TOKEN` — the same single secret that gates the kit's
1581
1728
  * preview-enable route (see `host-env.ts`).
1582
1729
  *
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.
1730
+ * Routes: the config's route records (AD-3/AD-11, EC-207), exactly as resolved —
1731
+ * the backend never carries route data, and the engine derives none (EC-590: the
1732
+ * implicit "`/` → the lowest-id `page`" home fallback is gone with every other
1733
+ * engine-dictated name; a project declares its `/`, the scaffold ships one).
1587
1734
  */
1588
1735
  interface LiveContentOptions {
1589
1736
  /** Convex deployment URL, e.g. `https://your-deployment-123.convex.cloud`. */
@@ -1596,7 +1743,7 @@ interface LiveContentOptions {
1596
1743
  locales: LocaleConfigInput;
1597
1744
  /** CMS collection schema — config-owned (AD-11); drives depth-1 relation population. */
1598
1745
  collections: readonly CollectionDef[];
1599
- /** Route records — config-owned (AD-3/AD-11); merged with the home fallback. */
1746
+ /** Route records — config-owned (AD-3/AD-11), the resolved project's. */
1600
1747
  routes: readonly RouteRecord[];
1601
1748
  /** `DRAFT_PREVIEW_TOKEN` — required for draft-perspective loads. */
1602
1749
  draftToken?: string;
@@ -1616,7 +1763,6 @@ interface DeliverySnapshotPayload {
1616
1763
  documents: unknown[];
1617
1764
  assets: unknown[];
1618
1765
  }
1619
-
1620
1766
  /**
1621
1767
  * Assemble a fetched delivery-snapshot payload into a `ContentSource` — the
1622
1768
  * pure half of {@link loadLiveContent}, exported so the parsing/assembly rules
@@ -1718,4 +1864,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
1718
1864
  */
1719
1865
  declare const PACKAGE = "@elytracms/next";
1720
1866
 
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 };
1867
+ 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 };