@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.d.ts +518 -408
- package/dist/index.js +348 -303
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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,
|
|
9
|
-
export {
|
|
10
|
-
import { revalidateTag,
|
|
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 = []
|
|
21
|
-
const
|
|
22
|
-
|
|
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) =>
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 [
|
|
532
|
+
return JSON.stringify([scope ?? "", mode ?? "", url]);
|
|
440
533
|
}
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
462
|
-
} catch
|
|
463
|
-
|
|
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 (
|
|
487
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
function
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
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
|
|
560
|
+
return seoMetadata(result.document, seoContextOf(result, assets));
|
|
536
561
|
}
|
|
537
562
|
async function requestPerspective() {
|
|
538
563
|
const { isEnabled } = await draftMode();
|
|
539
|
-
return isEnabled ?
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return
|
|
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 =
|
|
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
|
|
652
|
-
const
|
|
678
|
+
const candidates = [];
|
|
679
|
+
const collectionLocales = /* @__PURE__ */ new Map();
|
|
653
680
|
for (const route of routes) {
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
|
832
|
-
return function
|
|
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
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1160
|
-
|
|
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 =
|
|
1164
|
-
const
|
|
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" :
|
|
1173
|
-
const contentScope = live ? env.projectId :
|
|
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:
|
|
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:
|
|
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:
|
|
1202
|
-
collections:
|
|
1242
|
+
locales: project.locales,
|
|
1243
|
+
collections: project.collections,
|
|
1203
1244
|
routes,
|
|
1204
1245
|
...env.draftToken ? { draftToken: env.draftToken } : {},
|
|
1205
|
-
...
|
|
1246
|
+
...fetchOption ? { fetch: fetchOption } : {}
|
|
1206
1247
|
},
|
|
1207
1248
|
request.perspective
|
|
1208
1249
|
);
|
|
1209
1250
|
}
|
|
1210
|
-
if (
|
|
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
|
-
...
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
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
|