@elytracms/next 0.0.91 → 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 +379 -233
- package/dist/index.js +263 -240
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { Fragment as Fragment$1, isValidElement } from 'react';
|
|
|
3
3
|
import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
|
|
4
4
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
import { cacheTags, isDocumentHistory, resolvedAsset, createContentClient, createContentContext } from '@elytracms/core/content';
|
|
6
|
-
import { parseLocaleConfig, documentKey, splitLocalePath,
|
|
7
|
-
export {
|
|
8
|
-
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';
|
|
9
9
|
import { draftMode } from 'next/headers';
|
|
10
10
|
import { redirect, notFound, permanentRedirect } from 'next/navigation';
|
|
11
11
|
import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
|
|
@@ -13,6 +13,7 @@ import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE
|
|
|
13
13
|
export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
|
|
14
14
|
import NextImage from 'next/image';
|
|
15
15
|
import { normalizeImageSource } from '@elytracms/components';
|
|
16
|
+
import { projectFrontendUrl, resolveProject } from '@elytracms/core/studio-core';
|
|
16
17
|
export { defineComponent } from '@elytracms/core/component-registry';
|
|
17
18
|
export { z } from 'zod';
|
|
18
19
|
|
|
@@ -200,17 +201,11 @@ function createStaticContentSource(data) {
|
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
async function createContentSnapshot(adapter, projectId, options) {
|
|
203
|
-
const schema = await adapter.cms.findSchema(projectId);
|
|
204
204
|
const documentRecords = await adapter.cms.listDocuments(projectId);
|
|
205
205
|
const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
|
|
206
206
|
const assetRecords = await adapter.assets.listAssets(projectId);
|
|
207
207
|
const redirectRecords = await adapter.redirects.listRedirects(projectId);
|
|
208
|
-
const redirects = redirectRecords
|
|
209
|
-
id: record2.id,
|
|
210
|
-
from: record2.from,
|
|
211
|
-
to: record2.to,
|
|
212
|
-
permanent: record2.permanent
|
|
213
|
-
}));
|
|
208
|
+
const redirects = liveRedirectRecords(redirectRecords);
|
|
214
209
|
const versionsByKey = /* @__PURE__ */ new Map();
|
|
215
210
|
for (const record2 of versionRecords) {
|
|
216
211
|
const key = `${record2.collection}:${record2.docId}`;
|
|
@@ -231,12 +226,89 @@ async function createContentSnapshot(adapter, projectId, options) {
|
|
|
231
226
|
locales: options.locales,
|
|
232
227
|
...options.routes ? { routes: options.routes } : {},
|
|
233
228
|
redirects: options.redirects ? [...options.redirects] : redirects,
|
|
234
|
-
|
|
229
|
+
collections: options.collections,
|
|
235
230
|
documents,
|
|
236
231
|
assets: assetRecords,
|
|
237
232
|
assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
|
|
238
233
|
});
|
|
239
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
|
|
240
312
|
function canvasWrapContext(result, info) {
|
|
241
313
|
const collection = result.document?.collection;
|
|
242
314
|
return {
|
|
@@ -260,7 +332,7 @@ function scopeAssetsToPage(allAssets, registry, result, body, resolveRelation) {
|
|
|
260
332
|
for (const value of body.richTexts) {
|
|
261
333
|
collectRichTextRefsInValue(value, { onAsset: (id) => ids.add(id) });
|
|
262
334
|
}
|
|
263
|
-
const ogImage = result.document
|
|
335
|
+
const ogImage = seoObjectOf(result.document, result.seoField)[SEO_SUBFIELDS.image];
|
|
264
336
|
const ogImageId = typeof ogImage === "string" ? ogImage : ogImage && typeof ogImage === "object" && typeof ogImage.id === "string" ? ogImage.id : null;
|
|
265
337
|
if (ogImageId) ids.add(ogImageId);
|
|
266
338
|
return ids.size === 0 ? [] : allAssets.filter((asset) => ids.has(asset.id));
|
|
@@ -457,14 +529,20 @@ function revalidationTags(event, project) {
|
|
|
457
529
|
return [...tags].sort();
|
|
458
530
|
}
|
|
459
531
|
function canvasCacheKey({ scope, mode, url }) {
|
|
460
|
-
return [scope ?? "", mode ?? "", url];
|
|
532
|
+
return JSON.stringify([scope ?? "", mode ?? "", url]);
|
|
461
533
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
534
|
+
function parseCanvasCacheKey(key) {
|
|
535
|
+
let parsed;
|
|
536
|
+
try {
|
|
537
|
+
parsed = JSON.parse(key);
|
|
538
|
+
} catch {
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
if (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every((part) => typeof part === "string")) {
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
const [scope, mode, url] = parsed;
|
|
545
|
+
return { scope, mode, url };
|
|
468
546
|
}
|
|
469
547
|
var NEXT_CACHE_TAG_CALL_LIMIT = 128;
|
|
470
548
|
function entryTagCalls(tags, scope) {
|
|
@@ -476,100 +554,21 @@ function entryTagCalls(tags, scope) {
|
|
|
476
554
|
}
|
|
477
555
|
return calls;
|
|
478
556
|
}
|
|
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
|
-
);
|
|
492
|
-
}
|
|
493
|
-
return resolve;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// src/metadata.ts
|
|
497
|
-
function str(value) {
|
|
498
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
499
|
-
}
|
|
500
|
-
function looksLikeUrl(value) {
|
|
501
|
-
return /^(https?:)?\/\//.test(value) || value.startsWith("/");
|
|
502
|
-
}
|
|
503
|
-
function resolveOgImage(value, assets) {
|
|
504
|
-
if (value && typeof value === "object") {
|
|
505
|
-
const url = value.url;
|
|
506
|
-
return typeof url === "string" && url.length > 0 ? url : null;
|
|
507
|
-
}
|
|
508
|
-
if (typeof value !== "string" || value.length === 0) return null;
|
|
509
|
-
if (looksLikeUrl(value)) return value;
|
|
510
|
-
return assets.find((asset) => asset.id === value)?.url ?? null;
|
|
511
|
-
}
|
|
512
|
-
function openGraphLocale(locale) {
|
|
513
|
-
return locale.includes("-") ? locale.replace(/-/g, "_") : null;
|
|
514
|
-
}
|
|
515
|
-
function canvasPageMetadata(result, assets = []) {
|
|
516
|
-
const doc = result.document;
|
|
517
|
-
const seoTitle = str(doc?.["seoTitle"]);
|
|
518
|
-
const seoDescription = str(doc?.["seoDescription"]);
|
|
519
|
-
const seoOgTitle = str(doc?.["seoOgTitle"]);
|
|
520
|
-
const seoOgImage = resolveOgImage(doc?.["seoOgImage"], assets);
|
|
521
|
-
const seoNoindex = doc?.["seoNoindex"] === true;
|
|
522
|
-
const seoCanonical = str(doc?.["seoCanonical"]);
|
|
523
|
-
const docTitle = str(doc?.["name"]) ?? str(doc?.["title"]);
|
|
524
|
-
const title = seoTitle ?? docTitle;
|
|
525
|
-
const metadata = {};
|
|
526
|
-
if (title !== null) metadata.title = title;
|
|
527
|
-
if (seoDescription) metadata.description = seoDescription;
|
|
528
|
-
const ogTitle = seoOgTitle ?? title;
|
|
529
|
-
if (doc) {
|
|
530
|
-
const ogLocale = openGraphLocale(result.tags.locale);
|
|
531
|
-
metadata.openGraph = {
|
|
532
|
-
...ogTitle !== null ? { title: ogTitle } : {},
|
|
533
|
-
...seoDescription ? { description: seoDescription } : {},
|
|
534
|
-
...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
|
|
535
|
-
...ogLocale !== null ? { locale: ogLocale } : {}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
if (seoNoindex) metadata.robots = { index: false };
|
|
539
|
-
if (seoCanonical) metadata.alternates = { canonical: seoCanonical };
|
|
540
|
-
return metadata;
|
|
541
|
-
}
|
|
542
|
-
function mergeMetadataOverParent(parent, page) {
|
|
543
|
-
if (!parent) return page;
|
|
544
|
-
const merged = { ...page };
|
|
545
|
-
for (const [key, pageValue] of Object.entries(page)) {
|
|
546
|
-
const parentValue = parent[key];
|
|
547
|
-
if (isPlainObject(pageValue) && isPlainObject(parentValue)) {
|
|
548
|
-
merged[key] = { ...parentValue, ...pageValue };
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return merged;
|
|
552
|
-
}
|
|
553
|
-
function isPlainObject(value) {
|
|
554
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
555
|
-
}
|
|
556
557
|
var CANVAS_VALIDATION_SLUG = ["_elytra"];
|
|
557
558
|
var CANVAS_VALIDATION_PATH = pathFromSlug([...CANVAS_VALIDATION_SLUG]);
|
|
558
559
|
function defaultMetadata(result, assets) {
|
|
559
|
-
return
|
|
560
|
+
return seoMetadata(result.document, seoContextOf(result, assets));
|
|
560
561
|
}
|
|
561
562
|
async function requestPerspective() {
|
|
562
563
|
const { isEnabled } = await draftMode();
|
|
563
|
-
return isEnabled ?
|
|
564
|
-
}
|
|
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;
|
|
564
|
+
return isEnabled ? DRAFT_PERSPECTIVE : "published";
|
|
571
565
|
}
|
|
572
566
|
function createCanvasRoute(options) {
|
|
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."
|
|
570
|
+
);
|
|
571
|
+
}
|
|
573
572
|
const resolveFresh = async (url, perspective) => {
|
|
574
573
|
const source = await options.loadContent({ perspective });
|
|
575
574
|
return resolveCanvasPage(source, url, perspective, {
|
|
@@ -577,15 +576,29 @@ function createCanvasRoute(options) {
|
|
|
577
576
|
registry: options.components.registry
|
|
578
577
|
});
|
|
579
578
|
};
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
};
|
|
586
599
|
const loadOutcome = async (url, perspective) => {
|
|
587
|
-
if (
|
|
588
|
-
return
|
|
600
|
+
if (perspective !== "published" || !options.published) return resolveFresh(url, perspective);
|
|
601
|
+
return options.published(keyFor(url));
|
|
589
602
|
};
|
|
590
603
|
const Page = async (props) => {
|
|
591
604
|
const { slug } = await props.params;
|
|
@@ -622,7 +635,7 @@ function createCanvasRoute(options) {
|
|
|
622
635
|
const url = pathFromSlug(slug);
|
|
623
636
|
if (url === CANVAS_VALIDATION_PATH) return {};
|
|
624
637
|
const perspective = await requestPerspective();
|
|
625
|
-
if (
|
|
638
|
+
if (options.published && perspective === "published") {
|
|
626
639
|
const outcome2 = await loadOutcome(url, perspective);
|
|
627
640
|
if (outcome2.kind !== "render") return {};
|
|
628
641
|
return options.metadata ? options.metadata(outcome2.result, { url, perspective }) : defaultMetadata(outcome2.result, outcome2.assets);
|
|
@@ -639,29 +652,13 @@ function createCanvasRoute(options) {
|
|
|
639
652
|
const generateStaticParams = async () => [
|
|
640
653
|
{ slug: [...CANVAS_VALIDATION_SLUG] }
|
|
641
654
|
];
|
|
642
|
-
return { Page, generateMetadata, generateStaticParams };
|
|
643
|
-
}
|
|
644
|
-
function paramNamesOf(pattern) {
|
|
645
|
-
return splitPath(pattern).filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1));
|
|
646
|
-
}
|
|
647
|
-
function fillPattern(pattern, params) {
|
|
648
|
-
const segments = [];
|
|
649
|
-
for (const segment of splitPath(pattern)) {
|
|
650
|
-
if (!segment.startsWith(":")) {
|
|
651
|
-
segments.push(segment);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
const value = params[segment.slice(1)];
|
|
655
|
-
if (value === void 0 || value.length === 0) return null;
|
|
656
|
-
segments.push(encodeURIComponent(value));
|
|
657
|
-
}
|
|
658
|
-
return "/" + segments.join("/");
|
|
655
|
+
return { Page, generateMetadata, resolve, applyCacheTags, generateStaticParams };
|
|
659
656
|
}
|
|
660
657
|
function originOf(baseUrl) {
|
|
661
658
|
return baseUrl.replace(/\/+$/, "");
|
|
662
659
|
}
|
|
663
660
|
function canvasSitemapEntries(source, options) {
|
|
664
|
-
const routes =
|
|
661
|
+
const routes = source.routes ?? [];
|
|
665
662
|
const locales = source.locales;
|
|
666
663
|
const skipped = [];
|
|
667
664
|
const skip = (route, reason) => {
|
|
@@ -678,83 +675,79 @@ function canvasSitemapEntries(source, options) {
|
|
|
678
675
|
}
|
|
679
676
|
return client;
|
|
680
677
|
};
|
|
681
|
-
const
|
|
682
|
-
const
|
|
678
|
+
const candidates = [];
|
|
679
|
+
const collectionLocales = /* @__PURE__ */ new Map();
|
|
683
680
|
for (const route of routes) {
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
paths = [];
|
|
703
|
-
for (const row of rows) {
|
|
704
|
-
if (typeof row.values["slug"] !== "string" || row.values["slug"] === "") continue;
|
|
705
|
-
const composed = composePath(row, (id) => byId.get(id));
|
|
706
|
-
if (!composed.ok) continue;
|
|
707
|
-
paths.push("/" + [...mount.prefix, ...composed.segments].join("/"));
|
|
708
|
-
}
|
|
709
|
-
} else if (paramNames.length === 0) {
|
|
710
|
-
paths = [route.pattern];
|
|
711
|
-
} else {
|
|
712
|
-
const paramSets = options.params?.({ route, locale, client });
|
|
713
|
-
if (paramSets == null) {
|
|
714
|
-
skip(route, "dynamic route not enumerated (no params provider entry)");
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
paths = [];
|
|
718
|
-
for (const paramSet of paramSets) {
|
|
719
|
-
const path = fillPattern(route.pattern, paramSet);
|
|
720
|
-
if (path === null) {
|
|
721
|
-
skip(route, `params entry missing one of :${paramNames.join(", :")}`);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
paths.push(path);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
let unresolved = 0;
|
|
728
|
-
for (const path of paths) {
|
|
729
|
-
const result = client.resolvePage(path, locale);
|
|
730
|
-
if (result.status !== "ok") {
|
|
731
|
-
if (result.status !== "redirect") unresolved += 1;
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
if (result.document?.["seoNoindex"] === true) continue;
|
|
735
|
-
const { urlPath, addressable } = localeUrlPath(path, locale, locales);
|
|
736
|
-
if (!addressable) {
|
|
737
|
-
skip(
|
|
738
|
-
route,
|
|
739
|
-
`"${path}" (locale "${locale}") has no addressable URL \u2014 its first segment is read as a locale prefix`
|
|
740
|
-
);
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
const absolute = originOf(options.baseUrl) + urlPath;
|
|
744
|
-
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}`;
|
|
745
|
-
if (!byUrl.has(absolute)) byUrl.set(absolute, { group, locale });
|
|
746
|
-
const perLocale = byGroup.get(group) ?? /* @__PURE__ */ new Map();
|
|
747
|
-
if (!perLocale.has(locale)) perLocale.set(locale, absolute);
|
|
748
|
-
byGroup.set(group, perLocale);
|
|
749
|
-
}
|
|
750
|
-
if (mount && paths.length > 0 && unresolved === paths.length) {
|
|
751
|
-
skip(
|
|
752
|
-
route,
|
|
753
|
-
`hierarchy mount over "${mount.collection}" composed ${paths.length} path(s) in locale "${locale}", none of which the resolver serves`
|
|
754
|
-
);
|
|
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
|
+
});
|
|
755
699
|
}
|
|
756
700
|
}
|
|
757
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
|
+
}
|
|
758
751
|
const entries = [];
|
|
759
752
|
for (const [url, { group }] of byUrl) {
|
|
760
753
|
const perLocale = byGroup.get(group);
|
|
@@ -956,7 +949,7 @@ function parseRedirects(rows) {
|
|
|
956
949
|
function assembleLiveSource(payload, options, perspective) {
|
|
957
950
|
const locales = options.locales;
|
|
958
951
|
const documents = parseDocuments(payload.documents);
|
|
959
|
-
const routes =
|
|
952
|
+
const routes = [...options.routes];
|
|
960
953
|
const documentSources = perspective === "published" ? documents.map((doc) => ({ draft: doc, published: doc, versions: [] })) : documents;
|
|
961
954
|
return createStaticContentSource({
|
|
962
955
|
projectId: payload.projectId,
|
|
@@ -975,7 +968,13 @@ async function loadLiveContent(options, perspective) {
|
|
|
975
968
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
976
969
|
const args = { projectId: options.projectId };
|
|
977
970
|
let payload;
|
|
978
|
-
|
|
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
|
+
}
|
|
979
978
|
if (!options.draftToken) {
|
|
980
979
|
throw new Error(
|
|
981
980
|
"Draft preview requested but DRAFT_PREVIEW_TOKEN is not configured on the host."
|
|
@@ -1059,8 +1058,6 @@ function resolveGlobals(source, options = {}) {
|
|
|
1059
1058
|
if (typeof shaped.path === "string" && shaped.path.startsWith("/")) {
|
|
1060
1059
|
return { href: shaped.path, openInNewTab };
|
|
1061
1060
|
}
|
|
1062
|
-
const slug = str2(shaped.slug);
|
|
1063
|
-
if (slug) return { href: `/${slug}`, openInNewTab };
|
|
1064
1061
|
return null;
|
|
1065
1062
|
}
|
|
1066
1063
|
return null;
|
|
@@ -1187,12 +1184,35 @@ function createPreviewDisableRoute() {
|
|
|
1187
1184
|
}
|
|
1188
1185
|
|
|
1189
1186
|
// src/host-kit.ts
|
|
1190
|
-
function
|
|
1191
|
-
|
|
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);
|
|
1192
1208
|
}
|
|
1193
1209
|
function createElytraHost(options) {
|
|
1194
|
-
const env =
|
|
1195
|
-
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);
|
|
1196
1216
|
const live = env.convexUrl !== void 0 && env.projectId !== void 0;
|
|
1197
1217
|
if (!live && (env.convexUrl !== void 0 || env.projectId !== void 0)) {
|
|
1198
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];
|
|
@@ -1200,16 +1220,16 @@ function createElytraHost(options) {
|
|
|
1200
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.`
|
|
1201
1221
|
);
|
|
1202
1222
|
}
|
|
1203
|
-
const contentMode = live ? "live" :
|
|
1204
|
-
const contentScope = live ? env.projectId :
|
|
1205
|
-
const components = defineHostComponents(
|
|
1223
|
+
const contentMode = live ? "live" : fallbackContent ? "fallback" : "empty";
|
|
1224
|
+
const contentScope = live ? env.projectId : project.slug;
|
|
1225
|
+
const components = defineHostComponents(project.components ?? []);
|
|
1206
1226
|
const emptySource = createStaticContentSource({
|
|
1207
1227
|
projectId: contentScope,
|
|
1208
|
-
locales:
|
|
1228
|
+
locales: project.locales,
|
|
1209
1229
|
routes,
|
|
1210
1230
|
// Redirects are editorial DATA (EC-303) — the no-backend build has none.
|
|
1211
1231
|
redirects: [],
|
|
1212
|
-
collections:
|
|
1232
|
+
collections: project.collections,
|
|
1213
1233
|
documents: [],
|
|
1214
1234
|
assets: []
|
|
1215
1235
|
});
|
|
@@ -1219,16 +1239,16 @@ function createElytraHost(options) {
|
|
|
1219
1239
|
{
|
|
1220
1240
|
convexUrl: env.convexUrl,
|
|
1221
1241
|
projectId: env.projectId,
|
|
1222
|
-
locales:
|
|
1223
|
-
collections:
|
|
1242
|
+
locales: project.locales,
|
|
1243
|
+
collections: project.collections,
|
|
1224
1244
|
routes,
|
|
1225
1245
|
...env.draftToken ? { draftToken: env.draftToken } : {},
|
|
1226
|
-
...
|
|
1246
|
+
...fetchOption ? { fetch: fetchOption } : {}
|
|
1227
1247
|
},
|
|
1228
1248
|
request.perspective
|
|
1229
1249
|
);
|
|
1230
1250
|
}
|
|
1231
|
-
if (
|
|
1251
|
+
if (fallbackContent) return fallbackContent(request);
|
|
1232
1252
|
return Promise.resolve(emptySource);
|
|
1233
1253
|
};
|
|
1234
1254
|
const loadGlobals = async (globalsOptions = {}) => {
|
|
@@ -1236,21 +1256,22 @@ function createElytraHost(options) {
|
|
|
1236
1256
|
const source = await loadContent({ perspective });
|
|
1237
1257
|
return resolveGlobals(source, {
|
|
1238
1258
|
perspective,
|
|
1239
|
-
...
|
|
1240
|
-
});
|
|
1241
|
-
};
|
|
1242
|
-
const canvasRoute = (routeOptions = {}) => {
|
|
1243
|
-
const { cache, ...rest } = routeOptions;
|
|
1244
|
-
return createCanvasRoute({
|
|
1245
|
-
loadContent,
|
|
1246
|
-
components,
|
|
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 } },
|
|
1251
|
-
...rest
|
|
1259
|
+
...globals ? { conventions: globals } : {}
|
|
1252
1260
|
});
|
|
1253
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
|
+
});
|
|
1254
1275
|
const revalidateRoute = (overrides = {}) => createRevalidateRoute({
|
|
1255
1276
|
// An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
|
|
1256
1277
|
// default secret (an auth-shaped no-op is worse than an explicit gap).
|
|
@@ -1259,27 +1280,29 @@ function createElytraHost(options) {
|
|
|
1259
1280
|
// under (`canvasRoute` above) and every locale it serves. The emitter sends only
|
|
1260
1281
|
// the event, so this is the one place the scope and locales are read for it.
|
|
1261
1282
|
scope: contentScope,
|
|
1262
|
-
locales:
|
|
1283
|
+
locales: project.locales.locales
|
|
1263
1284
|
});
|
|
1264
1285
|
const previewRoute = () => createPreviewRoute({ token: env.draftToken });
|
|
1265
1286
|
const previewDisableRoute = () => createPreviewDisableRoute();
|
|
1266
1287
|
const pageOpensFullBleed = async (slug) => {
|
|
1267
|
-
const
|
|
1268
|
-
const perspective = isEnabled ? "draft" : "published";
|
|
1288
|
+
const perspective = await requestPerspective();
|
|
1269
1289
|
const source = await loadContent({ perspective });
|
|
1270
1290
|
const { locale, path } = splitLocalePath(pathFromSlug(slug), source.locales);
|
|
1271
1291
|
const client = source.client({ perspective, locale });
|
|
1272
1292
|
const resolved = client.resolvePage(path);
|
|
1273
1293
|
return firstBlockFullBleed(resolved.document, components.registry);
|
|
1274
1294
|
};
|
|
1295
|
+
const frontendUrl = projectFrontendUrl(project);
|
|
1275
1296
|
return {
|
|
1297
|
+
routeIssues,
|
|
1276
1298
|
collections: options.collections,
|
|
1277
1299
|
contentScope,
|
|
1278
1300
|
contentMode,
|
|
1279
1301
|
env,
|
|
1302
|
+
project,
|
|
1280
1303
|
routes,
|
|
1281
1304
|
components,
|
|
1282
|
-
...
|
|
1305
|
+
...frontendUrl !== void 0 ? { frontendUrl } : {},
|
|
1283
1306
|
loadContent,
|
|
1284
1307
|
loadGlobals,
|
|
1285
1308
|
canvasRoute,
|
|
@@ -1291,6 +1314,6 @@ function createElytraHost(options) {
|
|
|
1291
1314
|
}
|
|
1292
1315
|
var PACKAGE = "@elytracms/next";
|
|
1293
1316
|
|
|
1294
|
-
export { CANVAS_VALIDATION_SLUG, 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 };
|
|
1295
1318
|
//# sourceMappingURL=index.js.map
|
|
1296
1319
|
//# sourceMappingURL=index.js.map
|