@elytracms/next 0.0.88 → 0.0.91

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,25 @@
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 { documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
9
- export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
10
- import { revalidateTag, unstable_cache } from 'next/cache';
6
+ import { parseLocaleConfig, documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
7
+ export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
8
+ import { revalidateTag, cacheLife, 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
- import { createHmac, timingSafeEqual } from 'crypto';
15
- import { z } from 'zod';
16
- export { z } from 'zod';
12
+ import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE_HEADER } from '@elytracms/core/revalidation';
13
+ export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
17
14
  import NextImage from 'next/image';
15
+ import { normalizeImageSource } from '@elytracms/components';
16
+ export { defineComponent } from '@elytracms/core/component-registry';
17
+ export { z } from 'zod';
18
18
 
19
19
  // 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] };
20
+ function defineHostComponents(components = []) {
21
+ const { registry, implementations } = registerComponents(components);
22
+ return { registry, implementations };
32
23
  }
33
24
  function relationResolver(relations) {
34
25
  return (ref) => relations[`${ref.collection}:${ref.id}`] ?? null;
@@ -37,7 +28,22 @@ function linkResolver(links) {
37
28
  return (ref) => links[`${ref.collection}:${ref.id}`] ?? null;
38
29
  }
39
30
  function listingResolver(listings) {
40
- return (query) => listings[serializeListingQuery(query)] ?? null;
31
+ return (query) => {
32
+ const baked = listings[serializeListingQuery(query)];
33
+ return Array.isArray(baked) ? baked : null;
34
+ };
35
+ }
36
+ function listingFailureOf(node, components, listings) {
37
+ const manifest = components.registry.getManifest(node.componentId);
38
+ if (!manifest) return null;
39
+ const listing = nodeListingOf(node, manifest, components.registry);
40
+ if (!listing || listing.state === "awaiting") return null;
41
+ if (listing.state === "invalid") return listing.messages;
42
+ const baked = listings?.[serializeListingQuery(listing.query)];
43
+ return baked && !Array.isArray(baked) ? baked.errors.map((error) => error.message) : null;
44
+ }
45
+ function InvalidListingNotice(props) {
46
+ 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
47
  }
42
48
  function assetResolver(assets) {
43
49
  const byId = new Map(assets.map((asset) => [asset.id, asset]));
@@ -49,7 +55,7 @@ function assetResolver(assets) {
49
55
  height: asset.height,
50
56
  alt: asset.alt,
51
57
  focalPoint: asset.focalPoint,
52
- // EC-420: carry the LQIP so a placed base.Image blurs up on delivery.
58
+ // EC-420: carry the LQIP so a placed Image blurs up on delivery.
53
59
  lqip: asset.lqip,
54
60
  meta: asset.meta
55
61
  } : null;
@@ -86,7 +92,15 @@ async function CanvasRenderer(props) {
86
92
  // so a host that did not opt in degrades a reference link to the visible
87
93
  // `unresolved-ref` issue (EC-290) rather than a wrong href.
88
94
  ...props.links ? { resolveLink: linkResolver(props.links) } : {},
89
- decorateNode: stripServerErrorBoundary
95
+ decorateNode: (node, rendered) => {
96
+ const stripped = stripServerErrorBoundary(node, rendered);
97
+ const failure = listingFailureOf(node, components, props.listings);
98
+ if (!failure) return stripped;
99
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
100
+ /* @__PURE__ */ jsx(InvalidListingNotice, { nodeId: node.id, messages: failure }),
101
+ stripped
102
+ ] }, node.id);
103
+ }
90
104
  };
91
105
  const document = result.document;
92
106
  if (document) {
@@ -134,7 +148,7 @@ function mergeCacheTags(first, ...rest) {
134
148
  });
135
149
  }
