@cmssy/next 16.0.0 → 16.1.0

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/README.md CHANGED
@@ -45,10 +45,29 @@ the live-edit bridge. A published build does **not** require `editorOrigin`.
45
45
 
46
46
  ```ts
47
47
  // app/api/draft/route.ts
48
- import { createDraftRoute } from "@cmssy/next";
48
+ import { createDraftRoute } from "@cmssy/next/server";
49
49
  export const GET = createDraftRoute(cmssy);
50
50
  ```
51
51
 
52
+ ## Cache, and expire it on publish
53
+
54
+ ```ts
55
+ // app/[[...path]]/page.tsx
56
+ export const revalidate = 3600;
57
+ const cache = { revalidate };
58
+ export default createCmssyPage(cmssy, blocks, { editor: CmssyEditor, cache });
59
+
60
+ // app/api/revalidate/route.ts
61
+ import { createCmssyRevalidateRoute } from "@cmssy/next/server";
62
+ export const POST = createCmssyRevalidateRoute({
63
+ secret: process.env.CMSSY_WEBHOOK_SECRET,
64
+ });
65
+ ```
66
+
67
+ `cache` puts the SDK's published reads into the Next data cache under the
68
+ `cmssy-content` tag; the route expires that tag for a verified `content.changed`
69
+ webhook. Draft mode and the editor always read live.
70
+
52
71
  ## Edit-mode CSP
53
72
 
