@elytracms/next 0.0.90 → 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.js CHANGED
@@ -1,34 +1,26 @@
1
- import { ComponentRegistry } from '@elytracms/core/component-registry';
2
- export { defineComponent } from '@elytracms/core/component-registry';
3
- import { basePrimitives, getImplementation, registerImplementations, renderDocumentBodyItems, documentBodyItems, ImageManifest, serializeListingQuery, RenderErrorBoundary, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, normalizeImageSource } from '@elytracms/core/runtime-renderer';
4
- import { isValidElement } from 'react';
1
+ import { registerComponents, renderDocumentBodyItems, documentBodyItems, serializeListingQuery, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, RenderErrorBoundary, nodeListingOf, listingRequestOf } from '@elytracms/core/runtime-renderer';
2
+ import { Fragment as Fragment$1, isValidElement } from 'react';
5
3
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
6
- import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
7
5
  import { cacheTags, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
8
- import { parseLocaleConfig, documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
- export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
10
- import { revalidateTag, unstable_cache } from 'next/cache';
6
+ import { DEFAULT_SEO_FIELD, parseLocaleConfig, documentKey, liveRedirectRecords, SEO_SUBFIELDS, splitLocalePath, DRAFT_PERSPECTIVE, splitPath, localeUrlPath, variantOf, DRAFT_VARIANT, createRouter, presentationBodyFields, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
7
+ export { DRAFT_PERSPECTIVE, documentSchema, localeConfigSchema, localeUrlPath, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection, variantOf } from '@elytracms/core/cms-core';
8
+ import { revalidateTag, cacheTag } from 'next/cache';
11
9
  import { draftMode } from 'next/headers';
12
10
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
13
11
  import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
14
12
  import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE_HEADER } from '@elytracms/core/revalidation';
15
13
  export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
16
14
  import NextImage from 'next/image';
15
+ import { normalizeImageSource } from '@elytracms/components';
16
+ import { projectFrontendUrl, resolveProject } from '@elytracms/core/studio-core';
17
+ export { defineComponent } from '@elytracms/core/component-registry';
17
18
  export { z } from 'zod';
18
19
 
19
20
  // src/components.ts
20
- function defineHostComponents(components = [], options = {}) {
21
- const includeBase = options.includeBasePrimitives !== false;
22
- const base = includeBase ? basePrimitives() : { manifests: [], implementations: {} };
23
- const registry = new ComponentRegistry([...base.manifests, ...components.map((c) => c.manifest)]);
24
- const implementations = {};
25
- for (const manifest of base.manifests) {
26
- const implementation = getImplementation(base.implementations, manifest.id);
27
- if (implementation) implementations[manifest.id] = implementation;
28
- }
29
- const host = registerImplementations(components);
30
- Object.assign(implementations, host.implementations);
31
- return { registry, implementations, issues: [...registry.issues, ...host.issues] };
21
+ function defineHostComponents(components = []) {
22
+ const { registry, implementations } = registerComponents(components);
23
+ return { registry, implementations };
32
24
  }
33
25
  function relationResolver(relations) {
34
26
  return (ref) => relations[`${ref.collection}:${ref.id}`] ?? null;
@@ -37,7 +29,22 @@ function linkResolver(links) {
37
29
  return (ref) => links[`${ref.collection}:${ref.id}`] ?? null;
38
30
  }
39
31
  function listingResolver(listings) {
40
- return (query) => listings[serializeListingQuery(query)] ?? null;
32
+ return (query) => {
33
+ const baked = listings[serializeListingQuery(query)];
34
+ return Array.isArray(baked) ? baked : null;
35
+ };
36
+ }
37
+ function listingFailureOf(node, components, listings) {
38
+ const manifest = components.registry.getManifest(node.componentId);
39
+ if (!manifest) return null;
40
+ const listing = nodeListingOf(node, manifest, components.registry);
41
+ if (!listing || listing.state === "awaiting") return null;
42
+ if (listing.state === "invalid") return listing.messages;
43
+ const baked = listings?.[serializeListingQuery(listing.query)];
44
+ return baked && !Array.isArray(baked) ? baked.errors.map((error) => error.message) : null;
45
+ }
46
+ function InvalidListingNotice(props) {
47
+ return /* @__PURE__ */ jsx("div", { "data-ec-fallback": "invalid-listing", "data-ec-node-id": props.nodeId, role: "note", children: `This listing can't run: ${props.messages.join(" ")}` });
41
48
  }
42
49
  function assetResolver(assets) {
43
50
  const byId = new Map(assets.map((asset) => [asset.id, asset]));
@@ -49,7 +56,7 @@ function assetResolver(assets) {
49
56
  height: asset.height,
50
57
  alt: asset.alt,
51
58
  focalPoint: asset.focalPoint,
52
- // EC-420: carry the LQIP so a placed base.Image blurs up on delivery.
59
+ // EC-420: carry the LQIP so a placed Image blurs up on delivery.
53
60
  lqip: asset.lqip,
54
61
  meta: asset.meta
55
62
  } : null;
@@ -86,7 +93,15 @@ async function CanvasRenderer(props) {
86
93
  // so a host that did not opt in degrades a reference link to the visible
87
94
  // `unresolved-ref` issue (EC-290) rather than a wrong href.
88
95
  ...props.links ? { resolveLink: linkResolver(props.links) } : {},
89
- decorateNode: stripServerErrorBoundary
96
+ decorateNode: (node, rendered) => {
97
+ const stripped = stripServerErrorBoundary(node, rendered);
98
+ const failure = listingFailureOf(node, components, props.listings);
99
+ if (!failure) return stripped;
100
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
101
+ /* @__PURE__ */ jsx(InvalidListingNotice, { nodeId: node.id, messages: failure }),
102
+ stripped
103
+ ] }, node.id);
104
+ }
90
105
  };
91
106
  const document = result.document;
92
107
  if (document) {
@@ -186,17 +201,11 @@ function createStaticContentSource(data) {
186
201
  };
187
202
  }
188
203
  async function createContentSnapshot(adapter, projectId, options) {
189
- const schema = await adapter.cms.findSchema(projectId);
190
204
  const documentRecords = await adapter.cms.listDocuments(projectId);
191
205
  const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
192
206
  const assetRecords = await adapter.assets.listAssets(projectId);
193
207
  const redirectRecords = await adapter.redirects.listRedirects(projectId);
194
- const redirects = redirectRecords.filter((record2) => record2.enabled).map((record2) => ({
195
- id: record2.id,
196
- from: record2.from,
197
- to: record2.to,
198
- permanent: record2.permanent
199
- }));
208
+ const redirects = liveRedirectRecords(redirectRecords);
200
209
  const versionsByKey = /* @__PURE__ */ new Map();
201
210
  for (const record2 of versionRecords) {
202
211
  const key = `${record2.collection}:${record2.docId}`;
@@ -217,12 +226,89 @@ async function createContentSnapshot(adapter, projectId, options) {
217
226
  locales: options.locales,
218
227
  ...options.routes ? { routes: options.routes } : {},
219
228
  redirects: options.redirects ? [...options.redirects] : redirects,
220
- ...schema ? { collections: schema.collections } : {},
229
+ collections: options.collections,
221
230
  documents,
222
231
  assets: assetRecords,
223
232
  assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
224
233
  });
225
234
  }
235
+ function seoContextOf(result, assets = []) {
236
+ return {
237
+ seoField: result.seoField ?? null,
238
+ title: result.title ?? null,
239
+ locale: result.tags.locale,
240
+ assets
241
+ };
242
+ }
243
+ function str(value) {
244
+ return typeof value === "string" && value.length > 0 ? value : null;
245
+ }
246
+ function looksLikeUrl(value) {
247
+ return /^(https?:)?\/\//.test(value) || value.startsWith("/");
248
+ }
249
+ function resolveSeoImage(value, assets) {
250
+ if (value && typeof value === "object") {
251
+ const url = value.url;
252
+ return typeof url === "string" && url.length > 0 ? url : null;
253
+ }
254
+ if (typeof value !== "string" || value.length === 0) return null;
255
+ if (looksLikeUrl(value)) return value;
256
+ return assets.find((asset) => asset.id === value)?.url ?? null;
257
+ }
258
+ function openGraphLocale(locale) {
259
+ return locale.includes("-") ? locale.replace(/-/g, "_") : null;
260
+ }
261
+ function seoObjectOf(doc, seoField = DEFAULT_SEO_FIELD) {
262
+ if (!doc || seoField === null) return {};
263
+ const value = doc[seoField ?? DEFAULT_SEO_FIELD];
264
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
265
+ }
266
+ function seoMetadata(doc, ctx = {}) {
267
+ const seo = seoObjectOf(doc, ctx.seoField);
268
+ const assets = ctx.assets ?? [];
269
+ const seoTitle = str(seo[SEO_SUBFIELDS.title]);
270
+ const description = str(seo[SEO_SUBFIELDS.description]);
271
+ const ogTitle = str(seo[SEO_SUBFIELDS.ogTitle]);
272
+ const ogDescription = str(seo[SEO_SUBFIELDS.ogDescription]);
273
+ const image = resolveSeoImage(seo[SEO_SUBFIELDS.image], assets);
274
+ const noindex = seo[SEO_SUBFIELDS.noindex] === true;
275
+ const canonical = str(seo[SEO_SUBFIELDS.canonical]);
276
+ const docTitle = ctx.title !== void 0 ? str(ctx.title) : str(doc?.["title"]) ?? str(doc?.["name"]);
277
+ const title = seoTitle ?? docTitle;
278
+ const metadata = {};
279
+ if (title !== null) metadata.title = title;
280
+ if (description) metadata.description = description;
281
+ if (doc) {
282
+ const effectiveOgTitle = ogTitle ?? title;
283
+ const effectiveOgDescription = ogDescription ?? description;
284
+ const ogLocale = ctx.locale !== void 0 ? openGraphLocale(ctx.locale) : null;
285
+ metadata.openGraph = {
286
+ ...effectiveOgTitle !== null ? { title: effectiveOgTitle } : {},
287
+ ...effectiveOgDescription ? { description: effectiveOgDescription } : {},
288
+ ...image ? { images: [{ url: image }] } : {},
289
+ ...ogLocale !== null ? { locale: ogLocale } : {}
290
+ };
291
+ }
292
+ if (noindex) metadata.robots = { index: false };
293
+ if (canonical) metadata.alternates = { canonical };
294
+ return metadata;
295
+ }
296
+ function mergeMetadataOverParent(parent, page) {
297
+ if (!parent) return page;
298
+ const merged = { ...page };
299
+ for (const [key, pageValue] of Object.entries(page)) {
300
+ const parentValue = parent[key];
301
+ if (isPlainObject(pageValue) && isPlainObject(parentValue)) {
302
+ merged[key] = { ...parentValue, ...pageValue };
303
+ }
304
+ }
305
+ return merged;
306
+ }
307
+ function isPlainObject(value) {
308
+ return typeof value === "object" && value !== null && !Array.isArray(value);
309
+ }
310
+
311
+ // src/route-core.ts
226
312
  function canvasWrapContext(result, info) {
227
313
  const collection = result.document?.collection;
228
314
  return {
@@ -235,6 +321,7 @@ function canvasWrapContext(result, info) {
235
321
  function pathFromSlug(slug) {
236
322
  return "/" + (slug ?? []).join("/");
237
323
  }
324
+ var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
238
325
  function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
239
326
  if (allAssets.length === 0) return allAssets;
240
327
  const ids = /* @__PURE__ */ new Set();
@@ -245,7 +332,7 @@ function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
245
332
  for (const value of body.richTexts) {
246
333
  collectRichTextRefsInValue(value, { onAsset: (id) => ids.add(id) });
247
334
  }
248
- const ogImage = result.document?.["seoOgImage"];
335
+ const ogImage = seoObjectOf(result.document, result.seoField)[SEO_SUBFIELDS.image];
249
336
  const ogImageId = typeof ogImage === "string" ? ogImage : ogImage && typeof ogImage === "object" && typeof ogImage.id === "string" ? ogImage.id : null;
250
337
  if (ogImageId) ids.add(ogImageId);
251
338
  return ids.size === 0 ? [] : allAssets.filter((asset) => ids.has(asset.id));
@@ -281,27 +368,33 @@ function populatePageRelations(body, client, registry) {
281
368
  );
282
369
  return { relations, docKeys: [...refKeys] };
283
370
  }
284
- function populatePageListings(body, client, registry) {
285
- if (!registry) return { listings: {}, tags: [] };
371
+ function populatePageListings(body, client, registry, clock) {
372
+ if (!registry) return { listings: {}, tags: [], readsNow: false };
286
373
  const tags = [];
374
+ let instant;
375
+ const now = () => instant ??= clock();
287
376
  const listings = collectReferencedListings(
288
377
  body.nodes,
289
378
  registry,
290
379
  (query) => {
291
- const listed = client.listDocuments(query.collection, {
292
- where: query.where,
293
- ...query.sort ? { sort: query.sort } : {},
294
- ...query.limit !== void 0 ? { limit: query.limit } : {}
295
- });
380
+ const listed = client.listDocuments(query.collection, listDocumentsQueryOf(query, now));
296
381
  tags.push(listed.tags);
297
- return listed.ok ? listed.documents : [];
382
+ return listed.ok ? listed.documents : { errors: listed.errors };
298
383
  },
299
384
  // EC-255: reach listings nested inside transcluded reusable content (the same
300
385
  // depth-1 populate the relation bake uses), so a grid inside a reusable bakes —
301
386
  // and its membership tag is folded in — instead of rendering empty on the host.
302
387
  (ref) => client.resolveRelation(ref)
303
388
  );
304
- return { listings, tags };
389
+ return { listings, tags, readsNow: instant !== void 0 };
390
+ }
391
+ function listDocumentsQueryOf(query, now) {
392
+ const request = listingRequestOf(query, now);
393
+ return {
394
+ where: request.where,
395
+ ...request.sort ? { sort: request.sort } : {},
396
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
397
+ };
305
398
  }
306
399
  function populatePageLinks(body, client, registry) {
307
400
  if (!registry) return { links: {}, docKeys: [] };
@@ -359,11 +452,11 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
359
452
  options.registry
360
453
  );
361
454
  const relationTags = cacheTags({ docs: relationDocKeys, locale: result.tags.locale });
362
- const { listings, tags: listingTags } = populatePageListings(
363
- body,
364
- client,
365
- options.registry
366
- );
455
+ const {
456
+ listings,
457
+ tags: listingTags,
458
+ readsNow
459
+ } = populatePageListings(body, client, options.registry, options.clock ?? systemClock);
367
460
  const { links, docKeys: linkDocKeys } = populatePageLinks(body, client, options.registry);
368
461
  const linkTags = cacheTags({ docs: linkDocKeys, locale: result.tags.locale });
369
462
  return {
@@ -372,6 +465,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
372
465
  assets: bakedAssets,
373
466
  relations,
374
467
  listings,
468
+ readsNow,
375
469
  links,
376
470
  ...presentation ? { presentation } : {},
377
471
  tags: mergeCacheTags(
@@ -423,7 +517,6 @@ function revalidationTags(event, project) {
423
517
  case "delete":
424
518
  tags.add(docCacheTag(`${event.collection}:${event.id}`, scope));
425
519
  for (const locale of project.locales) tags.add(collectionCacheTag(event.collection, locale, scope));
426
- tags.add(projectSweepTag(scope));
427
520
  break;
428
521
  case "redirects":
429
522
  tags.add(projectSweepTag(scope));
@@ -436,135 +529,81 @@ function revalidationTags(event, project) {
436
529
  return [...tags].sort();
437
530
  }
438
531
  function canvasCacheKey({ scope, mode, url }) {
439
- return ["elytra-route", scope ?? "", mode ?? "", url];
532
+ return JSON.stringify([scope ?? "", mode ?? "", url]);
440
533
  }
441
- var CacheDiscoveryBail = class extends Error {
442
- constructor() {
443
- super("elytra cache discovery bail (never user-visible)");
444
- this.name = "CacheDiscoveryBail";
445
- }
446
- };
447
- function passthrough(impl) {
448
- return async () => impl();
449
- }
450
- async function loadCachedEntry(cache, keyParts, baselineTags, load) {
451
- let fresh;
452
- const probe = cache(
453
- passthrough(async () => {
454
- fresh = await load();
455
- throw new CacheDiscoveryBail();
456
- }),
457
- keyParts,
458
- { tags: [...baselineTags], revalidate: false }
459
- );
534
+ function parseCanvasCacheKey(key) {
535
+ let parsed;
460
536
  try {
461
- return await probe();
462
- } catch (error) {
463
- if (!(error instanceof CacheDiscoveryBail)) throw error;
464
- }
465
- const entry = fresh;
466
- const write = cache(
467
- passthrough(async () => entry),
468
- keyParts,
469
- { tags: entry.tags, revalidate: false }
470
- );
471
- return write();
472
- }
473
-
474
- // src/metadata.ts
475
- function str(value) {
476
- return typeof value === "string" && value.length > 0 ? value : null;
477
- }
478
- function looksLikeUrl(value) {
479
- return /^(https?:)?\/\//.test(value) || value.startsWith("/");
480
- }
481
- function resolveOgImage(value, assets) {
482
- if (value && typeof value === "object") {
483
- const url = value.url;
484
- return typeof url === "string" && url.length > 0 ? url : null;
537
+ parsed = JSON.parse(key);
538
+ } catch {
539
+ return void 0;
485
540
  }
486
- if (typeof value !== "string" || value.length === 0) return null;
487
- if (looksLikeUrl(value)) return value;
488
- return assets.find((asset) => asset.id === value)?.url ?? null;
489
- }
490
- function openGraphLocale(locale) {
491
- return locale.includes("-") ? locale.replace(/-/g, "_") : null;
492
- }
493
- function canvasPageMetadata(result, assets = []) {
494
- const doc = result.document;
495
- const seoTitle = str(doc?.["seoTitle"]);
496
- const seoDescription = str(doc?.["seoDescription"]);
497
- const seoOgTitle = str(doc?.["seoOgTitle"]);
498
- const seoOgImage = resolveOgImage(doc?.["seoOgImage"], assets);
499
- const seoNoindex = doc?.["seoNoindex"] === true;
500
- const seoCanonical = str(doc?.["seoCanonical"]);
501
- const docTitle = str(doc?.["name"]) ?? str(doc?.["title"]);
502
- const title = seoTitle ?? docTitle;
503
- const metadata = {};
504
- if (title !== null) metadata.title = title;
505
- if (seoDescription) metadata.description = seoDescription;
506
- const ogTitle = seoOgTitle ?? title;
507
- if (doc) {
508
- const ogLocale = openGraphLocale(result.tags.locale);
509
- metadata.openGraph = {
510
- ...ogTitle !== null ? { title: ogTitle } : {},
511
- ...seoDescription ? { description: seoDescription } : {},
512
- ...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
513
- ...ogLocale !== null ? { locale: ogLocale } : {}
514
- };
541
+ if (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every((part) => typeof part === "string")) {
542
+ return void 0;
515
543
  }
516
- if (seoNoindex) metadata.robots = { index: false };
517
- if (seoCanonical) metadata.alternates = { canonical: seoCanonical };
518
- return metadata;
519
- }
520
- function mergeMetadataOverParent(parent, page) {
521
- if (!parent) return page;
522
- const merged = { ...page };
523
- for (const [key, pageValue] of Object.entries(page)) {
524
- const parentValue = parent[key];
525
- if (isPlainObject(pageValue) && isPlainObject(parentValue)) {
526
- merged[key] = { ...parentValue, ...pageValue };
527
- }
544
+ const [scope, mode, url] = parsed;
545
+ return { scope, mode, url };
546
+ }
547
+ var NEXT_CACHE_TAG_CALL_LIMIT = 128;
548
+ function entryTagCalls(tags, scope) {
549
+ const sweep = projectSweepTag(scope);
550
+ const rest = nextCacheTags(tags, scope).filter((tag) => tag !== sweep);
551
+ const calls = [[sweep]];
552
+ for (let start = 0; start < rest.length; start += NEXT_CACHE_TAG_CALL_LIMIT) {
553
+ calls.push(rest.slice(start, start + NEXT_CACHE_TAG_CALL_LIMIT));
528
554
  }
529
- return merged;
530
- }
531
- function isPlainObject(value) {
532
- return typeof value === "object" && value !== null && !Array.isArray(value);
555
+ return calls;
533
556
  }
557
+ var CANVAS_VALIDATION_SLUG = ["_elytra"];
558
+ var CANVAS_VALIDATION_PATH = pathFromSlug([...CANVAS_VALIDATION_SLUG]);
534
559
  function defaultMetadata(result, assets) {
535
- return canvasPageMetadata(result, assets);
560
+ return seoMetadata(result.document, seoContextOf(result, assets));
536
561
  }
537
562
  async function requestPerspective() {
538
563
  const { isEnabled } = await draftMode();
539
- return isEnabled ? "draft" : "published";
564
+ return isEnabled ? DRAFT_PERSPECTIVE : "published";
540
565
  }
541
- var nextCacheWrapper = (cb, keyParts, options) => unstable_cache(cb, keyParts, { tags: options.tags, revalidate: options.revalidate });
542
566
  function createCanvasRoute(options) {
543
- const loadOutcome = async (url, perspective) => {
544
- const resolveFresh = async () => {
545
- const source = await options.loadContent({ perspective });
546
- return resolveCanvasPage(source, url, perspective, {
547
- // EC-205: page-scope the baked assets to what the page actually uses.
548
- registry: options.components.registry
549
- });
550
- };
551
- const devBypass = process.env.NODE_ENV === "development";
552
- if (!options.cache || perspective !== "published" || devBypass) return resolveFresh();
553
- const scope = options.cache.scope;
554
- const entry = await loadCachedEntry(
555
- nextCacheWrapper,
556
- canvasCacheKey({ scope, mode: options.cache.mode, url }),
557
- [projectSweepTag(scope)],
558
- async () => {
559
- const outcome = await resolveFresh();
560
- return { value: outcome, tags: nextCacheTags(outcome.tags, scope) };
561
- }
567
+ if (Object.hasOwn(options, "published") && options.published === void 0) {
568
+ throw new Error(
569
+ "Elytra canvas route: `published` was passed but is undefined. If it names your cache function (the one carrying the cache directive), declare that function ABOVE the canvasRoute() call \u2014 Next compiles a cached function into a variable assigned where it is written, so it is not hoisted like a plain function. To render without a cache, omit `published` entirely."
562
570
  );
563
- return entry.value;
571
+ }
572
+ const resolveFresh = async (url, perspective) => {
573
+ const source = await options.loadContent({ perspective });
574
+ return resolveCanvasPage(source, url, perspective, {
575
+ // EC-205: page-scope the baked assets to what the page actually uses.
576
+ registry: options.components.registry
577
+ });
578
+ };
579
+ const scope = options.scope ?? "";
580
+ const mode = options.mode ?? "";
581
+ const keyFor = (url) => canvasCacheKey({ scope, mode, url });
582
+ const resolve = async (key) => {
583
+ const parts = parseCanvasCacheKey(key);
584
+ if (!parts) {
585
+ throw new Error(
586
+ `Elytra canvas route: resolve() was handed ${JSON.stringify(key)}, which is not a key this route produced. Pass your cache function's argument through unchanged \u2014 it is the entry's key.`
587
+ );
588
+ }
589
+ if (parts.scope !== scope || parts.mode !== mode) {
590
+ throw new Error(
591
+ `Elytra canvas route: resolve() was handed a cache key of scope "${parts.scope}" and content mode "${parts.mode}", but this route resolves in scope "${scope}" and mode "${mode}". The entry would be keyed under one world and filled from another (EC-543). Pass each route's \`published\` function the key that route hands it, and call that same route's resolve().`
592
+ );
593
+ }
594
+ return resolveFresh(parts.url, "published");
595
+ };
596
+ const applyCacheTags = (result) => {
597
+ for (const tags of entryTagCalls(result.tags, scope)) cacheTag(...tags);
598
+ };
599
+ const loadOutcome = async (url, perspective) => {
600
+ if (perspective !== "published" || !options.published) return resolveFresh(url, perspective);
601
+ return options.published(keyFor(url));
564
602
  };
565
603
  const Page = async (props) => {
566
604
  const { slug } = await props.params;
567
605
  const url = pathFromSlug(slug);
606
+ if (url === CANVAS_VALIDATION_PATH) return notFound();
568
607
  const perspective = await requestPerspective();
569
608
  const outcome = await loadOutcome(url, perspective);
570
609
  options.onTags?.(outcome.tags, { url, perspective });
@@ -594,8 +633,9 @@ function createCanvasRoute(options) {
594
633
  const pageMetadata = async (props) => {
595
634
  const { slug } = await props.params;
596
635
  const url = pathFromSlug(slug);
636
+ if (url === CANVAS_VALIDATION_PATH) return {};
597
637
  const perspective = await requestPerspective();
598
- if (options.cache && perspective === "published") {
638
+ if (options.published && perspective === "published") {
599
639
  const outcome2 = await loadOutcome(url, perspective);
600
640
  if (outcome2.kind !== "render") return {};
601
641
  return options.metadata ? options.metadata(outcome2.result, { url, perspective }) : defaultMetadata(outcome2.result, outcome2.assets);
@@ -609,29 +649,16 @@ function createCanvasRoute(options) {
609
649
  const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
610
650
  return mergeMetadataOverParent(resolvedParent, page);
611
651
  };
612
- return { Page, generateMetadata };
613
- }
614
- function paramNamesOf(pattern) {
615
- return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
616
- }
617
- function fillPattern(pattern, params) {
618
- const segments = [];
619
- for (const segment of splitPath(pattern)) {
620
- if (!segment.startsWith(":")) {
621
- segments.push(segment);
622
- continue;
623
- }
624
- const value = params[segment.slice(1)];
625
- if (value === void 0 || value.length === 0) return null;
626
- segments.push(encodeURIComponent(value));
627
- }
628
- return "/" + segments.join("/");
652
+ const generateStaticParams = async () => [
653
+ { slug: [...CANVAS_VALIDATION_SLUG] }
654
+ ];
655
+ return { Page, generateMetadata, resolve, applyCacheTags, generateStaticParams };
629
656
  }
630
657
  function originOf(baseUrl) {
631
658
  return baseUrl.replace(/\/+$/, "");
632
659
  }
633
660
  function canvasSitemapEntries(source, options) {
634
- const routes = options.routes ?? source.routes ?? [];
661
+ const routes = source.routes ?? [];
635
662
  const locales = source.locales;
636
663
  const skipped = [];
637
664
  const skip = (route, reason) => {
@@ -648,83 +675,79 @@ function canvasSitemapEntries(source, options) {
648
675
  }
649
676
  return client;
650
677
  };
651
- const byUrl = /* @__PURE__ */ new Map();
652
- const byGroup = /* @__PURE__ */ new Map();
678
+ const candidates = [];
679
+ const collectionLocales = /* @__PURE__ */ new Map();
653
680
  for (const route of routes) {
654
- const paramNames = paramNamesOf(route.pattern);
655
- const mount = hierarchyMount(route);
656
- const routeLocales = route.locale ? [route.locale] : locales.locales;
657
- for (const locale of routeLocales) {
658
- const client = clientFor(locale);
659
- let paths;
660
- if (mount) {
661
- const listed = client.listDocuments(mount.collection);
662
- if (!listed.ok) {
663
- skip(route, `hierarchy collection "${mount.collection}" could not be listed`);
664
- continue;
665
- }
666
- const rows = listed.documents.map((doc) => ({
667
- collection: mount.collection,
668
- id: doc.id,
669
- values: { slug: doc["slug"], parent: doc["parent"] }
670
- }));
671
- const byId = new Map(rows.map((row) => [row.id, row]));
672
- paths = [];
673
- for (const row of rows) {
674
- if (typeof row.values["slug"] !== "string" || row.values["slug"] === "") continue;
675
- const composed = composePath(row, (id) => byId.get(id));
676
- if (!composed.ok) continue;
677
- paths.push("/" + [...mount.prefix, ...composed.segments].join("/"));
678
- }
679
- } else if (paramNames.length === 0) {
680
- paths = [route.pattern];
681
- } else {
682
- const paramSets = options.params?.({ route, locale, client });
683
- if (paramSets == null) {
684
- skip(route, "dynamic route not enumerated (no params provider entry)");
685
- continue;
686
- }
687
- paths = [];
688
- for (const paramSet of paramSets) {
689
- const path = fillPattern(route.pattern, paramSet);
690
- if (path === null) {
691
- skip(route, `params entry missing one of :${paramNames.join(", :")}`);
692
- continue;
693
- }
694
- paths.push(path);
695
- }
696
- }
697
- let unresolved = 0;
698
- for (const path of paths) {
699
- const result = client.resolvePage(path, locale);
700
- if (result.status !== "ok") {
701
- if (result.status !== "redirect") unresolved += 1;
702
- continue;
703
- }
704
- if (result.document?.["seoNoindex"] === true) continue;
705
- const { urlPath, addressable } = localeUrlPath(path, locale, locales);
706
- if (!addressable) {
707
- skip(
708
- route,
709
- `"${path}" (locale "${locale}") has no addressable URL \u2014 its first segment is read as a locale prefix`
710
- );
711
- continue;
712
- }
713
- const absolute = originOf(options.baseUrl) + urlPath;
714
- const group = result.document ? `${route.id}${COMPOSITE_KEY_SEPARATOR}doc${COMPOSITE_KEY_SEPARATOR}${result.document.collection}${COMPOSITE_KEY_SEPARATOR}${result.document.id}` : `${route.id}${COMPOSITE_KEY_SEPARATOR}path${COMPOSITE_KEY_SEPARATOR}${path}`;
715
- if (!byUrl.has(absolute)) byUrl.set(absolute, { group, locale });
716
- const perLocale = byGroup.get(group) ?? /* @__PURE__ */ new Map();
717
- if (!perLocale.has(locale)) perLocale.set(locale, absolute);
718
- byGroup.set(group, perLocale);
719
- }
720
- if (mount && paths.length > 0 && unresolved === paths.length) {
721
- skip(
722
- route,
723
- `hierarchy mount over "${mount.collection}" composed ${paths.length} path(s) in locale "${locale}", none of which the resolver serves`
724
- );
681
+ if (!route.document) continue;
682
+ const set = collectionLocales.get(route.document.collection) ?? /* @__PURE__ */ new Set();
683
+ for (const locale of route.locale ? [route.locale] : locales.locales) set.add(locale);
684
+ collectionLocales.set(route.document.collection, set);
685
+ }
686
+ for (const [collection, routeLocales] of collectionLocales) {
687
+ for (const locale of locales.locales) {
688
+ if (!routeLocales.has(locale)) continue;
689
+ for (const entry of clientFor(locale).urlsFor(collection)) {
690
+ candidates.push({
691
+ route: entry.route,
692
+ locale,
693
+ routePath: entry.routePath,
694
+ path: entry.path,
695
+ addressable: entry.addressable,
696
+ // EC-517: group by the DOCUMENT — the locale-invariant fact — scoped by route.
697
+ group: `${entry.route.id}${COMPOSITE_KEY_SEPARATOR}doc${COMPOSITE_KEY_SEPARATOR}${collection}${COMPOSITE_KEY_SEPARATOR}${entry.id}`
698
+ });
725
699
  }
726
700
  }
727
701
  }
702
+ for (const route of routes) {
703
+ if (route.document || splitPath(route.pattern).some((segment) => segment.startsWith(":"))) continue;
704
+ for (const locale of route.locale ? [route.locale] : locales.locales) {
705
+ const { urlPath, addressable } = localeUrlPath(route.pattern, locale, locales);
706
+ candidates.push({
707
+ route,
708
+ locale,
709
+ routePath: route.pattern,
710
+ path: urlPath,
711
+ addressable,
712
+ group: `${route.id}${COMPOSITE_KEY_SEPARATOR}path${COMPOSITE_KEY_SEPARATOR}${route.pattern}`
713
+ });
714
+ }
715
+ }
716
+ const byUrl = /* @__PURE__ */ new Map();
717
+ const byGroup = /* @__PURE__ */ new Map();
718
+ const tally = /* @__PURE__ */ new Map();
719
+ for (const candidate of candidates) {
720
+ const key = `${candidate.route.id}${COMPOSITE_KEY_SEPARATOR}${candidate.locale}`;
721
+ const count = tally.get(key) ?? { route: candidate.route, locale: candidate.locale, total: 0, unresolved: 0 };
722
+ count.total += 1;
723
+ tally.set(key, count);
724
+ const result = clientFor(candidate.locale).resolvePage(candidate.routePath, candidate.locale);
725
+ if (result.status !== "ok") {
726
+ if (result.status !== "redirect") count.unresolved += 1;
727
+ continue;
728
+ }
729
+ if (seoObjectOf(result.document, result.seoField)[SEO_SUBFIELDS.noindex] === true) continue;
730
+ if (!candidate.addressable) {
731
+ skip(
732
+ candidate.route,
733
+ `"${candidate.routePath}" (locale "${candidate.locale}") has no addressable URL \u2014 its first segment is read as a locale prefix`
734
+ );
735
+ continue;
736
+ }
737
+ const absolute = originOf(options.baseUrl) + candidate.path;
738
+ if (!byUrl.has(absolute)) byUrl.set(absolute, { group: candidate.group, locale: candidate.locale });
739
+ const perLocale = byGroup.get(candidate.group) ?? /* @__PURE__ */ new Map();
740
+ if (!perLocale.has(candidate.locale)) perLocale.set(candidate.locale, absolute);
741
+ byGroup.set(candidate.group, perLocale);
742
+ }
743
+ for (const { route, locale, total, unresolved } of tally.values()) {
744
+ if (total > 0 && unresolved === total && route.document && splitPath(route.pattern).at(-1)?.endsWith("*")) {
745
+ skip(
746
+ route,
747
+ `hierarchy mount over "${route.document.collection}" composed ${total} path(s) in locale "${locale}", none of which the resolver serves`
748
+ );
749
+ }
750
+ }
728
751
  const entries = [];
729
752
  for (const [url, { group }] of byUrl) {
730
753
  const perLocale = byGroup.get(group);
@@ -828,8 +851,8 @@ function createAssetImageLoader(options = {}) {
828
851
  return `${src}${separator}${widthParam}=${width}&${qualityParam}=${quality ?? defaultQuality}`;
829
852
  };
830
853
  }
831
- function createNextImagePrimitive(options) {
832
- return function NextImagePrimitive(props) {
854
+ function createNextImage(options) {
855
+ return function ElytraNextImage(props) {
833
856
  const { url, width, height, alt, focalPoint, lqip } = normalizeImageSource(props);
834
857
  if (url.length === 0) return null;
835
858
  return /* @__PURE__ */ jsx(
@@ -844,11 +867,12 @@ function createNextImagePrimitive(options) {
844
867
  );
845
868
  };
846
869
  }
847
- function nextImagePrimitive(options = {}) {
848
- return {
849
- manifest: ImageManifest,
850
- implementation: createNextImagePrimitive(options)
851
- };
870
+ function withNextImage(components, options = {}) {
871
+ const list = [...components ?? []];
872
+ const image = registerComponents(list).registry.roleComponent("image");
873
+ if (!image) return list;
874
+ const implementation = createNextImage(options);
875
+ return list.map((entry) => entry.manifest === image ? { manifest: entry.manifest, implementation } : entry);
852
876
  }
853
877
  function elytraHostImage(options = {}) {
854
878
  return ({ asset, alt, className, priority, sizes, loading }) => /* @__PURE__ */ jsx(
@@ -925,7 +949,7 @@ function parseRedirects(rows) {
925
949
  function assembleLiveSource(payload, options, perspective) {
926
950
  const locales = options.locales;
927
951
  const documents = parseDocuments(payload.documents);
928
- const routes = mergeRouteRecords([...options.routes], fallbackHomeRoutes(documents, locales));
952
+ const routes = [...options.routes];
929
953
  const documentSources = perspective === "published" ? documents.map((doc) => ({ draft: doc, published: doc, versions: [] })) : documents;
930
954
  return createStaticContentSource({
931
955
  projectId: payload.projectId,
@@ -944,7 +968,13 @@ async function loadLiveContent(options, perspective) {
944
968
  const fetchImpl = options.fetch ?? globalThis.fetch;
945
969
  const args = { projectId: options.projectId };
946
970
  let payload;
947
- if (perspective === "draft") {
971
+ const variant = variantOf(perspective);
972
+ if (variant !== void 0) {
973
+ if (variant !== DRAFT_VARIANT) {
974
+ throw new Error(
975
+ `Elytra live content: no delivery snapshot serves the "${variant}" variant \u2014 the deployment serves the published snapshot and the "${DRAFT_VARIANT}" variant's only.`
976
+ );
977
+ }
948
978
  if (!options.draftToken) {
949
979
  throw new Error(
950
980
  "Draft preview requested but DRAFT_PREVIEW_TOKEN is not configured on the host."
@@ -1028,8 +1058,6 @@ function resolveGlobals(source, options = {}) {
1028
1058
  if (typeof shaped.path === "string" && shaped.path.startsWith("/")) {
1029
1059
  return { href: shaped.path, openInNewTab };
1030
1060
  }
1031
- const slug = str2(shaped.slug);
1032
- if (slug) return { href: `/${slug}`, openInNewTab };
1033
1061
  return null;
1034
1062
  }
1035
1063
  return null;
@@ -1156,12 +1184,35 @@ function createPreviewDisableRoute() {
1156
1184
  }
1157
1185
 
1158
1186
  // src/host-kit.ts
1159
- function materializeRoutes(routes, collections) {
1160
- return typeof routes === "function" ? routes([...collections]) : [...routes];
1187
+ function resolveHostProject(entry) {
1188
+ const result = resolveProject(entry);
1189
+ if (result.ok) return result.project;
1190
+ const lines = result.issues.filter((issue) => issue.severity === "error").map((issue) => {
1191
+ const at = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
1192
+ return ` \u2022 ${issue.code}${at}: ${issue.message}`;
1193
+ });
1194
+ throw new Error(
1195
+ `Elytra delivery host: the project config "${String(entry["slug"])}" is invalid \u2014 the studio refuses it too:
1196
+ ${lines.join("\n")}`
1197
+ );
1198
+ }
1199
+ function surfaceRouteIssues(slug, issues) {
1200
+ if (issues.length === 0) return;
1201
+ const building = process.env["NEXT_PHASE"] === "phase-production-build";
1202
+ if (!building && process.env["NODE_ENV"] === "production") return;
1203
+ const lines = issues.map((issue) => ` \u2022 ${issue.severity} ${issue.code}: ${issue.message}`);
1204
+ const report = `[elytra] project "${slug}": ${issues.length} route issue(s) \u2014
1205
+ ${lines.join("\n")}`;
1206
+ if (issues.some((issue) => issue.severity === "error")) console.error(report);
1207
+ else console.warn(report);
1161
1208
  }
1162
1209
  function createElytraHost(options) {
1163
- const env = resolveHostEnv(options.env ?? process.env);
1164
- const routes = materializeRoutes(options.routes, options.collections);
1210
+ const { env: envOption, fallbackContent, globals, fetch: fetchOption, ...entry } = options;
1211
+ const project = resolveHostProject(entry);
1212
+ const env = resolveHostEnv(envOption ?? process.env);
1213
+ const routes = project.routes;
1214
+ const routeIssues = createRouter(project).issues;
1215
+ surfaceRouteIssues(project.slug, routeIssues);
1165
1216
  const live = env.convexUrl !== void 0 && env.projectId !== void 0;
1166
1217
  if (!live && (env.convexUrl !== void 0 || env.projectId !== void 0)) {
1167
1218
  const [present, missing] = env.convexUrl !== void 0 ? [ELYTRA_HOST_ENV_KEYS.convexUrl, ELYTRA_HOST_ENV_KEYS.projectId] : [ELYTRA_HOST_ENV_KEYS.projectId, ELYTRA_HOST_ENV_KEYS.convexUrl];
@@ -1169,26 +1220,16 @@ function createElytraHost(options) {
1169
1220
  `Elytra delivery host: ${present} is set but ${missing} is not \u2014 live content needs both. Left as it is, this site would serve fallback content and look healthy. Set ${missing} to go live, or unset ${present} to run the no-backend fallback on purpose.`
1170
1221
  );
1171
1222
  }
1172
- const contentMode = live ? "live" : options.fallbackContent ? "fallback" : "empty";
1173
- const contentScope = live ? env.projectId : options.slug;
1174
- const components = defineHostComponents([
1175
- // Batteries included (EC-368 image defaults): graph `base.Image` nodes render
1176
- // through next/image — resized variants via the host's own optimizer. The
1177
- // canonical manifest stays (first registration wins in defineHostComponents).
1178
- ...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
1179
- // EC-539: the option is the CONFIG's entry shape (`implementation: unknown`)
1180
- // so the recommended `{ ...site }` spread compiles; EC-541: registration
1181
- // takes that shape too and checks each implementation at runtime (see the
1182
- // option's doc comment) — no narrowing cast here.
1183
- ...options.components ?? []
1184
- ]);
1223
+ const contentMode = live ? "live" : fallbackContent ? "fallback" : "empty";
1224
+ const contentScope = live ? env.projectId : project.slug;
1225
+ const components = defineHostComponents(project.components ?? []);
1185
1226
  const emptySource = createStaticContentSource({
1186
1227
  projectId: contentScope,
1187
- locales: options.locales,
1228
+ locales: project.locales,
1188
1229
  routes,
1189
1230
  // Redirects are editorial DATA (EC-303) — the no-backend build has none.
1190
1231
  redirects: [],
1191
- collections: [...options.collections],
1232
+ collections: project.collections,
1192
1233
  documents: [],
1193
1234
  assets: []
1194
1235
  });
@@ -1198,16 +1239,16 @@ function createElytraHost(options) {
1198
1239
  {
1199
1240
  convexUrl: env.convexUrl,
1200
1241
  projectId: env.projectId,
1201
- locales: options.locales,
1202
- collections: options.collections,
1242
+ locales: project.locales,
1243
+ collections: project.collections,
1203
1244
  routes,
1204
1245
  ...env.draftToken ? { draftToken: env.draftToken } : {},
1205
- ...options.fetch ? { fetch: options.fetch } : {}
1246
+ ...fetchOption ? { fetch: fetchOption } : {}
1206
1247
  },
1207
1248
  request.perspective
1208
1249
  );
1209
1250
  }
1210
- if (options.fallbackContent) return options.fallbackContent(request);
1251
+ if (fallbackContent) return fallbackContent(request);
1211
1252
  return Promise.resolve(emptySource);
1212
1253
  };
1213
1254
  const loadGlobals = async (globalsOptions = {}) => {
@@ -1215,21 +1256,22 @@ function createElytraHost(options) {
1215
1256
  const source = await loadContent({ perspective });
1216
1257
  return resolveGlobals(source, {
1217
1258
  perspective,
1218
- ...options.globals ? { conventions: options.globals } : {}
1219
- });
1220
- };
1221
- const canvasRoute = (routeOptions = {}) => {
1222
- const { cache, ...rest } = routeOptions;
1223
- return createCanvasRoute({
1224
- loadContent,
1225
- components,
1226
- // Tag scope = `contentScope`, the SAME value `revalidateRoute` derives its
1227
- // invalidation tags with (EC-579) — one scope, so they cannot disagree.
1228
- // EC-543: the mode is in the entry key so no world ever reads another's entries.
1229
- ...cache === false ? {} : { cache: { mode: contentMode, scope: contentScope } },
1230
- ...rest
1259
+ ...globals ? { conventions: globals } : {}
1231
1260
  });
1232
1261
  };
1262
+ const canvasRoute = (routeOptions = {}) => createCanvasRoute({
1263
+ ...routeOptions,
1264
+ loadContent,
1265
+ components,
1266
+ // EC-620: the route caches nothing itself; these reach the host's own cache
1267
+ // function. Tag scope = `contentScope`, the SAME value `revalidateRoute` derives
1268
+ // its invalidation tags with (EC-579) — so `route.applyCacheTags` writes exactly
1269
+ // the strings the receiver revalidates. EC-543: the mode rides in every cache key
1270
+ // the route hands out, so no world ever reads another's entries. Spread last:
1271
+ // neither can be overridden.
1272
+ scope: contentScope,
1273
+ mode: contentMode
1274
+ });
1233
1275
  const revalidateRoute = (overrides = {}) => createRevalidateRoute({
1234
1276
  // An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
1235
1277
  // default secret (an auth-shaped no-op is worse than an explicit gap).
@@ -1238,26 +1280,29 @@ function createElytraHost(options) {
1238
1280
  // under (`canvasRoute` above) and every locale it serves. The emitter sends only
1239
1281
  // the event, so this is the one place the scope and locales are read for it.
1240
1282
  scope: contentScope,
1241
- locales: options.locales.locales
1283
+ locales: project.locales.locales
1242
1284
  });
1243
1285
  const previewRoute = () => createPreviewRoute({ token: env.draftToken });
1244
1286
  const previewDisableRoute = () => createPreviewDisableRoute();
1245
1287
  const pageOpensFullBleed = async (slug) => {
1246
- const { isEnabled } = await draftMode();
1247
- const perspective = isEnabled ? "draft" : "published";
1288
+ const perspective = await requestPerspective();
1248
1289
  const source = await loadContent({ perspective });
1249
1290
  const { locale, path } = splitLocalePath(pathFromSlug(slug), source.locales);
1250
1291
  const client = source.client({ perspective, locale });
1251
1292
  const resolved = client.resolvePage(path);
1252
1293
  return firstBlockFullBleed(resolved.document, components.registry);
1253
1294
  };
1295
+ const frontendUrl = projectFrontendUrl(project);
1254
1296
  return {
1297
+ routeIssues,
1298
+ collections: options.collections,
1255
1299
  contentScope,
1256
1300
  contentMode,
1257
1301
  env,
1302
+ project,
1258
1303
  routes,
1259
1304
  components,
1260
- ...options.frontendUrl !== void 0 ? { frontendUrl: options.frontendUrl } : {},
1305
+ ...frontendUrl !== void 0 ? { frontendUrl } : {},
1261
1306
  loadContent,
1262
1307
  loadGlobals,
1263
1308
  canvasRoute,
@@ -1269,6 +1314,6 @@ function createElytraHost(options) {
1269
1314
  }
1270
1315
  var PACKAGE = "@elytracms/next";
1271
1316
 
1272
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, nextImagePrimitive, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag };
1317
+ export { CANVAS_VALIDATION_SLUG, CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, 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 };
1273
1318
  //# sourceMappingURL=index.js.map
1274
1319
  //# sourceMappingURL=index.js.map