@elytracms/next 0.0.99 → 0.0.101

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
@@ -394,6 +394,14 @@ interface CanvasWrapContext extends CanvasRequestInfo {
394
394
  * a serializable declaration).
395
395
  */
396
396
  result: ResolvePageResult;
397
+ /**
398
+ * The request's locale — the one the page resolved and renders in (EC-720). A NAME for
399
+ * `result.tags.locale`, the value the route split from the URL once and that the page,
400
+ * its metadata, listings, assets and beacon already read: never a second derivation, so
401
+ * a wrapper's chrome strings and the content it wraps cannot disagree. Hand it to
402
+ * `elytra.loadGlobals({ locale })` for chrome the wrapper reads itself.
403
+ */
404
+ locale: Locale;
397
405
  }
398
406
  /**
399
407
  * EC-618 — what the route's `analytics` slot is told about the page it renders on: the
@@ -490,6 +498,13 @@ type CanvasPageOutcome = {
490
498
  presentation?: DeliveryPresentation;
491
499
  tags: CacheTags;
492
500
  };
501
+ /**
502
+ * The outcome a page renders (EC-722): what `route.load(url)` returns once the route has
503
+ * answered a redirect or a not-found itself — a caller of it never branches on `kind`.
504
+ */
505
+ type CanvasRenderOutcome = Extract<CanvasPageOutcome, {
506
+ kind: 'render';
507
+ }>;
493
508
  /** Build the request path from a Next catch-all `slug` param. */
494
509
  declare function pathFromSlug(slug: readonly string[] | undefined): string;
495
510
 