136
150
  function createStaticContentSource(data) {
137
- const locales = data.locales;
151
+ const locales = parseLocaleConfig(data.locales);
138
152
  const routes = data.routes ?? [];
139
153
  const redirects = data.redirects ?? [];
140
154
  const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
@@ -235,6 +249,7 @@ function canvasWrapContext(result, info) {
235
249
  function pathFromSlug(slug) {
236
250
  return "/" + (slug ?? []).join("/");
237
251
  }
252
+ var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
238
253
  function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
239
254
  if (allAssets.length === 0) return allAssets;
240
255
  const ids = /* @__PURE__ */ new Set();
@@ -281,27 +296,33 @@ function populatePageRelations(body, client, registry) {
281
296
  );
282
297
  return { relations, docKeys: [...refKeys] };
283
298
  }
284
- function populatePageListings(body, client, registry) {
285
- if (!registry) return { listings: {}, tags: [] };
299
+ function populatePageListings(body, client, registry, clock) {
300
+ if (!registry) return { listings: {}, tags: [], readsNow: false };
286
301
  const tags = [];
302
+ let instant;
303
+ const now = () => instant ??= clock();
287
304
  const listings = collectReferencedListings(
288
305
  body.nodes,
289
306
  registry,
290
307
  (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
- });
308
+ const listed = client.listDocuments(query.collection, listDocumentsQueryOf(query, now));
296
309
  tags.push(listed.tags);
297
- return listed.ok ? listed.documents : [];
310
+ return listed.ok ? listed.documents : { errors: listed.errors };
298
311
  },
299
312
  // EC-255: reach listings nested inside transcluded reusable content (the same
300
313
  // depth-1 populate the relation bake uses), so a grid inside a reusable bakes —
301
314
  // and its membership tag is folded in — instead of rendering empty on the host.
302
315
  (ref) => client.resolveRelation(ref)
303
316
  );
304
- return { listings, tags };
317
+ return { listings, tags, readsNow: instant !== void 0 };
318
+ }
319
+ function listDocumentsQueryOf(query, now) {
320
+ const request = listingRequestOf(query, now);
321
+ return {
322
+ where: request.where,
323
+ ...request.sort ? { sort: request.sort } : {},
324
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
325
+ };
305
326
  }
306
327
  function populatePageLinks(body, client, registry) {
307
328
  if (!registry) return { links: {}, docKeys: [] };
@@ -359,11 +380,11 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
359
380
  options.registry
360
381
  );
361
382
  const relationTags = cacheTags({ docs: relationDocKeys, locale: result.tags.locale });
362
- const { listings, tags: listingTags } = populatePageListings(
363
- body,
364
- client,
365
- options.registry
366
- );
383
+ const {
384
+ listings,
385
+ tags: listingTags,
386
+ readsNow
387
+ } = populatePageListings(body, client, options.registry, options.clock ?? systemClock);
367
388
  const { links, docKeys: linkDocKeys } = populatePageLinks(body, client, options.registry);
368
389
  const linkTags = cacheTags({ docs: linkDocKeys, locale: result.tags.locale });
