@elytracms/next 0.0.87 → 0.0.90
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 +200 -114
- package/dist/index.js +72 -68
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,16 +5,16 @@ import { isValidElement } from 'react';
|
|
|
5
5
|
import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
|
|
6
6
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
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';
|
|
8
|
+
import { parseLocaleConfig, documentKey, splitLocalePath, hierarchyMount, composePath, localeUrlPath, mergeRouteRecords, fallbackHomeRoutes, presentationBodyFields, splitPath, documentSchema, redirectRecordSchema } from '@elytracms/core/cms-core';
|
|
9
|
+
export { documentSchema, fallbackHomeRoutes, localeConfigSchema, localeUrlPath, mergeRouteRecords, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection } from '@elytracms/core/cms-core';
|
|
10
10
|
import { revalidateTag, unstable_cache } from 'next/cache';
|
|
11
11
|
import { draftMode } from 'next/headers';
|
|
12
12
|
import { redirect, notFound, permanentRedirect } from 'next/navigation';
|
|
13
13
|
import { COMPOSITE_KEY_SEPARATOR } from '@elytracms/core/project-graph';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
export { z } from 'zod';
|
|
14
|
+
import { verifyRevalidationSignature, readRevalidationBody, REVALIDATE_SIGNATURE_HEADER } from '@elytracms/core/revalidation';
|
|
15
|
+
export { REVALIDATE_SIGNATURE_HEADER, revalidationEventSchema, signRevalidationBody, signedRevalidationRequest } from '@elytracms/core/revalidation';
|
|
17
16
|
import NextImage from 'next/image';
|
|
17
|
+
export { z } from 'zod';
|
|
18
18
|
|
|
19
19
|
// src/components.ts
|
|
20
20
|
function defineHostComponents(components = [], options = {}) {
|
|
@@ -134,7 +134,7 @@ function mergeCacheTags(first, ...rest) {
|
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
function createStaticContentSource(data) {
|
|
137
|
-
const locales = data.locales;
|
|
137
|
+
const locales = parseLocaleConfig(data.locales);
|
|
138
138
|
const routes = data.routes ?? [];
|
|
139
139
|
const redirects = data.redirects ?? [];
|
|
140
140
|
const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
|
|
@@ -414,6 +414,27 @@ function nextCacheTags(tags, scope) {
|
|
|
414
414
|
for (const assetId of tags.assets) all.add(assetCacheTag(assetId, scope));
|
|
415
415
|
return [...all].sort();
|
|
416
416
|
}
|
|
417
|
+
function revalidationTags(event, project) {
|
|
418
|
+
const { scope } = project;
|
|
419
|
+
const tags = /* @__PURE__ */ new Set();
|
|
420
|
+
switch (event.kind) {
|
|
421
|
+
case "publish":
|
|
422
|
+
case "unpublish":
|
|
423
|
+
case "delete":
|
|
424
|
+
tags.add(docCacheTag(`${event.collection}:${event.id}`, scope));
|
|
425
|
+
for (const locale of project.locales) tags.add(collectionCacheTag(event.collection, locale, scope));
|
|
426
|
+
tags.add(projectSweepTag(scope));
|
|
427
|
+
break;
|
|
428
|
+
case "redirects":
|
|
429
|
+
tags.add(projectSweepTag(scope));
|
|
430
|
+
break;
|
|
431
|
+
case "asset":
|
|
432
|
+
tags.add(assetCacheTag(event.id, scope));
|
|
433
|
+
tags.add(projectSweepTag(scope));
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
return [...tags].sort();
|
|
437
|
+
}
|
|
417
438
|
function canvasCacheKey({ scope, mode, url }) {
|
|
418
439
|
return ["elytra-route", scope ?? "", mode ?? "", url];
|
|
419
440
|
}
|
|
@@ -467,7 +488,7 @@ function resolveOgImage(value, assets) {
|
|
|
467
488
|
return assets.find((asset) => asset.id === value)?.url ?? null;
|
|
468
489
|
}
|
|
469
490
|
function openGraphLocale(locale) {
|
|
470
|
-
return locale.replace(/-/g, "_");
|
|
491
|
+
return locale.includes("-") ? locale.replace(/-/g, "_") : null;
|
|
471
492
|
}
|
|
472
493
|
function canvasPageMetadata(result, assets = []) {
|
|
473
494
|
const doc = result.document;
|
|
@@ -484,17 +505,32 @@ function canvasPageMetadata(result, assets = []) {
|
|
|
484
505
|
if (seoDescription) metadata.description = seoDescription;
|
|
485
506
|
const ogTitle = seoOgTitle ?? title;
|
|
486
507
|
if (doc) {
|
|
508
|
+
const ogLocale = openGraphLocale(result.tags.locale);
|
|
487
509
|
metadata.openGraph = {
|
|
488
510
|
...ogTitle !== null ? { title: ogTitle } : {},
|
|
489
511
|
...seoDescription ? { description: seoDescription } : {},
|
|
490
512
|
...seoOgImage ? { images: [{ url: seoOgImage }] } : {},
|
|
491
|
-
locale:
|
|
513
|
+
...ogLocale !== null ? { locale: ogLocale } : {}
|
|
492
514
|
};
|
|
493
515
|
}
|
|
494
516
|
if (seoNoindex) metadata.robots = { index: false };
|
|
495
517
|
if (seoCanonical) metadata.alternates = { canonical: seoCanonical };
|
|
496
518
|
return metadata;
|
|
497
519
|
}
|
|
520
|
+
function mergeMetadataOverParent(parent, page) {
|
|
521
|
+
if (!parent) return page;
|
|
522
|
+
const merged = { ...page };
|
|
523
|
+
for (const [key, pageValue] of Object.entries(page)) {
|
|
524
|
+
const parentValue = parent[key];
|
|
525
|
+
if (isPlainObject(pageValue) && isPlainObject(parentValue)) {
|
|
526
|
+
merged[key] = { ...parentValue, ...pageValue };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return merged;
|
|
530
|
+
}
|
|
531
|
+
function isPlainObject(value) {
|
|
532
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
533
|
+
}
|
|
498
534
|
function defaultMetadata(result, assets) {
|
|
499
535
|
return canvasPageMetadata(result, assets);
|
|
500
536
|
}
|
|
@@ -555,7 +591,7 @@ function createCanvasRoute(options) {
|
|
|
555
591
|
}
|
|
556
592
|
}
|
|
557
593
|
};
|
|
558
|
-
const
|
|
594
|
+
const pageMetadata = async (props) => {
|
|
559
595
|
const { slug } = await props.params;
|
|
560
596
|
const url = pathFromSlug(slug);
|
|
561
597
|
const perspective = await requestPerspective();
|
|
@@ -569,6 +605,10 @@ function createCanvasRoute(options) {
|
|
|
569
605
|
if (outcome.kind !== "render") return {};
|
|
570
606
|
return options.metadata ? options.metadata(outcome.result, { url, perspective }) : defaultMetadata(outcome.result, outcome.assets);
|
|
571
607
|
};
|
|
608
|
+
const generateMetadata = async (props, parent) => {
|
|
609
|
+
const [page, resolvedParent] = await Promise.all([pageMetadata(props), parent]);
|
|
610
|
+
return mergeMetadataOverParent(resolvedParent, page);
|
|
611
|
+
};
|
|
572
612
|
return { Page, generateMetadata };
|
|
573
613
|
}
|
|
574
614
|
function paramNamesOf(pattern) {
|
|
@@ -700,71 +740,33 @@ function createCanvasSitemap(options) {
|
|
|
700
740
|
return canvasSitemapEntries(source, options).entries;
|
|
701
741
|
};
|
|
702
742
|
}
|
|
703
|
-
|
|
704
|
-
var revalidatePayloadSchema = z.object({
|
|
705
|
-
projectId: z.string().min(1),
|
|
706
|
-
/** The operation that fired, e.g. `documents.setState`, `publishing.publish`. */
|
|
707
|
-
event: z.string().min(1),
|
|
708
|
-
/** Fully-formatted Next tag strings (already scoped) — revalidated verbatim. */
|
|
709
|
-
tags: z.array(z.string().min(1)),
|
|
710
|
-
/** ISO timestamp of emission. Informational in v1 (no replay window). */
|
|
711
|
-
timestamp: z.string().min(1)
|
|
712
|
-
});
|
|
713
|
-
function signRevalidateBody(rawBody, secret) {
|
|
714
|
-
return `sha256=${createHmac("sha256", secret).update(rawBody, "utf8").digest("hex")}`;
|
|
715
|
-
}
|
|
716
|
-
var SIGNATURE_PREFIX = "sha256=";
|
|
717
|
-
var HEX_PATTERN = /^[0-9a-f]+$/;
|
|
718
|
-
function evaluateRevalidateRequest(rawBody, signature, secret) {
|
|
743
|
+
async function evaluateRevalidateRequest(rawBody, signature, secret) {
|
|
719
744
|
if (secret === void 0 || secret.length === 0) {
|
|
720
745
|
return { ok: false, status: 401, message: "Revalidation webhook is not configured." };
|
|
721
746
|
}
|
|
722
|
-
if (signature
|
|
747
|
+
if (typeof signature !== "string" || signature.length === 0) {
|
|
723
748
|
return { ok: false, status: 401, message: "Missing or malformed webhook signature." };
|
|
724
749
|
}
|
|
725
|
-
|
|
726
|
-
const expectedHex = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
|
|
727
|
-
if (providedHex.length !== expectedHex.length || !HEX_PATTERN.test(providedHex) || !timingSafeEqual(Buffer.from(providedHex, "hex"), Buffer.from(expectedHex, "hex"))) {
|
|
750
|
+
if (!await verifyRevalidationSignature(rawBody, signature, secret)) {
|
|
728
751
|
return { ok: false, status: 401, message: "Invalid webhook signature." };
|
|
729
752
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
} catch {
|
|
734
|
-
return { ok: false, status: 400, message: "Webhook body is not valid JSON." };
|
|
735
|
-
}
|
|
736
|
-
const payload = revalidatePayloadSchema.safeParse(parsed);
|
|
737
|
-
if (!payload.success) {
|
|
738
|
-
return { ok: false, status: 400, message: "Webhook payload has an invalid shape." };
|
|
739
|
-
}
|
|
740
|
-
return { ok: true, payload: payload.data };
|
|
741
|
-
}
|
|
742
|
-
function corsHeaders(allowOrigin) {
|
|
743
|
-
if (allowOrigin === void 0 || allowOrigin.length === 0) return {};
|
|
744
|
-
return {
|
|
745
|
-
"access-control-allow-origin": allowOrigin,
|
|
746
|
-
"access-control-allow-methods": "POST, OPTIONS",
|
|
747
|
-
"access-control-allow-headers": `content-type, ${REVALIDATE_SIGNATURE_HEADER}`
|
|
748
|
-
};
|
|
753
|
+
const read = readRevalidationBody(rawBody);
|
|
754
|
+
if (!read.ok) return { ok: false, status: 400, message: read.message };
|
|
755
|
+
return { ok: true, event: read.event };
|
|
749
756
|
}
|
|
750
757
|
function createRevalidateRoute(options) {
|
|
751
|
-
const
|
|
758
|
+
const project = { scope: options.scope, locales: [...options.locales] };
|
|
752
759
|
return {
|
|
753
760
|
async POST(request) {
|
|
754
761
|
const rawBody = await request.text();
|
|
755
762
|
const signature = request.headers.get(REVALIDATE_SIGNATURE_HEADER);
|
|
756
|
-
const evaluation = evaluateRevalidateRequest(rawBody, signature, options.secret);
|
|
763
|
+
const evaluation = await evaluateRevalidateRequest(rawBody, signature, options.secret);
|
|
757
764
|
if (!evaluation.ok) {
|
|
758
|
-
return Response.json(
|
|
759
|
-
{ error: evaluation.message },
|
|
760
|
-
{ status: evaluation.status, headers: cors }
|
|
761
|
-
);
|
|
765
|
+
return Response.json({ error: evaluation.message }, { status: evaluation.status });
|
|
762
766
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
async OPTIONS() {
|
|
767
|
-
return new Response(null, { status: 204, headers: cors });
|
|
767
|
+
const tags = revalidationTags(evaluation.event, project);
|
|
768
|
+
for (const tag of tags) revalidateTag(tag, { expire: 0 });
|
|
769
|
+
return Response.json({ revalidated: tags });
|
|
768
770
|
}
|
|
769
771
|
};
|
|
770
772
|
}
|
|
@@ -875,8 +877,7 @@ var ELYTRA_HOST_ENV_KEYS = {
|
|
|
875
877
|
convexUrl: "ELYTRA_CONVEX_URL",
|
|
876
878
|
projectId: "ELYTRA_PROJECT_ID",
|
|
877
879
|
draftToken: "DRAFT_PREVIEW_TOKEN",
|
|
878
|
-
revalidateSecret: "REVALIDATE_SECRET"
|
|
879
|
-
studioOrigin: "STUDIO_ORIGIN"
|
|
880
|
+
revalidateSecret: "REVALIDATE_SECRET"
|
|
880
881
|
};
|
|
881
882
|
function cleaned(value) {
|
|
882
883
|
const trimmed = value?.trim();
|
|
@@ -1222,11 +1223,10 @@ function createElytraHost(options) {
|
|
|
1222
1223
|
return createCanvasRoute({
|
|
1223
1224
|
loadContent,
|
|
1224
1225
|
components,
|
|
1225
|
-
// Tag scope =
|
|
1226
|
-
// tags
|
|
1227
|
-
// EC-543: the mode is in the entry key so no world ever reads another's
|
|
1228
|
-
|
|
1229
|
-
...cache === false ? {} : { cache: { mode: contentMode, ...cache ?? { scope: contentScope } } },
|
|
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
1230
|
...rest
|
|
1231
1231
|
});
|
|
1232
1232
|
};
|
|
@@ -1234,7 +1234,11 @@ function createElytraHost(options) {
|
|
|
1234
1234
|
// An unset secret keeps the endpoint CLOSED (401) — the kit never ships a
|
|
1235
1235
|
// default secret (an auth-shaped no-op is worse than an explicit gap).
|
|
1236
1236
|
secret: "secret" in overrides ? overrides.secret : env.revalidateSecret,
|
|
1237
|
-
|
|
1237
|
+
// EC-579: the receiver's OWN project — the scope its cache entries are written
|
|
1238
|
+
// under (`canvasRoute` above) and every locale it serves. The emitter sends only
|
|
1239
|
+
// the event, so this is the one place the scope and locales are read for it.
|
|
1240
|
+
scope: contentScope,
|
|
1241
|
+
locales: options.locales.locales
|
|
1238
1242
|
});
|
|
1239
1243
|
const previewRoute = () => createPreviewRoute({ token: env.draftToken });
|
|
1240
1244
|
const previewDisableRoute = () => createPreviewDisableRoute();
|
|
@@ -1265,6 +1269,6 @@ function createElytraHost(options) {
|
|
|
1265
1269
|
}
|
|
1266
1270
|
var PACKAGE = "@elytracms/next";
|
|
1267
1271
|
|
|
1268
|
-
export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE,
|
|
1272
|
+
export { CanvasRenderer, ELYTRA_HOST_ENV_KEYS, ElytraImage, PACKAGE, assembleLiveSource, bodyItemsOf, canvasPageMetadata, canvasSitemapEntries, collectionCacheTag, createAssetImageLoader, createCanvasRoute, createCanvasSitemap, createContentSnapshot, createElytraHost, createPreviewDisableRoute, createPreviewRoute, createRevalidateRoute, createStaticContentSource, defineHostComponents, docCacheTag, elytraHostImage, evaluatePreviewRequest, evaluateRevalidateRequest, firstBlockFullBleed, loadCachedEntry, loadLiveContent, mergeCacheTags, mergeMetadataOverParent, nextCacheTags, nextImagePrimitive, openGraphLocale, pathFromSlug, projectSweepTag, resolveCanvasPage, resolveGlobals, resolveHostEnv, revalidationTags, routeCacheTag, safeRedirectTarget, scopeCacheTag };
|
|
1269
1273
|
//# sourceMappingURL=index.js.map
|
|
1270
1274
|
//# sourceMappingURL=index.js.map
|