@@ -834,6 +849,31 @@ interface CanvasRoute {
834
849
  applyCacheTags: (result: {
835
850
  tags: CacheTags;
836
851
  }) => void;
852
+ /**
853
+ * EC-722 — resolve one URL for a route of YOUR OWN and settle its status, before any
854
+ * `<Suspense>` boundary: `const outcome = await route.load('/blog/' + slug)`.
855
+ *
856
+ * It does what {@link Page} does before rendering, and nothing else: reads the request's
857
+ * perspective, resolves a published request through your
858
+ * {@link CanvasRouteOptions.published} cache function with the SAME key `Page` hands it
859
+ * (one entry; a draft resolves fresh), and answers a redirect with
860
+ * `redirect()`/`permanentRedirect()` and a missing document with `notFound()` itself —
861
+ * so what comes back is always a page to render.
862
+ *
863
+ * **Call it outside every `<Suspense>` boundary** (the route's page, not a suspended
864
+ * child). Inside one, the response status is already committed when the call settles:
865
+ * measured, a hand-written route that resolved and called `notFound()` inside
866
+ * `<Suspense>` answered a missing document with HTTP 200 (EC-567 V11). Outside, under
867
+ * Cache Components, it is legal because a published request is a cache read through
868
+ * `published` — the reason the catch-all page can stand outside a boundary too. Without
869
+ * `published`, a live-mode read outside `<Suspense>` fails `next build` under Cache
870
+ * Components (the same rule `Page` lives by); without Cache Components it is a plain
871
+ * dynamic read.
872
+ *
873
+ * Your route's own `generateMetadata` can delegate to {@link generateMetadata} with the
874
+ * catch-all slug of the same URL; `load` fires no `onTags` (the route's `Page` does).
875
+ */
876
+ load: (url: string) => Promise<CanvasRenderOutcome>;
837
877
  /**
838
878
  * `export const generateStaticParams = route.generateStaticParams` — needed in the
839
879
  * catch-all page file only when the HOST enables `cacheComponents` (that flag's
@@ -873,7 +913,8 @@ declare function requestPerspective(): Promise<Perspective>;
873
913
  /**
874
914
  * Create the canvas route for `app/[[...slug]]/page.tsx` — `{ Page, generateMetadata,
875
915
  * generateStaticParams }` to export, plus `resolve` / `applyCacheTags` for the host's
876
- * own cache function ({@link CanvasRouteOptions.published}).
916
+ * own cache function ({@link CanvasRouteOptions.published}) and `load` for a route of the
917
+ * host's own (EC-722 — {@link CanvasRoute.load}).
877
918
  */
878
919
  declare function createCanvasRoute(options: CanvasRouteOptions): CanvasRoute;
879
920
 
@@ -951,7 +992,7 @@ declare function seoMetadata(doc: Record<string, unknown> | null | undefined, ct
951
992
  * page segment's `openGraph` object REPLACES the layout's whole `openGraph`, so a
952
993
  * site-wide `type`, `siteName`, site image or `locale` declared once in the root
953
994
  * layout never reached a canvas page, and both real sites hand-rolled around it
954
- * differently (NK-039's page hook, Konditorei's post bypass). This is the one
995
+ * differently (a page hook on one, a post bypass on the other). This is the one
955
996
  * merge, applied by the canvas route to whatever builds the page's metadata (the
956
997
  * kit's default or the host's `metadata` option): every object-valued key the
957
998
  * page sets (`openGraph`, `twitter`, `robots`, `alternates`, …) is merged one
@@ -1336,7 +1377,7 @@ interface ElytraImageProps {
1336
1377
  quality?: number;
1337
1378
  className?: string;
1338
1379
  /**
1339
- * Loading strategy forwarded to `next/image` (EC-428/NK-026). `'eager'` opts an
1380
+ * Loading strategy forwarded to `next/image` (EC-428). `'eager'` opts an
1340
1381
  * image out of lazy-loading without the preload `priority` implies. Next throws
1341
1382
  * on `priority` + `loading='lazy'` together, so {@link ElytraImage} drops `lazy`
1342
1383
  * when `priority` is set (priority wins) — delivery never crashes.
@@ -1597,6 +1638,12 @@ interface SiteGlobals {
1597
1638
  interface ResolveGlobalsOptions {
1598
1639
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
1599
1640
  perspective?: Perspective;
1641
+ /**
1642
+ * The request's locale (EC-720). Default: the source's default locale. A locale the
1643
+ * project does not declare throws a located `unknown-locale` error — read silently, it
1644
+ * would serve every field from the default locale's fallback and look like a translation
1645
+ * gap rather than a wrong argument.
1646
+ */
1600
1647
  locale?: Locale;
1601
1648
  conventions?: GlobalsConventions;
1602
1649
  }
@@ -1830,6 +1877,16 @@ interface HostCanvasRouteOptions {
1830
1877
  interface LoadGlobalsOptions {
1831
1878
  /** Perspective of the chrome read. Default `'published'` (live is published-only). */
1832
1879
  perspective?: Perspective;
1880
+ /**
1881
+ * The request's locale (EC-720) — the chrome's settings, menus and link paths are read in
1882
+ * it. Default: the project's default locale, which is every page's locale on a
1883
+ * single-locale site. On a multi-locale site pass the locale the page renders in: from a
1884
+ * layout in the catch-all segment, `resolveLocale({ pathname: pathFromSlug(slug) },
1885
+ * locales)` — the SAME split the catch-all resolves the page with — or a `wrap`
1886
+ * function's `ctx.locale`. A locale the project does not declare is an error, never a
1887
+ * silent read of the default.
1888
+ */
1889
+ locale?: Locale;
1833
1890
  }
1834
1891
  /**
1835
1892
  * Which world {@link ElytraHost.loadContent} reads from.
@@ -1920,7 +1977,8 @@ interface ElytraHost<Collections extends readonly CollectionDef[] = readonly Col
1920
1977
  /**
1921
1978
  * Load + resolve the site-chrome globals (settings singleton, navigation by
1922
1979
  * slot alias, link/asset resolution — see `host-globals.ts`). One content
1923
- * load per call; published perspective by default.
1980
+ * load per call; published perspective and the project's default locale by
1981
+ * default — pass the request's `locale` on a multi-locale site (EC-720).
1924
1982
  *
1925
1983
  * An UNCACHED read, like {@link loadContent}: under `cacheComponents`, render the
1926
1984
  * chrome that awaits it inside `<Suspense>` — and keep the canvas page OUTSIDE
@@ -2132,4 +2190,4 @@ declare function evaluatePreviewRequest(requestUrl: string | URL, expectedToken:
2132
2190
  */
2133
2191
  declare const PACKAGE = "@elytracms/next";
2134
2192
 
2135
- export { type AssetImageLoaderOptions, type BakedListing, type BotGateVerdict, CANVAS_VALIDATION_SLUG, type CanvasBeaconProps, 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 FixedWindowLimiter, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type IngestBotGate, type IngestEnrich, type IngestEvaluation, type IngestRouteHandlers, type IngestRouteOptions, 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, canvasBeaconProps, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createFixedWindowLimiter, createIngestRoute, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultIngestClientKey, defineHostComponents, docCacheTag, elytraHostImage, evaluateIngestRequest, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isCrossOrigin, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, readCappedBody, requestPerspective, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, seoContextOf, seoMetadata, seoObjectOf, withNextImage };
2193
+ export { type AssetImageLoaderOptions, type BakedListing, type BotGateVerdict, CANVAS_VALIDATION_SLUG, type CanvasBeaconProps, type CanvasCacheKey, type CanvasContentRequest, type CanvasPageOutcome, type CanvasRenderOutcome, 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 FixedWindowLimiter, type GlobalsConventions, type HostCanvasRouteOptions, type HostComponent, type HostComponents, type IngestBotGate, type IngestEnrich, type IngestEvaluation, type IngestRouteHandlers, type IngestRouteOptions, 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, canvasBeaconProps, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createFixedWindowLimiter, createIngestRoute, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultIngestClientKey, defineHostComponents, docCacheTag, elytraHostImage, evaluateIngestRequest, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isCrossOrigin, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, readCappedBody, requestPerspective, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, seoContextOf, seoMetadata, seoObjectOf, withNextImage };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { registerComponents, createRenderContext, renderDocumentBodyItems, docum
2
2
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
3
3
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
4
  import { cacheTags, isDocumentHistory, assetWithoutUrlIssue, assetMetaDriftIssue, resolvedAsset, parseDeliveredDocuments, parseDeliveredRedirects, createContentClient, createContentContext } from '@elytracms/core/content';
5
- import { DEFAULT_SEO_FIELD, parseLocaleConfig, documentKey, readLiveRedirects, SEO_SUBFIELDS, splitLocalePath, DRAFT_PERSPECTIVE, splitPath, localeUrlPath, variantOf, DRAFT_VARIANT, createRouter, deliverCompositionProps, presentationBodyFields } from '@elytracms/core/cms-core';
5
+ import { DEFAULT_SEO_FIELD, parseLocaleConfig, documentKey, readLiveRedirects, SEO_SUBFIELDS, DRAFT_PERSPECTIVE, splitPath, localeUrlPath, variantOf, DRAFT_VARIANT, createRouter, splitLocalePath, deliverCompositionProps, presentationBodyFields, validateLocale } from '@elytracms/core/cms-core';
6
6
  export { DRAFT_PERSPECTIVE, documentSchema, localeConfigSchema, localeUrlPath, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection, variantOf } from '@elytracms/core/cms-core';
7
7
  import { assetMetaLens, assetMetaDrift, assetReadLocale, redirectRecordKind } from '@elytracms/core/persistence';
8
8
  import { propFieldLookupOf } from '@elytracms/core/component-registry';
@@ -309,7 +309,8 @@ function canvasWrapContext(result, info) {
309
309
  ...info,
310
310
  ...collection !== void 0 ? { collection } : {},
311
311
  document: result.document,
312
- result
312
+ result,
313
+ locale: result.tags.locale
313
314
  };
314
315
  }
315
316
  function canvasBeaconProps(result, info) {
@@ -442,10 +443,13 @@ function populatePageLinks(body, client, registry) {
442
443
  }
443
444
  return { links, docKeys: Object.keys(refs) };
444
445
  }
445
- async function resolveCanvasPage(source, url, perspective, options = {}) {
446
+ function resolveRequestPage(source, url, perspective) {
446
447
  const { locale, path } = splitLocalePath(url, source.locales);
447
448
  const client = source.client({ perspective, locale });
448
- const resolved = client.resolvePage(path);
449
+ return { locale, client, resolved: client.resolvePage(path) };
450
+ }
451
+ async function resolveCanvasPage(source, url, perspective, options = {}) {
452
+ const { locale, client, resolved } = resolveRequestPage(source, url, perspective);
449
453
  if (resolved.status === "redirect" && resolved.redirect) {
450
454
  return {
451
455
  kind: "redirect",
@@ -626,41 +630,49 @@ function createCanvasRoute(options) {
626
630
  if (perspective !== "published" || !options.published) return resolveFresh(url, perspective);
627
631
  return options.published(keyFor(url));
628
632
  };
629
- const Page = async (props) => {
630
- const { slug } = await props.params;
631
- const url = pathFromSlug(slug);
632
- if (url === CANVAS_VALIDATION_PATH) return notFound();
633
- const perspective = await requestPerspective();
634
- const outcome = await loadOutcome(url, perspective);
635
- options.onTags?.(outcome.tags, { url, perspective });
633
+ const settle = (outcome) => {
636
634
  switch (outcome.kind) {
637
635
  case "redirect":
638
636
  return outcome.permanent ? permanentRedirect(outcome.target) : redirect(outcome.target);
639
637
  case "not-found":
640
638
  return notFound();
641
- case "render": {
642
- const rendered = /* @__PURE__ */ jsx(
643
- CanvasRenderer,
644
- {
645
- result: outcome.result,
646
- components: options.components,
647
- assets: outcome.assets,
648
- relations: outcome.relations,
649
- listings: outcome.listings,
650
- links: outcome.links,
651
- presentation: outcome.presentation
652
- }
653
- );
654
- const page = options.wrap ? options.wrap(rendered, canvasWrapContext(outcome.result, { url, perspective })) : rendered;
655
- const Beacon = options.analytics;
656
- if (!Beacon || perspective !== "published") return page;
657
- return /* @__PURE__ */ jsxs(Fragment, { children: [
658
- page,
659
- /* @__PURE__ */ jsx(Beacon, { ...canvasBeaconProps(outcome.result, { url}) })
660
- ] });
661
- }
639
+ case "render":
640
+ return outcome;
662
641
  }
663
642
  };
643
+ const load = async (url) => {
644
+ if (url === CANVAS_VALIDATION_PATH) return notFound();
645
+ const perspective = await requestPerspective();
646
+ return settle(await loadOutcome(url, perspective));
647
+ };
648
+ const Page = async (props) => {
649
+ const { slug } = await props.params;
650
+ const url = pathFromSlug(slug);
651
+ if (url === CANVAS_VALIDATION_PATH) return notFound();
652
+ const perspective = await requestPerspective();
653
+ const loaded = await loadOutcome(url, perspective);
654
+ options.onTags?.(loaded.tags, { url, perspective });
655
+ const outcome = settle(loaded);
656
+ const rendered = /* @__PURE__ */ jsx(
657
+ CanvasRenderer,
658
+ {
659
+ result: outcome.result,
660
+ components: options.components,
661
+ assets: outcome.assets,
662
+ relations: outcome.relations,
663
+ listings: outcome.listings,
664
+ links: outcome.links,
665
+ presentation: outcome.presentation
666
+ }
667
+ );
668
+ const page = options.wrap ? options.wrap(rendered, canvasWrapContext(outcome.result, { url, perspective })) : rendered;
669
+ const Beacon = options.analytics;
670
+ if (!Beacon || perspective !== "published") return page;
671
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
672
+ page,
673
+ /* @__PURE__ */ jsx(Beacon, { ...canvasBeaconProps(outcome.result, { url}) })
674
+ ] });
675
+ };
664
676
  const pageMetadata = async (props) => {
665
677
  const { slug } = await props.params;
666
678
  const url = pathFromSlug(slug);
@@ -683,7 +695,7 @@ function createCanvasRoute(options) {
683
695
  const generateStaticParams = async () => [
684
696
  { slug: [...CANVAS_VALIDATION_SLUG] }
685
697
  ];
686
- return { Page, generateMetadata, resolve, applyCacheTags, generateStaticParams };
698
+ return { Page, generateMetadata, resolve, applyCacheTags, load, generateStaticParams };
687
699
  }
688
700
  function originOf(baseUrl) {
689
701
  return baseUrl.replace(/\/+$/, "");
@@ -1189,9 +1201,14 @@ async function loadLiveContent(options, perspective) {
1189
1201
  }
1190
1202
  return assembleLiveSource(payload, options, perspective);
1191
1203
  }
1192
-
1193
- // src/host-globals.ts
1194
1204
  var DEFAULT_ITEMS_FIELDS = ["items", "links"];
1205
+ function assertDeclaredLocale(source, locale) {
1206
+ const [issue] = validateLocale(source.locales, locale);
1207
+ if (!issue) return;
1208
+ throw new Error(
1209
+ `Elytra chrome globals: ${issue.code} at ${issue.path.join(".")} \u2014 the locale "${locale}" is not one of this project's locales (it declares ${source.locales.locales.join(", ")}). Pass the locale the page renders in \u2014 resolveLocale({ pathname }, locales) splits it from the URL exactly as the catch-all does \u2014 or omit it for the default locale.`
1210
+ );
1211
+ }
1195
1212
  function record(value) {
1196
1213
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1197
1214
  }
@@ -1207,7 +1224,8 @@ function resolveGlobals(source, options = {}) {
1207
1224
  const settingsCollection = conventions.settingsCollection ?? "settings";
1208
1225
  const navigationCollection = conventions.navigationCollection ?? "navigation";
1209
1226
  const itemsFields = conventions.navigationItemsFields ?? DEFAULT_ITEMS_FIELDS;
1210
- const client = source.client({ perspective, ...options.locale ? { locale: options.locale } : {} });
1227
+ if (options.locale !== void 0) assertDeclaredLocale(source, options.locale);
1228
+ const client = source.client({ perspective, ...options.locale !== void 0 ? { locale: options.locale } : {} });
1211
1229
  const assets = source.assets?.(options.locale) ?? [];
1212
1230
  const assetById = new Map(assets.map((asset) => [asset.id, asset]));
1213
1231
  const listDocs = (collection) => {
@@ -1445,6 +1463,8 @@ function createElytraHost(options) {
1445
1463
  const source = await loadContent({ perspective });
1446
1464
  return resolveGlobals(source, {
1447
1465
  perspective,
1466
+ // EC-720: the request's locale, judged against the project's locales by the core.
1467
+ ...globalsOptions.locale !== void 0 ? { locale: globalsOptions.locale } : {},
1448
1468
  ...globals ? { conventions: globals } : {}
1449
1469
  });
1450
1470
  };
@@ -1481,9 +1501,7 @@ function createElytraHost(options) {
1481
1501
  const pageOpensFullBleed = async (slug) => {
1482
1502
  const perspective = await requestPerspective();
1483
1503
  const source = await loadContent({ perspective });
1484
- const { locale, path } = splitLocalePath(pathFromSlug(slug), source.locales);
1485
- const client = source.client({ perspective, locale });
1486
- const resolved = client.resolvePage(path);
1504
+ const { resolved } = resolveRequestPage(source, pathFromSlug(slug), perspective);
1487
1505
  return firstBlockFullBleed(resolved.document, components.registry);
1488
1506
  };
1489
1507
  const frontendUrl = projectFrontendUrl(project);