369
390
  return {
@@ -372,6 +393,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
372
393
  assets: bakedAssets,
373
394
  relations,
374
395
  listings,
396
+ readsNow,
375
397
  links,
376
398
  ...presentation ? { presentation } : {},
377
399
  tags: mergeCacheTags(
@@ -414,40 +436,61 @@ function nextCacheTags(tags, scope) {
414
436
  for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
415
437
  return [...all].sort();
416
438
  }
417
- function canvasCacheKey({ scope, mode, url }) {
418
- return ["elytra-route", scope ?? "", mode ?? "", url];
439
+ function revalidationTags(event, project) {
440
+ const { scope } = project;
441
+ const tags = /* @__PURE__ */ new Set();
442
+ switch (event.kind) {
443
+ case "publish":
444
+ case "unpublish":
445
+ case "delete":
446
+ tags.add(docCacheTag(`${event.collection}:${event.id}`, scope));
447
+ for (const locale of project.locales) tags.add(collectionCacheTag(event.collection, locale, scope));
448
+ break;
449
+ case "redirects":
450
+ tags.add(projectSweepTag(scope));
451
+ break;
452
+ case "asset":
453
+ tags.add(assetCacheTag(event.id, scope));
454
+ tags.add(projectSweepTag(scope));
455
+ break;
456
+ }
457
+ return [...tags].sort();
419
458
  }
420
- var CacheDiscoveryBail = class extends Error {
421
- constructor() {
422
- super("elytra cache discovery bail (never user-visible)");
423
- this.name = "CacheDiscoveryBail";
459
+ function canvasCacheKey({ scope, mode, url }) {
460
+ return [scope ?? "", mode ?? "", url];
461
+ }
462
+ var CANVAS_CACHE_LIFE = { revalidate: Infinity, expire: Infinity };
463
+ var CANVAS_DEV_CACHE_LIFE = { revalidate: 1, expire: 300 };
464
+ var CANVAS_CLOCK_CACHE_LIFE = { revalidate: 60, expire: Infinity };
465
+ function canvasEntryLife(nodeEnv, readsClock = false) {
466
+ if (nodeEnv === "development") return CANVAS_DEV_CACHE_LIFE;
467
+ return readsClock ? CANVAS_CLOCK_CACHE_LIFE : CANVAS_CACHE_LIFE;
468
+ }
469
+ var NEXT_CACHE_TAG_CALL_LIMIT = 128;
470
+ function entryTagCalls(tags, scope) {
471
+ const sweep = projectSweepTag(scope);
472
+ const rest = nextCacheTags(tags, scope).filter((tag) => tag !== sweep);
473
+ const calls = [[sweep]];
474
+ for (let start = 0; start < rest.length; start += NEXT_CACHE_TAG_CALL_LIMIT) {
475
+ calls.push(rest.slice(start, start + NEXT_CACHE_TAG_CALL_LIMIT));
424
476
  }
425
- };
426
- function passthrough(impl) {
427
- return async () => impl();
428
- }
429
- async function loadCachedEntry(cache, keyParts, baselineTags, load) {
430
- let fresh;
431
- const probe = cache(
432
- passthrough(async () => {
433
- fresh = await load();
434
- throw new CacheDiscoveryBail();
435
- }),
436
- keyParts,
437
- { tags: [...baselineTags], revalidate: false }
438
- );
439
- try {
440
- return await probe();
441
- } catch (error) {
442
- if (!(error instanceof CacheDiscoveryBail)) throw error;
477
+ return calls;
478
+ }
479
+ var canvasResolvers = /* @__PURE__ */ new Map();
480
+ function resolverKey(scope, mode) {
481
+ return JSON.stringify([scope, mode]);
482
+ }
483
+ function registerCanvasResolver(scope, mode, resolve) {
484
+ canvasResolvers.set(resolverKey(scope, mode), resolve);
485
+ }
486
+ function canvasResolverFor(scope, mode) {
487
+ const resolve = canvasResolvers.get(resolverKey(scope, mode));
488
+ if (!resolve) {
489
+ throw new Error(
490
+ `Elytra delivery cache: no canvas route registered for scope "${scope}" and content mode "${mode}" in this server process \u2014 the cached route function has no loader to run. This is a wiring defect: create the route with createCanvasRoute / elytra.canvasRoute() before its Page renders.`
491
+ );
443
492
  }
444
- const entry = fresh;
445
- const write = cache(
446
- passthrough(async () => entry),
447
- keyParts,
448
- { tags: entry.tags, revalidate: false }
449
- );
450
- return write();
493
+ return resolve;
451
494
  }
452
495
 
453
496
  // src/metadata.ts
@@ -510,6 +553,8 @@ function mergeMetadataOverParent(parent, page) {
510
553
  function isPlainObject(value) {
511
554
  return typeof value === "object" && value !== null && !Array.isArray(value);
512
555
  }
556
+ var CANVAS_VALIDATION_SLUG = ["_elytra"];
557
+ var CANVAS_VALIDATION_PATH = pathFromSlug([...CANVAS_VALIDATION_SLUG]);
513
558
  function defaultMetadata(result, assets) {
514
559
  return canvasPageMetadata(result, assets);
515
560
  }
@@ -517,33 +562,35 @@ async function requestPerspective() {
517
562
  const { isEnabled } = await draftMode();
518
563
  return isEnabled ? "draft" : "published";
519
564
  }
520
- var nextCacheWrapper = (cb, keyParts, options) => unstable_cache(cb, keyParts, { tags: options.tags, revalidate: options.revalidate });
565
+ async function cachedCanvasOutcome(scope, mode, url) {
566
+ "use cache";
567
+ const outcome = await canvasResolverFor(scope, mode)(url);
568
+ cacheLife(canvasEntryLife(process.env.NODE_ENV, outcome.kind === "render" && outcome.readsNow));
569
+ for (const tags of entryTagCalls(outcome.tags, scope)) cacheTag(...tags);
570
+ return outcome;
571
+ }
521
572
  function createCanvasRoute(options) {
573
+ const resolveFresh = async (url, perspective) => {
574
+ const source = await options.loadContent({ perspective });
575
+ return resolveCanvasPage(source, url, perspective, {
576
+ // EC-205: page-scope the baked assets to what the page actually uses.
577
+ registry: options.components.registry
578
+ });
579
+ };
580
+ const cache = options.cache;
581
+ const entryKey = (url) => canvasCacheKey({ scope: cache?.scope, mode: cache?.mode, url });
582
+ if (cache) {
583
+ const [scope, mode] = entryKey("");
584
+ registerCanvasResolver(scope, mode, (url) => resolveFresh(url, "published"));
585
+ }
522
586
  const loadOutcome = async (url, perspective) => {
523
- const resolveFresh = async () => {
524
- const source = await options.loadContent({ perspective });
525
- return resolveCanvasPage(source, url, perspective, {
526
- // EC-205: page-scope the baked assets to what the page actually uses.
527
- registry: options.components.registry
528
- });
529
- };
530
- const devBypass = process.env.NODE_ENV === "development";
531
- if (!options.cache || perspective !== "published" || devBypass) return resolveFresh();
532
- const scope = options.cache.scope;
533
- const entry = await loadCachedEntry(
534
- nextCacheWrapper,
535
- canvasCacheKey({ scope, mode: options.cache.mode, url }),
536
- [projectSweepTag(scope)],
537
- async () => {
538
- const outcome = await resolveFresh();
539
- return { value: outcome, tags: nextCacheTags(outcome.tags, scope) };
540
- }
541
- );
542
- return entry.value;
587
+ if (!cache || perspective !== "published") return resolveFresh(url, perspective);
588
+ return cachedCanvasOutcome(...entryKey(url));
543
589
  };
544
590
  const Page = async (props) => {
545
591
  const { slug } = await props.params;
546
592
  const url = pathFromSlug(slug);
593
+ if (url === CANVAS_VALIDATION_PATH) return notFound();
547
594
  const perspective = await requestPerspective();
548
595
  const outcome = await loadOutcome(url, perspective);
549
596
  options.onTags?.(outcome.tags, { url, perspective });
@@ -573,8 +620,9 @@ function createCanvasRoute(options) {
573
620
  const pageMetadata = async (props) => {
574
621
  const { slug } = await props.params;
575
622
  const url = pathFromSlug(slug);
623
+ if (url === CANVAS_VALIDATION_PATH) return {};
576
624
  const perspective = await requestPerspective();
577
- if (options.cache && perspective === "published") {
625
+ if (cache && perspective === "published") {
578
626
  const outcome2 = await loadOutcome(url, perspective);
579
627
  if (outcome2.kind !== "render") return {};
580
628
  return options.metadata ? options.metadata(outcome2.result, { url, perspective }) : defaultMetadata(outcome2.result, outcome2.assets);
@@ -585,10 +633,13 @@ function createCanvasRoute(options) {
585
633
  return options.metadata ? options.metadata(outcome.result, { url, perspective }) : defaultMetadata(outcome.result, outcome.assets);
586
634
  };
587
635
  const generateMetadata = async (props, parent) => {
588
- const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent ?? null]);
636
+ const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
589
637
  return mergeMetadataOverParent(resolvedParent, page);
590
638
  };
591
- return { Page, generateMetadata };
639
+ const generateStaticParams = async () => [
640
+ { slug: [...CANVAS_VALIDATION_SLUG] }
641
+ ];
642
+ return { Page, generateMetadata, generateStaticParams };
592
643
  }
593
644
  function paramNamesOf(pattern) {
594
645
  return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
@@ -719,71 +770,33 @@ function createCanvasSitemap(options) {
719
770
  return canvasSitemapEntries(source, options).entries;
720
771
  };
721
772
  }
722
- var REVALIDATE_SIGNATURE_HEADER = "x-elytra-signature";
723
- var revalidatePayloadSchema = z.object({
724
- projectId: z.string().min(1),
725
- /** The operation that fired, e.g. `documents.setState`, `publishing.publish`. */
726
- event: z.string().min(1),
727
- /** Fully-formatted Next tag strings (already scoped) — revalidated verbatim. */
728
- tags: z.array(z.string().min(1)),
729
- /** ISO timestamp of emission. Informational in v1 (no replay window). */
730
- timestamp: z.string().min(1)
731
- });
732
- function signRevalidateBody(rawBody, secret) {
733
- return `sha256=${createHmac("sha256", secret).update(rawBody, "utf8").digest("hex")}`;
734
- }
735
- var SIGNATURE_PREFIX = "sha256=";
736
- var HEX_PATTERN = /^[0-9a-f]+$/;
737
- function evaluateRevalidateRequest(rawBody, signature, secret) {
773
+ async function evaluateRevalidateRequest(rawBody, signature, secret) {
738
774
  if (secret === void 0 || secret.length === 0) {
739
775
  return { ok: false, status: 401, message: "Revalidation webhook is not configured." };
740
776
  }
741
- if (signature === null || signature === void 0 || !signature.startsWith(SIGNATURE_PREFIX)) {
777
+ if (typeof signature !== "string" || signature.length === 0) {
742
778
  return { ok: false, status: 401, message: "Missing or malformed webhook signature." };
743
779
  }
744
- const providedHex = signature.slice(SIGNATURE_PREFIX.length).toLowerCase();
745
- const expectedHex = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
746
- if (providedHex.length !== expectedHex.length || !HEX_PATTERN.test(providedHex) || !timingSafeEqual(Buffer.from(providedHex, "hex"), Buffer.from(expectedHex, "hex"))) {
780
+ if (!await verifyRevalidationSignature(rawBody, signature, secret)) {
747
781
  return { ok: false, status: 401, message: "Invalid webhook signature." };
748
782
  }
749
- let parsed;
750
- try {
751
- parsed = JSON.parse(rawBody);
752
- } catch {
753
- return { ok: false, status: 400, message: "Webhook body is not valid JSON." };
754
- }
755
- const payload = revalidatePayloadSchema.safeParse(parsed);
756
- if (!payload.success) {
757
- return { ok: false, status: 400, message: "Webhook payload has an invalid shape." };
758
- }
759
- return { ok: true, payload: payload.data };
760
- }
761
- function corsHeaders(allowOrigin) {
762
- if (allowOrigin === void 0 || allowOrigin.length === 0) return {};
763
- return {
764
- "access-control-allow-origin": allowOrigin,
765
- "access-control-allow-methods": "POST, OPTIONS",
766
- "access-control-allow-headers": `content-type, ${REVALIDATE_SIGNATURE_HEADER}`
767
- };
783
+ const read = readRevalidationBody(rawBody);
784
+ if (!read.ok) return { ok: false, status: 400, message: read.message };
785
+ return { ok: true, event: read.event };
768
786
  }
769
787
  function createRevalidateRoute(options) {
770
- const cors = corsHeaders(options.allowOrigin);
788
+ const project = { scope: options.scope, locales: [...options.locales] };
771
789
  return {
772
790
  async POST(request) {
773
791
  const rawBody = await request.text();
774
792
  const signature = request.headers.get(REVALIDATE_SIGNATURE_HEADER);
775
- const evaluation = evaluateRevalidateRequest(rawBody, signature, options.secret);
793
+ const evaluation = await evaluateRevalidateRequest(rawBody, signature, options.secret);
776
794
  if (!evaluation.ok) {
777
- return Response.json(
778
- { error: evaluation.message },
779
- { status: evaluation.status, headers: cors }
780
- );
795
+ return Response.json({ error: evaluation.message }, { status: evaluation.status });
781
796
  }
782
- for (const tag of evaluation.payload.tags) revalidateTag(tag, { expire: 0 });
783
- return Response.json({ revalidated: evaluation.payload.tags }, { headers: cors });
784
- },
785
- async OPTIONS() {
786
- return new Response(null, { status: 204, headers: cors });
797
+ const tags = revalidationTags(evaluation.event, project);
798
+ for (const tag of tags) revalidateTag(tag, { expire: 0 });
799
+ return Response.json({ revalidated: tags });
787
800
  }
788
801
  };
789
802
  }
@@ -845,8 +858,8 @@ function createAssetImageLoader(options = {}) {
845
858
  return `${src}${separator}${widthParam}=${width}&${qualityParam}=${quality ?? defaultQuality}`;
846
859
  };
847
860
  }
848
- function createNextImagePrimitive(options) {
849
- return function NextImagePrimitive(props) {
861
+ function createNextImage(options) {
862
+ return function ElytraNextImage(props) {
850
863
  const { url, width, height, alt, focalPoint, lqip } = normalizeImageSource(props);
851
864
  if (url.length === 0) return null;
852
865
  return /* @__PURE__ */ jsx(
@@ -861,11 +874,12 @@ function createNextImagePrimitive(options) {
861
874
  );
862
875
  };
863
876
  }
864
- function nextImagePrimitive(options = {}) {
865
- return {
866
- manifest: ImageManifest,
867
- implementation: createNextImagePrimitive(options)
868
- };
877
+ function withNextImage(components, options = {}) {
878
+ const list = [...components ?? []];
879
+ const image = registerComponents(list).registry.roleComponent("image");
880
+ if (!image) return list;
881
+ const implementation = createNextImage(options);
882
+ return list.map((entry) => entry.manifest === image ? { manifest: entry.manifest, implementation } : entry);
869
883
  }
870
884
  function elytraHostImage(options = {}) {
871
885
  return ({ asset, alt, className, priority, sizes, loading }) => /* @__PURE__ */ jsx(
@@ -894,8 +908,7 @@ var ELYTRA_HOST_ENV_KEYS = {
894
908
  convexUrl: "ELYTRA_CONVEX_URL",
895
909
  projectId: "ELYTRA_PROJECT_ID",
896
910
  draftToken: "DRAFT_PREVIEW_TOKEN",
897
- revalidateSecret: "REVALIDATE_SECRET",
898
- studioOrigin: "STUDIO_ORIGIN"
911
+ revalidateSecret: "REVALIDATE_SECRET"
899
912
  };
900
913
  function cleaned(value) {
901
914
  const trimmed = value?.trim();
@@ -1189,17 +1202,7 @@ function createElytraHost(options) {
1189
1202
  }
1190
1203
  const contentMode = live ? "live" : options.fallbackContent ? "fallback" : "empty";
1191
1204
  const contentScope = live ? env.projectId : options.slug;
1192
- const components = defineHostComponents([
1193
- // Batteries included (EC-368 image defaults): graph `base.Image` nodes render
1194
- // through next/image — resized variants via the host's own optimizer. The
1195
- // canonical manifest stays (first registration wins in defineHostComponents).
1196
- ...options.image === false ? [] : [nextImagePrimitive(options.image ?? {})],
1197
- // EC-539: the option is the CONFIG's entry shape (`implementation: unknown`)
1198
- // so the recommended `{ ...site }` spread compiles; EC-541: registration
1199
- // takes that shape too and checks each implementation at runtime (see the
1200
- // option's doc comment) — no narrowing cast here.
1201
- ...options.components ?? []
1202
- ]);
1205
+ const components = defineHostComponents(options.components ?? []);
1203
1206
  const emptySource = createStaticContentSource({
1204
1207
  projectId: contentScope,
1205
1208
  locales: options.locales,
@@ -1241,11 +1244,10 @@ function createElytraHost(options) {
1241
1244
  return createCanvasRoute({
1242
1245
  loadContent,
1243
1246
  components,
1244
- // Tag scope = the project id the studio publishes under, so its webhook
1245
- // tags match this site's cached entries.
1246
- // EC-543: the mode is in the entry key so no world ever reads another's
1247
- // entries; an explicit override may replace the scope, not drop the mode.
1248
- ...cache === false ? {} : { cache: { mode: contentMode, ...cache ?? { scope: contentScope } } },
1247
+ // Tag scope = `contentScope`, the SAME value `revalidateRoute` derives its
1248
+ // invalidation tags with (EC-579) — one scope, so they cannot disagree.
1249
+ // EC-543: the mode is in the entry key so no world ever reads another's entries.
1250
+ ...cache === false ? {} : { cache: { mode: contentMode, scope: contentScope } },
1249
1251
  ...rest
1250
1252
  });
1251
1253
  };
@@ -1253,7 +1255,11 @@ function createElytraHost(options) {
1253
1255
  // An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
1254
1256
  // default secret (an auth-shaped no-op is worse than an explicit gap).
1255
1257
  secret: "secret" in overrides ? overrides.secret : env.revalidateSecret,
1256
- ...overrides.allowOrigin !== void 0 ? { allowOrigin: overrides.allowOrigin } : env.studioOrigin !== void 0 ? { allowOrigin: env.studioOrigin } : {}
1258
+ // EC-579: the receiver's OWN project — the scope its cache entries are written
1259
+ // under (`canvasRoute` above) and every locale it serves. The emitter sends only
1260
+ // the event, so this is the one place the scope and locales are read for it.
1261
+ scope: contentScope,
1262
+ locales: options.locales.locales
1257
1263
  });
1258
1264
  const previewRoute = () => createPreviewRoute({ token: env.draftToken });
1259
1265
  const previewDisableRoute = () => createPreviewDisableRoute();
@@ -1267,6 +1273,7 @@ function createElytraHost(options) {
1267
1273
  return firstBlockFullBleed(resolved.document, components.registry);
1268
1274
  };
1269
1275
  return {
1276
+ collections: options.collections,
1270
1277
  contentScope,
1271
1278
  contentMode,
1272
1279
  env,
@@ -1284,6 +1291,6 @@ function createElytraHost(options) {
1284
1291
  }
1285
1292
  var PACKAGE = "@elytracms/next";
1286
1293
 
1287
- export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, REVALIDATE_SIGNATURE_HEADER, 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, revalidatePayloadSchema, routeCacheTag, safeRedirectTarget, scopeCacheTag, signRevalidateBody };
1294
+ export { CANVAS_VALIDATION_SLUG, 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, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag, withNextImage };
1288
1295
  //# sourceMappingURL=index.js.map
1289
1296
  //# sourceMappingURL=index.js.map