54
73
  ```ts
@@ -93,9 +112,13 @@ re-exported from `@cmssy/next` (and `@cmssy/core`).
93
112
 
94
113
  ## Exports
95
114
 
96
- `createCmssyPage`, `createDraftRoute`, `cmssyCspHeaders` / `applyCmssyCsp`,
97
- `isCmssyEditRequest` / `isCmssyEditMode`, `createCmssyLocaleMiddleware` /
98
- `resolveLocaleFromPathname`, and the `CmssyConfig` type.
115
+ `@cmssy/next/server`: `createCmssyPage`, `createCmssyEditPage`,
116
+ `createDraftRoute`, `createCmssyRevalidateRoute`, `CmssyLayoutSlot`,
117
+ `resolveCmssyLayout`, `isCmssyEditMode`. `@cmssy/next/middleware`:
118
+ `createCmssyProxy`, `cmssyEditRewrite`, `applyCmssyCsp`, `isCmssyEditRequest`.
119
+ Root: `defineCmssyConfig`, `localizeHref`, `nextRetryMode`, `cmssyCachedFetch`,
120
+ `CMSSY_CONTENT_TAG` and the types. The full list, with signatures, is in
121
+ [docs/reference/sdk-api.md](../../docs/reference/sdk-api.md).
99
122
 
100
123
  ## License
101
124
 
@@ -1,4 +1,4 @@
1
- import { CmssyRetryMode, RetryOption, CmssyConfig } from '@cmssy/core';
1
+ import { CmssyRetryMode, FetchLike, RetryOption, CmssyConfig } from '@cmssy/core';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ComponentType } from 'react';
4
4
  import { CmssyPageData, CmssyFormDefinition, BlockDefinition } from '@cmssy/react';
@@ -7,6 +7,13 @@ import { EditBridgeConfig } from '@cmssy/react/client';
7
7
  declare const NEXT_BUILD_PHASE = "phase-production-build";
8
8
  declare function nextRetryMode(): CmssyRetryMode;
9
9
 
10
+ declare const CMSSY_CONTENT_TAG = "cmssy-content";
11
+ interface CmssyDataCacheOptions {
12
+ revalidate: number | false;
13
+ tags?: string[];
14
+ }
15
+ declare function cmssyCachedFetch(cache: CmssyDataCacheOptions): FetchLike;
16
+
10
17
  interface CmssyEditorProps {
11
18
  page: CmssyPageData;
12
19
  locale: string;
@@ -28,6 +35,7 @@ interface CreateCmssyPageOptions {
28
35
  path?: string;
29
36
  appContext?: CmssyAppContext;
30
37
  retry?: RetryOption;
38
+ cache?: CmssyDataCacheOptions;
31
39
  }
32
40
  interface CatchAllParams {
33
41
  path?: string[];
@@ -40,4 +48,4 @@ interface CatchAllProps {
40
48
  declare function createCmssyPage(config: CmssyConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
41
49
  declare function createCmssyEditPage(config: CmssyConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
42
50
 
43
- export { type CmssyAppContext as C, NEXT_BUILD_PHASE as N, type CmssyEditorProps as a, type CreateCmssyPageOptions as b, createCmssyEditPage as c, createCmssyPage as d, nextRetryMode as n };
51
+ export { type CmssyDataCacheOptions as C, NEXT_BUILD_PHASE as N, CMSSY_CONTENT_TAG as a, type CmssyAppContext as b, type CmssyEditorProps as c, type CreateCmssyPageOptions as d, cmssyCachedFetch as e, createCmssyEditPage as f, createCmssyPage as g, nextRetryMode as n };
@@ -1,4 +1,4 @@
1
- import { CmssyRetryMode, RetryOption, CmssyConfig } from '@cmssy/core';
1
+ import { CmssyRetryMode, FetchLike, RetryOption, CmssyConfig } from '@cmssy/core';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ComponentType } from 'react';
4
4
  import { CmssyPageData, CmssyFormDefinition, BlockDefinition } from '@cmssy/react';
@@ -7,6 +7,13 @@ import { EditBridgeConfig } from '@cmssy/react/client';
7
7
  declare const NEXT_BUILD_PHASE = "phase-production-build";
8
8
  declare function nextRetryMode(): CmssyRetryMode;
9
9
 
10
+ declare const CMSSY_CONTENT_TAG = "cmssy-content";
11
+ interface CmssyDataCacheOptions {
12
+ revalidate: number | false;
13
+ tags?: string[];
14
+ }
15
+ declare function cmssyCachedFetch(cache: CmssyDataCacheOptions): FetchLike;
16
+
10
17
  interface CmssyEditorProps {
11
18
  page: CmssyPageData;
12
19
  locale: string;
@@ -28,6 +35,7 @@ interface CreateCmssyPageOptions {
28
35
  path?: string;
29
36
  appContext?: CmssyAppContext;
30
37
  retry?: RetryOption;
38
+ cache?: CmssyDataCacheOptions;
31
39
  }
32
40
  interface CatchAllParams {
33
41
  path?: string[];
@@ -40,4 +48,4 @@ interface CatchAllProps {
40
48
  declare function createCmssyPage(config: CmssyConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
41
49
  declare function createCmssyEditPage(config: CmssyConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
42
50
 
43
- export { type CmssyAppContext as C, NEXT_BUILD_PHASE as N, type CmssyEditorProps as a, type CreateCmssyPageOptions as b, createCmssyEditPage as c, createCmssyPage as d, nextRetryMode as n };
51
+ export { type CmssyDataCacheOptions as C, NEXT_BUILD_PHASE as N, CMSSY_CONTENT_TAG as a, type CmssyAppContext as b, type CmssyEditorProps as c, type CreateCmssyPageOptions as d, cmssyCachedFetch as e, createCmssyEditPage as f, createCmssyPage as g, nextRetryMode as n };
package/dist/index.cjs CHANGED
@@ -11,6 +11,19 @@ function nextRetryMode() {
11
11
  return phase === NEXT_BUILD_PHASE ? "build" : "interactive";
12
12
  }
13
13
 
14
+ // src/data-cache.ts
15
+ var CMSSY_CONTENT_TAG = "cmssy-content";
16
+ function cmssyContentTags(tags = []) {
17
+ return [.../* @__PURE__ */ new Set([CMSSY_CONTENT_TAG, ...tags])];
18
+ }
19
+ function cmssyCachedFetch(cache) {
20
+ const next = {
21
+ revalidate: cache.revalidate,
22
+ tags: cmssyContentTags(cache.tags)
23
+ };
24
+ return (url, init) => fetch(url, { ...init, next });
25
+ }
26
+
14
27
  Object.defineProperty(exports, "CMSSY_EDIT_HEADER", {
15
28
  enumerable: true,
16
29
  get: function () { return core.CMSSY_EDIT_HEADER; }
@@ -55,5 +68,7 @@ Object.defineProperty(exports, "verifyCmssyWebhook", {
55
68
  enumerable: true,
56
69
  get: function () { return core.verifyCmssyWebhook; }
57
70
  });
71
+ exports.CMSSY_CONTENT_TAG = CMSSY_CONTENT_TAG;
58
72
  exports.NEXT_BUILD_PHASE = NEXT_BUILD_PHASE;
73
+ exports.cmssyCachedFetch = cmssyCachedFetch;
59
74
  exports.nextRetryMode = nextRetryMode;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CmssyConfig, CmssyEnvConfig, CmssyLayout, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRegion, CmssyRegionOf, CmssyRegionSettings, CmssyRegionSettingsOf, CmssyRetryMode, CmssyWebhookError, CmssyWebhookEvent, DEFAULT_CMSSY_EDITOR_ORIGINS, LayoutRegion, RetryOption, RetryPolicy, VerifyCmssyWebhookOptions, defineCmssyConfig, defineCmssyLayout, localizeHref, resolveEditorOrigin, verifyCmssyWebhook } from '@cmssy/core';
2
- export { C as CmssyAppContext, a as CmssyEditorProps, b as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, n as nextRetryMode } from './index-ZUEbpOlo.cjs';
2
+ export { a as CMSSY_CONTENT_TAG, b as CmssyAppContext, C as CmssyDataCacheOptions, c as CmssyEditorProps, d as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, e as cmssyCachedFetch, n as nextRetryMode } from './index-D6hhjPH3.cjs';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
5
5
  import '@cmssy/react';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CmssyConfig, CmssyEnvConfig, CmssyLayout, CmssyPageData, CmssyPageMeta, CmssyPageSummary, CmssyRegion, CmssyRegionOf, CmssyRegionSettings, CmssyRegionSettingsOf, CmssyRetryMode, CmssyWebhookError, CmssyWebhookEvent, DEFAULT_CMSSY_EDITOR_ORIGINS, LayoutRegion, RetryOption, RetryPolicy, VerifyCmssyWebhookOptions, defineCmssyConfig, defineCmssyLayout, localizeHref, resolveEditorOrigin, verifyCmssyWebhook } from '@cmssy/core';
2
- export { C as CmssyAppContext, a as CmssyEditorProps, b as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, n as nextRetryMode } from './index-ZUEbpOlo.js';
2
+ export { a as CMSSY_CONTENT_TAG, b as CmssyAppContext, C as CmssyDataCacheOptions, c as CmssyEditorProps, d as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, e as cmssyCachedFetch, n as nextRetryMode } from './index-D6hhjPH3.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
5
5
  import '@cmssy/react';
package/dist/index.js CHANGED
@@ -9,4 +9,17 @@ function nextRetryMode() {
9
9
  return phase === NEXT_BUILD_PHASE ? "build" : "interactive";
10
10
  }
11
11
 
12
- export { NEXT_BUILD_PHASE, nextRetryMode };
12
+ // src/data-cache.ts
13
+ var CMSSY_CONTENT_TAG = "cmssy-content";
14
+ function cmssyContentTags(tags = []) {
15
+ return [.../* @__PURE__ */ new Set([CMSSY_CONTENT_TAG, ...tags])];
16
+ }
17
+ function cmssyCachedFetch(cache) {
18
+ const next = {
19
+ revalidate: cache.revalidate,
20
+ tags: cmssyContentTags(cache.tags)
21
+ };
22
+ return (url, init) => fetch(url, { ...init, next });
23
+ }
24
+
25
+ export { CMSSY_CONTENT_TAG, NEXT_BUILD_PHASE, cmssyCachedFetch, nextRetryMode };
package/dist/server.cjs CHANGED
@@ -9,6 +9,7 @@ var internalServer = require('@cmssy/react/internal-server');
9
9
  var internal = require('@cmssy/core/internal');
10
10
  var core = require('@cmssy/core');
11
11
  var jsxRuntime = require('react/jsx-runtime');
12
+ var cache = require('next/cache');
12
13
 
13
14
  // src/server.ts
14
15
 
@@ -18,6 +19,19 @@ function nextRetryMode() {
18
19
  const phase = typeof process !== "undefined" ? process.env?.NEXT_PHASE : void 0;
19
20
  return phase === NEXT_BUILD_PHASE ? "build" : "interactive";
20
21
  }
22
+
23
+ // src/data-cache.ts
24
+ var CMSSY_CONTENT_TAG = "cmssy-content";
25
+ function cmssyContentTags(tags = []) {
26
+ return [.../* @__PURE__ */ new Set([CMSSY_CONTENT_TAG, ...tags])];
27
+ }
28
+ function cmssyCachedFetch(cache) {
29
+ const next = {
30
+ revalidate: cache.revalidate,
31
+ tags: cmssyContentTags(cache.tags)
32
+ };
33
+ return (url, init) => fetch(url, { ...init, next });
34
+ }
21
35
  function hasEditFlag(value) {
22
36
  return Array.isArray(value) ? value.includes("1") : value === "1";
23
37
  }
@@ -49,7 +63,8 @@ function buildCmssyPageRenderer(config, blocks, options, editRoute) {
49
63
  workspaceSlug: config.workspaceSlug
50
64
  };
51
65
  const client = react.createCmssyClient(clientConfig);
52
- const requestOptions = { retry: options?.retry ?? nextRetryMode() };
66
+ const retryOptions = { retry: options?.retry ?? nextRetryMode() };
67
+ const cachedFetch = options?.cache ? cmssyCachedFetch(options.cache) : void 0;
53
68
  const fixedPath = options?.path?.split("/").map((segment) => segment.trim()).filter(Boolean);
54
69
  return async function CmssyCatchAllPage({
55
70
  params,
@@ -70,6 +85,7 @@ function buildCmssyPageRenderer(config, blocks, options, editRoute) {
70
85
  }
71
86
  const editMode = isEnabled || editorActive;
72
87
  const devAllowed = internal.isDevelopment() && Boolean(config.devToken?.trim());
88
+ const requestOptions = cachedFetch && !editMode && !devAllowed ? { ...retryOptions, fetch: cachedFetch } : retryOptions;
73
89
  const siteLocales = await internal.resolveSiteLocales(clientConfig, requestOptions);
74
90
  const { defaultLocale, locales: enabledLocales } = siteLocales;
75
91
  const split = internal.splitLocaleFromPath(path, siteLocales);
@@ -253,10 +269,49 @@ function createDraftRoute(config) {
253
269
  navigation.redirect(location);
254
270
  };
255
271
  }
272
+ var SIGNATURE_HEADER = "x-cmssy-signature";
273
+ function createCmssyRevalidateRoute(config) {
274
+ const tags = cmssyContentTags(config.tags);
275
+ const secrets = (Array.isArray(config.secret) ? config.secret : [config.secret]).filter((secret) => Boolean(secret));
276
+ return async function POST(request) {
277
+ if (secrets.length === 0) {
278
+ return new Response(
279
+ "cmssy: createCmssyRevalidateRoute has no signing secret - set CMSSY_WEBHOOK_SECRET to the secret cmssy shows for this webhook and pass it as `secret`",
280
+ { status: 500 }
281
+ );
282
+ }
283
+ const body = await request.text();
284
+ try {
285
+ await core.verifyCmssyWebhook({
286
+ body,
287
+ signatureHeader: request.headers.get(SIGNATURE_HEADER),
288
+ secret: secrets,
289
+ toleranceSeconds: config.toleranceSeconds
290
+ });
291
+ } catch (err) {
292
+ if (err instanceof core.CmssyWebhookError) {
293
+ return new Response(err.message, { status: 401 });
294
+ }
295
+ throw err;
296
+ }
297
+ for (const tag of tags) cache.revalidateTag(tag, { expire: 0 });
298
+ return Response.json({ revalidated: tags });
299
+ };
300
+ }
256
301
  async function isCmssyEditMode() {
257
302
  const h = await headers.headers();
258
303
  return h.get(core.CMSSY_EDIT_HEADER) === "1";
259
304
  }
305
+ function resolveCmssyLayout(config, options) {
306
+ const { cache, ...rest } = options;
307
+ const live = rest.editMode || rest.preview === true;
308
+ const fetchImpl = rest.fetch ?? (cache && !live ? cmssyCachedFetch(cache) : void 0);
309
+ return react.resolveCmssyLayout(config, {
310
+ ...rest,
311
+ retry: rest.retry ?? nextRetryMode(),
312
+ ...fetchImpl ? { fetch: fetchImpl } : {}
313
+ });
314
+ }
260
315
  async function CmssyLayoutSlot({
261
316
  config,
262
317
  blocks,
@@ -269,9 +324,10 @@ async function CmssyLayoutSlot({
269
324
  editable,
270
325
  appContext,
271
326
  retry,
327
+ cache,
272
328
  children
273
329
  }) {
274
- const layout = await react.resolveCmssyLayout(config, {
330
+ const layout = await resolveCmssyLayout(config, {
275
331
  region,
276
332
  blocks,
277
333
  editMode,
@@ -279,25 +335,23 @@ async function CmssyLayoutSlot({
279
335
  editable,
280
336
  ...page !== void 0 ? { page } : {},
281
337
  appContext,
282
- retry: retry ?? nextRetryMode(),
338
+ retry,
339
+ cache,
283
340
  ...explicitLocale !== void 0 ? { locale: explicitLocale } : { path: path ?? [] }
284
341
  });
285
342
  if (!children) return layout.element;
286
343
  const { groups, settings, page: routed, element } = layout;
287
344
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children({ groups, settings, page: routed, element }) });
288
345
  }
289
- function resolveCmssyLayout2(config, options) {
290
- return react.resolveCmssyLayout(config, {
291
- ...options,
292
- retry: options.retry ?? nextRetryMode()
293
- });
294
- }
295
346
 
347
+ exports.CMSSY_CONTENT_TAG = CMSSY_CONTENT_TAG;
296
348
  exports.CmssyLayoutSlot = CmssyLayoutSlot;
297
349
  exports.NEXT_BUILD_PHASE = NEXT_BUILD_PHASE;
350
+ exports.cmssyCachedFetch = cmssyCachedFetch;
298
351
  exports.createCmssyEditPage = createCmssyEditPage;
299
352
  exports.createCmssyPage = createCmssyPage;
353
+ exports.createCmssyRevalidateRoute = createCmssyRevalidateRoute;
300
354
  exports.createDraftRoute = createDraftRoute;
301
355
  exports.isCmssyEditMode = isCmssyEditMode;
302
356
  exports.nextRetryMode = nextRetryMode;
303
- exports.resolveCmssyLayout = resolveCmssyLayout2;
357
+ exports.resolveCmssyLayout = resolveCmssyLayout;
package/dist/server.d.cts CHANGED
@@ -1,10 +1,11 @@
1
- export { C as CmssyAppContext, a as CmssyEditorProps, b as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, c as createCmssyEditPage, d as createCmssyPage, n as nextRetryMode } from './index-ZUEbpOlo.cjs';
1
+ import { C as CmssyDataCacheOptions } from './index-D6hhjPH3.cjs';
2
+ export { a as CMSSY_CONTENT_TAG, b as CmssyAppContext, c as CmssyEditorProps, d as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, e as cmssyCachedFetch, f as createCmssyEditPage, g as createCmssyPage, n as nextRetryMode } from './index-D6hhjPH3.cjs';
2
3
  import { CmssyConfig, CmssyRegionOf, RetryOption } from '@cmssy/core';
3
4
  export { CmssyRetryMode, RetryOption, RetryPolicy } from '@cmssy/core';
4
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
  import { ComponentType, ReactNode } from 'react';
6
- import { BlockDefinition, CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions } from '@cmssy/react';
7
- export { CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions } from '@cmssy/react';
7
+ import { BlockDefinition, CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions as ResolveCmssyLayoutOptions$1 } from '@cmssy/react';
8
+ export { CmssyLayoutEditableProps, CmssyLayoutResolution } from '@cmssy/react';
8
9
  import '@cmssy/react/client';
9
10
 
10
11
  type CmssyDraftRouteConfig = Pick<CmssyConfig, "draftSecret"> & {
@@ -12,6 +13,13 @@ type CmssyDraftRouteConfig = Pick<CmssyConfig, "draftSecret"> & {
12
13
  };
13
14
  declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
14
15
 
16
+ interface CmssyRevalidateRouteConfig {
17
+ secret: string | string[] | undefined;
18
+ tags?: string[];
19
+ toleranceSeconds?: number;
20
+ }
21
+ declare function createCmssyRevalidateRoute(config: CmssyRevalidateRouteConfig): (request: Request) => Promise<Response>;
22
+
15
23
  declare function isCmssyEditMode(): Promise<boolean>;
16
24
 
17
25
  type CmssyLayoutSlotRenderProps<C extends CmssyConfig = CmssyConfig, P extends CmssyRegionOf<C> = CmssyRegionOf<C>> = Pick<CmssyLayoutResolution<C, P>, "groups" | "settings" | "page" | "element">;
@@ -25,6 +33,7 @@ interface CmssyLayoutSlotBaseProps<C extends CmssyConfig, P extends CmssyRegionO
25
33
  editable: ComponentType<CmssyLayoutEditableProps>;
26
34
  appContext?: Record<string, unknown>;
27
35
  retry?: RetryOption;
36
+ cache?: CmssyDataCacheOptions;
28
37
  children?: (layout: CmssyLayoutSlotRenderProps<C, P>) => ReactNode;
29
38
  }
30
39
  type CmssyLayoutSlotLocaleSource = {
@@ -35,8 +44,11 @@ type CmssyLayoutSlotLocaleSource = {
35
44
  path?: never;
36
45
  };
37
46
  type CmssyLayoutSlotProps<C extends CmssyConfig = CmssyConfig, P extends CmssyRegionOf<C> = CmssyRegionOf<C>> = CmssyLayoutSlotBaseProps<C, P> & CmssyLayoutSlotLocaleSource;
38
- declare function CmssyLayoutSlot<C extends CmssyConfig, P extends CmssyRegionOf<C>>({ config, blocks, region, path, locale: explicitLocale, editMode, preview, page, editable, appContext, retry, children, }: CmssyLayoutSlotProps<C, P>): Promise<react_jsx_runtime.JSX.Element>;
47
+ declare function CmssyLayoutSlot<C extends CmssyConfig, P extends CmssyRegionOf<C>>({ config, blocks, region, path, locale: explicitLocale, editMode, preview, page, editable, appContext, retry, cache, children, }: CmssyLayoutSlotProps<C, P>): Promise<react_jsx_runtime.JSX.Element>;
39
48
 
49
+ type ResolveCmssyLayoutOptions<C extends CmssyConfig, P extends CmssyRegionOf<C>> = ResolveCmssyLayoutOptions$1<C, P> & {
50
+ cache?: CmssyDataCacheOptions;
51
+ };
40
52
  declare function resolveCmssyLayout<C extends CmssyConfig, P extends CmssyRegionOf<C>>(config: C, options: ResolveCmssyLayoutOptions<C, P>): Promise<CmssyLayoutResolution<C, P>>;
41
53
 
42
- export { type CmssyDraftRouteConfig, CmssyLayoutSlot, type CmssyLayoutSlotProps, type CmssyLayoutSlotRenderProps, createDraftRoute, isCmssyEditMode, resolveCmssyLayout };
54
+ export { CmssyDataCacheOptions, type CmssyDraftRouteConfig, CmssyLayoutSlot, type CmssyLayoutSlotProps, type CmssyLayoutSlotRenderProps, type CmssyRevalidateRouteConfig, type ResolveCmssyLayoutOptions, createCmssyRevalidateRoute, createDraftRoute, isCmssyEditMode, resolveCmssyLayout };
package/dist/server.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- export { C as CmssyAppContext, a as CmssyEditorProps, b as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, c as createCmssyEditPage, d as createCmssyPage, n as nextRetryMode } from './index-ZUEbpOlo.js';
1
+ import { C as CmssyDataCacheOptions } from './index-D6hhjPH3.js';
2
+ export { a as CMSSY_CONTENT_TAG, b as CmssyAppContext, c as CmssyEditorProps, d as CreateCmssyPageOptions, N as NEXT_BUILD_PHASE, e as cmssyCachedFetch, f as createCmssyEditPage, g as createCmssyPage, n as nextRetryMode } from './index-D6hhjPH3.js';
2
3
  import { CmssyConfig, CmssyRegionOf, RetryOption } from '@cmssy/core';
3
4
  export { CmssyRetryMode, RetryOption, RetryPolicy } from '@cmssy/core';
4
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
6
  import { ComponentType, ReactNode } from 'react';
6
- import { BlockDefinition, CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions } from '@cmssy/react';
7
- export { CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions } from '@cmssy/react';
7
+ import { BlockDefinition, CmssyLayoutEditableProps, CmssyLayoutResolution, ResolveCmssyLayoutOptions as ResolveCmssyLayoutOptions$1 } from '@cmssy/react';
8
+ export { CmssyLayoutEditableProps, CmssyLayoutResolution } from '@cmssy/react';
8
9
  import '@cmssy/react/client';
9
10
 
10
11
  type CmssyDraftRouteConfig = Pick<CmssyConfig, "draftSecret"> & {
@@ -12,6 +13,13 @@ type CmssyDraftRouteConfig = Pick<CmssyConfig, "draftSecret"> & {
12
13
  };
13
14
  declare function createDraftRoute(config: CmssyDraftRouteConfig): (request: Request) => Promise<Response>;
14
15
 
16
+ interface CmssyRevalidateRouteConfig {
17
+ secret: string | string[] | undefined;
18
+ tags?: string[];
19
+ toleranceSeconds?: number;
20
+ }
21
+ declare function createCmssyRevalidateRoute(config: CmssyRevalidateRouteConfig): (request: Request) => Promise<Response>;
22
+
15
23
  declare function isCmssyEditMode(): Promise<boolean>;
16
24
 
17
25
  type CmssyLayoutSlotRenderProps<C extends CmssyConfig = CmssyConfig, P extends CmssyRegionOf<C> = CmssyRegionOf<C>> = Pick<CmssyLayoutResolution<C, P>, "groups" | "settings" | "page" | "element">;
@@ -25,6 +33,7 @@ interface CmssyLayoutSlotBaseProps<C extends CmssyConfig, P extends CmssyRegionO
25
33
  editable: ComponentType<CmssyLayoutEditableProps>;
26
34
  appContext?: Record<string, unknown>;
27
35
  retry?: RetryOption;
36
+ cache?: CmssyDataCacheOptions;
28
37
  children?: (layout: CmssyLayoutSlotRenderProps<C, P>) => ReactNode;
29
38
  }
30
39
  type CmssyLayoutSlotLocaleSource = {
@@ -35,8 +44,11 @@ type CmssyLayoutSlotLocaleSource = {
35
44
  path?: never;
36
45
  };
37
46
  type CmssyLayoutSlotProps<C extends CmssyConfig = CmssyConfig, P extends CmssyRegionOf<C> = CmssyRegionOf<C>> = CmssyLayoutSlotBaseProps<C, P> & CmssyLayoutSlotLocaleSource;
38
- declare function CmssyLayoutSlot<C extends CmssyConfig, P extends CmssyRegionOf<C>>({ config, blocks, region, path, locale: explicitLocale, editMode, preview, page, editable, appContext, retry, children, }: CmssyLayoutSlotProps<C, P>): Promise<react_jsx_runtime.JSX.Element>;
47
+ declare function CmssyLayoutSlot<C extends CmssyConfig, P extends CmssyRegionOf<C>>({ config, blocks, region, path, locale: explicitLocale, editMode, preview, page, editable, appContext, retry, cache, children, }: CmssyLayoutSlotProps<C, P>): Promise<react_jsx_runtime.JSX.Element>;
39
48
 
49
+ type ResolveCmssyLayoutOptions<C extends CmssyConfig, P extends CmssyRegionOf<C>> = ResolveCmssyLayoutOptions$1<C, P> & {
50
+ cache?: CmssyDataCacheOptions;
51
+ };
40
52
  declare function resolveCmssyLayout<C extends CmssyConfig, P extends CmssyRegionOf<C>>(config: C, options: ResolveCmssyLayoutOptions<C, P>): Promise<CmssyLayoutResolution<C, P>>;
41
53
 
42
- export { type CmssyDraftRouteConfig, CmssyLayoutSlot, type CmssyLayoutSlotProps, type CmssyLayoutSlotRenderProps, createDraftRoute, isCmssyEditMode, resolveCmssyLayout };
54
+ export { CmssyDataCacheOptions, type CmssyDraftRouteConfig, CmssyLayoutSlot, type CmssyLayoutSlotProps, type CmssyLayoutSlotRenderProps, type CmssyRevalidateRouteConfig, type ResolveCmssyLayoutOptions, createCmssyRevalidateRoute, createDraftRoute, isCmssyEditMode, resolveCmssyLayout };
package/dist/server.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import 'server-only';
2
2
  import { draftMode, headers } from 'next/headers';
3
3
  import { notFound, redirect } from 'next/navigation';
4
- import { createCmssyClient, CmssyServerPage, resolveCmssyLayout } from '@cmssy/react';
4
+ import { createCmssyClient, CmssyServerPage, resolveCmssyLayout as resolveCmssyLayout$1 } from '@cmssy/react';
5
5
  import { CmssyLocaleProvider } from '@cmssy/react/internal';
6
6
  import { blocksToSchemas, resolveEditorBlockData } from '@cmssy/react/internal-server';
7
7
  import { isDevelopment, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, toCspOrigin, cmssySecretsMatch } from '@cmssy/core/internal';
8
- import { CMSSY_SECRET_QUERY_PARAM, resolveEditorOrigin, CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM } from '@cmssy/core';
8
+ import { CMSSY_SECRET_QUERY_PARAM, resolveEditorOrigin, verifyCmssyWebhook, CmssyWebhookError, CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM } from '@cmssy/core';
9
9
  import { jsx, Fragment } from 'react/jsx-runtime';
10
+ import { revalidateTag } from 'next/cache';
10
11
 
11
12
  // src/server.ts
12
13
 
@@ -16,6 +17,19 @@ function nextRetryMode() {
16
17
  const phase = typeof process !== "undefined" ? process.env?.NEXT_PHASE : void 0;
17
18
  return phase === NEXT_BUILD_PHASE ? "build" : "interactive";
18
19
  }
20
+
21
+ // src/data-cache.ts
22
+ var CMSSY_CONTENT_TAG = "cmssy-content";
23
+ function cmssyContentTags(tags = []) {
24
+ return [.../* @__PURE__ */ new Set([CMSSY_CONTENT_TAG, ...tags])];
25
+ }
26
+ function cmssyCachedFetch(cache) {
27
+ const next = {
28
+ revalidate: cache.revalidate,
29
+ tags: cmssyContentTags(cache.tags)
30
+ };
31
+ return (url, init) => fetch(url, { ...init, next });
32
+ }
19
33
  function hasEditFlag(value) {
20
34
  return Array.isArray(value) ? value.includes("1") : value === "1";
21
35
  }
@@ -47,7 +61,8 @@ function buildCmssyPageRenderer(config, blocks, options, editRoute) {
47
61
  workspaceSlug: config.workspaceSlug
48
62
  };
49
63
  const client = createCmssyClient(clientConfig);
50
- const requestOptions = { retry: options?.retry ?? nextRetryMode() };
64
+ const retryOptions = { retry: options?.retry ?? nextRetryMode() };
65
+ const cachedFetch = options?.cache ? cmssyCachedFetch(options.cache) : void 0;
51
66
  const fixedPath = options?.path?.split("/").map((segment) => segment.trim()).filter(Boolean);
52
67
  return async function CmssyCatchAllPage({
53
68
  params,
@@ -68,6 +83,7 @@ function buildCmssyPageRenderer(config, blocks, options, editRoute) {
68
83
  }
69
84
  const editMode = isEnabled || editorActive;
70
85
  const devAllowed = isDevelopment() && Boolean(config.devToken?.trim());
86
+ const requestOptions = cachedFetch && !editMode && !devAllowed ? { ...retryOptions, fetch: cachedFetch } : retryOptions;
71
87
  const siteLocales = await resolveSiteLocales(clientConfig, requestOptions);
72
88
  const { defaultLocale, locales: enabledLocales } = siteLocales;
73
89
  const split = splitLocaleFromPath(path, siteLocales);
@@ -251,10 +267,49 @@ function createDraftRoute(config) {
251
267
  redirect(location);
252
268
  };
253
269
  }
270
+ var SIGNATURE_HEADER = "x-cmssy-signature";
271
+ function createCmssyRevalidateRoute(config) {
272
+ const tags = cmssyContentTags(config.tags);
273
+ const secrets = (Array.isArray(config.secret) ? config.secret : [config.secret]).filter((secret) => Boolean(secret));
274
+ return async function POST(request) {
275
+ if (secrets.length === 0) {
276
+ return new Response(
277
+ "cmssy: createCmssyRevalidateRoute has no signing secret - set CMSSY_WEBHOOK_SECRET to the secret cmssy shows for this webhook and pass it as `secret`",
278
+ { status: 500 }
279
+ );
280
+ }
281
+ const body = await request.text();
282
+ try {
283
+ await verifyCmssyWebhook({
284
+ body,
285
+ signatureHeader: request.headers.get(SIGNATURE_HEADER),
286
+ secret: secrets,
287
+ toleranceSeconds: config.toleranceSeconds
288
+ });
289
+ } catch (err) {
290
+ if (err instanceof CmssyWebhookError) {
291
+ return new Response(err.message, { status: 401 });
292
+ }
293
+ throw err;
294
+ }
295
+ for (const tag of tags) revalidateTag(tag, { expire: 0 });
296
+ return Response.json({ revalidated: tags });
297
+ };
298
+ }
254
299
  async function isCmssyEditMode() {
255
300
  const h = await headers();
256
301
  return h.get(CMSSY_EDIT_HEADER) === "1";
257
302
  }
303
+ function resolveCmssyLayout(config, options) {
304
+ const { cache, ...rest } = options;
305
+ const live = rest.editMode || rest.preview === true;
306
+ const fetchImpl = rest.fetch ?? (cache && !live ? cmssyCachedFetch(cache) : void 0);
307
+ return resolveCmssyLayout$1(config, {
308
+ ...rest,
309
+ retry: rest.retry ?? nextRetryMode(),
310
+ ...fetchImpl ? { fetch: fetchImpl } : {}
311
+ });
312
+ }
258
313
  async function CmssyLayoutSlot({
259
314
  config,
260
315
  blocks,
@@ -267,6 +322,7 @@ async function CmssyLayoutSlot({
267
322
  editable,
268
323
  appContext,
269
324
  retry,
325
+ cache,
270
326
  children
271
327
  }) {
272
328
  const layout = await resolveCmssyLayout(config, {
@@ -277,18 +333,13 @@ async function CmssyLayoutSlot({
277
333
  editable,
278
334
  ...page !== void 0 ? { page } : {},
279
335
  appContext,
280
- retry: retry ?? nextRetryMode(),
336
+ retry,
337
+ cache,
281
338
  ...explicitLocale !== void 0 ? { locale: explicitLocale } : { path: path ?? [] }
282
339
  });
283
340
  if (!children) return layout.element;
284
341
  const { groups, settings, page: routed, element } = layout;
285
342
  return /* @__PURE__ */ jsx(Fragment, { children: children({ groups, settings, page: routed, element }) });
286
343
  }
287
- function resolveCmssyLayout2(config, options) {
288
- return resolveCmssyLayout(config, {
289
- ...options,
290
- retry: options.retry ?? nextRetryMode()
291
- });
292
- }
293
344
 
294
- export { CmssyLayoutSlot, NEXT_BUILD_PHASE, createCmssyEditPage, createCmssyPage, createDraftRoute, isCmssyEditMode, nextRetryMode, resolveCmssyLayout2 as resolveCmssyLayout };
345
+ export { CMSSY_CONTENT_TAG, CmssyLayoutSlot, NEXT_BUILD_PHASE, cmssyCachedFetch, createCmssyEditPage, createCmssyPage, createCmssyRevalidateRoute, createDraftRoute, isCmssyEditMode, nextRetryMode, resolveCmssyLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "16.0.0",
3
+ "version": "16.1.0",
4
4
  "description": "Next.js App Router bindings for cmssy headless sites (createCmssyPage + draft preview)",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -71,7 +71,7 @@
71
71
  "dist"
72
72
  ],
73
73
  "peerDependencies": {
74
- "@cmssy/react": "^16.0.0",
74
+ "@cmssy/react": "^16.1.0",
75
75
  "next": ">=15",
76
76
  "react": "^18.2.0 || ^19.0.0",
77
77
  "react-dom": "^18.2.0 || ^19.0.0"
@@ -85,12 +85,12 @@
85
85
  "tsup": "^8.3.0",
86
86
  "typescript": "^5.6.0",
87
87
  "vitest": "^2.1.0",
88
- "@cmssy/react": "16.0.0"
88
+ "@cmssy/react": "16.1.0"
89
89
  },
90
90
  "dependencies": {
91
91
  "@cmssy/types": "0.41.0",
92
92
  "server-only": "^0.0.1",
93
- "@cmssy/core": "16.0.0"
93
+ "@cmssy/core": "16.1.0"
94
94
  },
95
95
  "scripts": {
96
96
  "build": "tsup",