@elytracms/next 0.0.80 → 0.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { ComponentRegistry } from '@elytracms/core/component-registry';
2
2
  export { defineComponent } from '@elytracms/core/component-registry';
3
- import { basePrimitives, getImplementation, renderComposition, ImageManifest, serializeListingQuery, RenderErrorBoundary, collectReferencedAssetIds, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, normalizeImageSource } from '@elytracms/core/runtime-renderer';
3
+ import { basePrimitives, getImplementation, renderDocumentBodyItems, documentBodyItems, ImageManifest, serializeListingQuery, RenderErrorBoundary, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, normalizeImageSource } from '@elytracms/core/runtime-renderer';
4
4
  import { isValidElement } from 'react';
5
5
  import { jsx, Fragment } from 'react/jsx-runtime';
6
6
  import { cacheTags, isSectionSourceId, parseCmsSourceQueryConfig, listQueryOfCmsConfig, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
7
- import { documentKey, splitPath, hierarchyMount, composePath, mergeRouteRecords, fallbackHomeRoutes, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
8
- export { documentSchema, fallbackHomeRoutes, localeConfigSchema, mergeRouteRecords, redirectRecordSchema, routeRecordSchema } from '@elytracms/core/cms-core';
7
+ import { documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
8
+ export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, routeRecordSchema, splitLocalePath } from '@elytracms/core/cms-core';
9
9
  import { revalidateTag, unstable_cache } from 'next/cache';
10
10
  import { draftMode } from 'next/headers';
11
11
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
@@ -148,11 +148,16 @@ async function CanvasRenderer(props) {
148
148
  };
149
149
  const document = result.document;
150
150
  if (document) {
151
- const composition = compositionOf(document);
152
- return /* @__PURE__ */ jsx(Fragment, { children: renderComposition(composition, ctx) });
151
+ return /* @__PURE__ */ jsx(Fragment, { children: renderDocumentBodyItems(bodyItemsOf(document, props.presentation), ctx) });
153
152
  }
154
153
  return /* @__PURE__ */ jsx(MissingDocumentFallback, {});
155
154
  }
155
+ function bodyItemsOf(document, presentation) {
156
+ if (!document) return [];
157
+ const fields = presentation?.body ?? [];
158
+ if (fields.length > 0) return fields.flatMap((name) => documentBodyItems(document[name]));
159
+ return documentBodyItems(compositionOf(document));
160
+ }
156
161
  function compositionOf(document) {
157
162
  const body = document["body"];
158
163
  if (Array.isArray(body) || isComponentNodeValue(body)) return body;
@@ -164,11 +169,8 @@ function compositionOf(document) {
164
169
  }
165
170
  return void 0;
166
171
  }
167
- function compositionNodesOf(document) {
168
- if (!document) return [];
169
- const composition = compositionOf(document);
170
- const candidates = Array.isArray(composition) ? composition : [composition];
171
- return candidates.filter((node) => isComponentNodeValue(node));
172
+ function compositionNodesOf(document, presentation) {
173
+ return documentBodyCompositionNodes(bodyItemsOf(document, presentation));
172
174
  }
173
175
  function isComponentNodeValue(value) {
174
176
  return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.componentId === "string";
@@ -219,9 +221,8 @@ function materializeSourcePayloads(input) {
219
221
  }
220
222
  return { payloads, tags };
221
223
  }
222
- var DEFAULT_LOCALES = { default: "en", locales: ["en"] };
223
224
  function createStaticContentSource(data) {
224
- const locales = data.locales ?? DEFAULT_LOCALES;
225
+ const locales = data.locales;
225
226
  const routes = data.routes ?? [];
226
227
  const redirects = data.redirects ?? [];
227
228
  const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
@@ -265,10 +266,15 @@ function createStaticContentSource(data) {
265
266
  // dimensions. Baked into the (serializable) render outcome by the route
266
267
  // helper, so a cached page keeps its images during a backend outage.
267
268
  assets: () => [...assets.values()].map((record2) => resolvedAsset(record2, record2.url ?? assetUrl(record2))),
268
- ...data.sources ? { sources: data.sources } : {}
269
+ ...data.sources ? { sources: data.sources } : {},
270
+ // EC-499: the schema the route helper reads a collection's declared
271
+ // `presentation` from (which field is the delivery body, which component
272
+ // wraps it). Already prefetched for the lookup above — exposing it costs
273
+ // nothing and keeps the declaration reachable without a second wiring seam.
274
+ ...data.collections ? { collections: data.collections } : {}
269
275
  };
270
276
  }
271
- async function createContentSnapshot(adapter, projectId, options = {}) {
277
+ async function createContentSnapshot(adapter, projectId, options) {
272
278
  const schema = await adapter.cms.findSchema(projectId);
273
279
  const documentRecords = await adapter.cms.listDocuments(projectId);
274
280
  const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
@@ -297,7 +303,7 @@ async function createContentSnapshot(adapter, projectId, options = {}) {
297
303
  });
298
304
  return createStaticContentSource({
299
305
  projectId,
300
- ...options.locales ? { locales: options.locales } : {},
306
+ locales: options.locales,
301
307
  ...options.routes ? { routes: options.routes } : {},
302
308
  redirects: options.redirects ? [...options.redirects] : redirects,
303
309
  ...options.sources ? { sources: options.sources } : {},
@@ -307,37 +313,57 @@ async function createContentSnapshot(adapter, projectId, options = {}) {
307
313
  assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
308
314
  });
309
315
  }
316
+ function canvasWrapContext(result, info) {
317
+ const collection = result.document?.collection;
318
+ return {
319
+ ...info,
320
+ ...collection !== void 0 ? { collection } : {},
321
+ document: result.document,
322
+ result
323
+ };
324
+ }
310
325
  function pathFromSlug(slug) {
311
326
  return "/" + (slug ?? []).join("/");
312
327
  }
313
- function splitLocalePath(url, locales) {
314
- const segments = splitPath(url);
315
- const first = segments[0];
316
- if (first !== void 0 && first !== locales.default && locales.locales.includes(first)) {
317
- return { locale: first, path: "/" + segments.slice(1).join("/") };
318
- }
319
- return { locale: locales.default, path: "/" + segments.join("/") };
320
- }
321
- function scopeAssetsToPage(allAssets, registry, result, resolveRelation) {
328
+ function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
322
329
  if (allAssets.length === 0) return allAssets;
323
330
  const ids = /* @__PURE__ */ new Set();
324
331
  const seen = /* @__PURE__ */ new Set();
325
- for (const node of compositionNodesOf(result.document)) {
332
+ for (const node of body.nodes) {
326
333
  collectReferencedAssetIds(node, registry, ids, resolveRelation, seen);
327
334
  }
335
+ for (const value of body.richTexts) {
336
+ collectRichTextRefsInValue(value, { onAsset: (id) => ids.add(id) });
337
+ }
328
338
  const ogImage = result.document?.["seoOgImage"];
329
339
  const ogImageId = typeof ogImage === "string" ? ogImage : ogImage && typeof ogImage === "object" && typeof ogImage.id === "string" ? ogImage.id : null;
330
340
  if (ogImageId) ids.add(ogImageId);
331
341
  return ids.size === 0 ? [] : allAssets.filter((asset) => ids.has(asset.id));
332
342
  }
333
- function pageCompositionNodes(result) {
334
- return [...compositionNodesOf(result.document)];
343
+ function pageBodyOf(result, presentation) {
344
+ const items2 = bodyItemsOf(result.document, presentation);
345
+ const richTexts = documentBodyRichTextValues(items2);
346
+ return {
347
+ nodes: [
348
+ ...documentBodyCompositionNodes(items2),
349
+ ...richTexts.flatMap((value) => richTextEmbedsInValue(value))
350
+ ],
351
+ richTexts
352
+ };
353
+ }
354
+ function presentationFor(source, result) {
355
+ const collectionId = result.document?.collection;
356
+ if (!collectionId || !source.collections) return void 0;
357
+ const collection = source.collections.find((candidate) => candidate.id === collectionId);
358
+ if (!collection?.presentation) return void 0;
359
+ const body = presentationBodyFields(collection);
360
+ return body.length > 0 ? { body: [...body] } : void 0;
335
361
  }
336
- function populatePageRelations(result, client, registry) {
362
+ function populatePageRelations(body, client, registry) {
337
363
  if (!registry) return { relations: {}, docKeys: [] };
338
364
  const refKeys = /* @__PURE__ */ new Set();
339
365
  const relations = collectReferencedRelations(
340
- pageCompositionNodes(result),
366
+ body.nodes,
341
367
  registry,
342
368
  (ref) => client.resolveRelation(ref),
343
369
  {},
@@ -345,11 +371,11 @@ function populatePageRelations(result, client, registry) {
345
371
  );
346
372
  return { relations, docKeys: [...refKeys] };
347
373
  }
348
- function populatePageListings(result, client, registry) {
374
+ function populatePageListings(body, client, registry) {
349
375
  if (!registry) return { listings: {}, tags: [] };
350
376
  const tags = [];
351
377
  const listings = collectReferencedListings(
352
- pageCompositionNodes(result),
378
+ body.nodes,
353
379
  registry,
354
380
  (query) => {
355
381
  const listed = client.listDocuments(query.collection, {
@@ -367,13 +393,20 @@ function populatePageListings(result, client, registry) {
367
393
  );
368
394
  return { listings, tags };
369
395
  }
370
- function populatePageLinks(result, client, registry) {
396
+ function populatePageLinks(body, client, registry) {
371
397
  if (!registry) return { links: {}, docKeys: [] };
372
398
  const refs = collectReferencedRichTextLinks(
373
- pageCompositionNodes(result),
399
+ body.nodes,
374
400
  registry,
375
401
  (ref) => client.resolveRelation(ref)
376
402
  );
403
+ for (const value of body.richTexts) {
404
+ collectRichTextRefsInValue(value, {
405
+ onLink: (ref) => {
406
+ refs[`${ref.collection}:${ref.id}`] = ref;
407
+ }
408
+ });
409
+ }
377
410
  const links = {};
378
411
  for (const [key, ref] of Object.entries(refs)) {
379
412
  const url = client.resolveLink(ref);
@@ -416,11 +449,14 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
416
449
  });
417
450
  payloads = { ...payloads, ...custom };
418
451
  }
452
+ const presentation = presentationFor(source, result);
453
+ const body = pageBodyOf(result, presentation);
419
454
  const allAssets = source.assets?.() ?? [];
420
455
  const bakedAssets = options.registry ? scopeAssetsToPage(
421
456
  allAssets,
422
457
  options.registry,
423
458
  result,
459
+ body,
424
460
  (ref) => client.resolveRelation(ref)
425
461
  ) : allAssets;
426
462
  const assetTags = cacheTags({
@@ -428,17 +464,17 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
428
464
  locale: result.tags.locale
429
465
  });
430
466
  const { relations, docKeys: relationDocKeys } = populatePageRelations(
431
- result,
467
+ body,
432
468
  client,
433
469
  options.registry
434
470
  );
435
471
  const relationTags = cacheTags({ docs: relationDocKeys, locale: result.tags.locale });
436
472
  const { listings, tags: listingTags } = populatePageListings(
437
- result,
473
+ body,
438
474
  client,
439
475
  options.registry
440
476
  );
441
- const { links, docKeys: linkDocKeys } = populatePageLinks(result, client, options.registry);
477
+ const { links, docKeys: linkDocKeys } = populatePageLinks(body, client, options.registry);
442
478
  const linkTags = cacheTags({ docs: linkDocKeys, locale: result.tags.locale });
443
479
  return {
444
480
  kind: "render",
@@ -448,6 +484,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
448
484
  relations,
449
485
  listings,
450
486
  links,
487
+ ...presentation ? { presentation } : {},
451
488
  tags: mergeCacheTags(
452
489
  result.tags,
453
490
  ...extraTags,
@@ -606,8 +643,8 @@ function createCanvasRoute(options) {
606
643
  return outcome.permanent ? permanentRedirect(outcome.target) : redirect(outcome.target);
607
644
  case "not-found":
608
645
  return notFound();
609
- case "render":
610
- return /* @__PURE__ */ jsx(
646
+ case "render": {
647
+ const rendered = /* @__PURE__ */ jsx(
611
648
  CanvasRenderer,
612
649
  {
613
650
  result: outcome.result,
@@ -616,9 +653,13 @@ function createCanvasRoute(options) {
616
653
  assets: outcome.assets,
617
654
  relations: outcome.relations,
618
655
  listings: outcome.listings,
619
- links: outcome.links
656
+ links: outcome.links,
657
+ presentation: outcome.presentation
620
658
  }
621
659
  );
660
+ if (!options.wrap) return rendered;
661
+ return options.wrap(rendered, canvasWrapContext(outcome.result, { url, perspective }));
662
+ }
622
663
  }
623
664
  };
624
665
  const generateMetadata = async (props) => {
@@ -653,10 +694,8 @@ function fillPattern(pattern, params) {
653
694
  }
654
695
  return "/" + segments.join("/");
655
696
  }
656
- function absoluteUrl(baseUrl, locale, defaultLocale, path) {
657
- const origin = baseUrl.replace(/\/+$/, "");
658
- if (locale === defaultLocale) return origin + path;
659
- return origin + (path === "/" ? `/${locale}` : `/${locale}${path}`);
697
+ function originOf(baseUrl) {
698
+ return baseUrl.replace(/\/+$/, "");
660
699
  }
661
700
  function canvasSitemapEntries(source, options) {
662
701
  const routes = options.routes ?? source.routes ?? [];
@@ -721,14 +760,32 @@ function canvasSitemapEntries(source, options) {
721
760
  paths.push(path);
722
761
  }
723
762
  }
763
+ let unresolved = 0;
724
764
  for (const path of paths) {
725
765
  const result = client.resolvePage(path, locale);
726
- if (result.status !== "ok") continue;
766
+ if (result.status !== "ok") {
767
+ if (result.status !== "redirect") unresolved += 1;
768
+ continue;
769
+ }
727
770
  if (result.document?.["seoNoindex"] === true) continue;
771
+ const { urlPath, addressable } = localeUrlPath(path, locale, locales);
772
+ if (!addressable) {
773
+ skip(
774
+ route,
775
+ `"${path}" (locale "${locale}") has no addressable URL \u2014 its first segment is read as a locale prefix`
776
+ );
777
+ continue;
778
+ }
728
779
  const perLocale = byPath.get(path) ?? /* @__PURE__ */ new Map();
729
- perLocale.set(locale, absoluteUrl(options.baseUrl, locale, locales.default, path));
780
+ perLocale.set(locale, originOf(options.baseUrl) + urlPath);
730
781
  byPath.set(path, perLocale);
731
782
  }
783
+ if (mount && paths.length > 0 && unresolved === paths.length) {
784
+ skip(
785
+ route,
786
+ `hierarchy mount over "${mount.collection}" composed ${paths.length} path(s) in locale "${locale}", none of which the resolver serves`
787
+ );
788
+ }
732
789
  }
733
790
  }
734
791
  const entries = [];
@@ -810,7 +867,7 @@ function createRevalidateRoute(options) {
810
867
  { status: evaluation.status, headers: cors }
811
868
  );
812
869
  }
813
- for (const tag of evaluation.payload.tags) revalidateTag(tag, "max");
870
+ for (const tag of evaluation.payload.tags) revalidateTag(tag, { expire: 0 });
814
871
  return Response.json({ revalidated: evaluation.payload.tags }, { headers: cors });
815
872
  },
816
873
  async OPTIONS() {
@@ -914,8 +971,8 @@ function elytraHostImage(options = {}) {
914
971
  }
915
972
 
916
973
  // src/full-bleed.ts
917
- function firstBlockFullBleed(document, registry) {
918
- const first = compositionNodesOf(document)[0];
974
+ function firstBlockFullBleed(document, registry, presentation) {
975
+ const first = compositionNodesOf(document, presentation)[0];
919
976
  if (!first) return false;
920
977
  return registry.getManifest(first.componentId)?.presentation?.fullBleed === true;
921
978
  }
@@ -1299,6 +1356,6 @@ function createElytraHost(options) {
1299
1356
  }
1300
1357
  var PACKAGE = "@elytracms/next";
1301
1358
 
1302
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError, splitLocalePath };
1359
+ export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectBindingSourceIds, collectionCacheTag, createAssetImageLoader, createBindingResolver, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defaultBindingPayloads, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, isSourcePayloadError, loadCachedEntry, loadLiveContent, materializeSourcePayloads, mergeCacheTags, nextCacheTags, nextImagePrimitive, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, resolvePayloadToken, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody, sourcePayloadError };
1303
1360
  //# sourceMappingURL=index.js.map
1304
1361
  //# sourceMappingURL=index.js.map