@cmssy/next 0.3.0 → 0.5.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
@@ -69,17 +69,21 @@ import { CmssyLink } from "@cmssy/next/client";
69
69
  ```
70
70
 
71
71
  Add middleware so the root layout (which can't read the path) resolves the right
72
- locale via `getCmssyLocale`:
72
+ locale via `getCmssyLocale`. On Next.js 16 the file is `proxy.ts` (the renamed
73
+ middleware convention); on Next.js 15 use `middleware.ts` with the same body.
73
74
 
74
75
  ```ts
75
- // middleware.ts
76
+ // proxy.ts (Next.js 16) — or middleware.ts on Next.js 15
76
77
  import { createCmssyLocaleMiddleware } from "@cmssy/next";
77
78
  import { cmssy } from "@/cmssy/config";
78
79
 
79
- export const middleware = createCmssyLocaleMiddleware(cmssy);
80
- export const config = { matcher: ["/((?!_next|api|.*\\..*).*)"] };
80
+ export const proxy = createCmssyLocaleMiddleware(cmssy);
81
+ export const config = { matcher: ["/((?!_next/|api/|.*\\..*).*)"] };
81
82
  ```
82
83
 
84
+ On Next.js 15, name the file `middleware.ts` and rename the export to
85
+ `middleware` (`export const middleware = createCmssyLocaleMiddleware(cmssy)`).
86
+
83
87
  Language switcher and raw markup helpers live in `@cmssy/react`:
84
88
  `buildLocaleSwitchHref(target, pathname, locale)`, `localizeHref(href, locale)`,
85
89
  `localizeHtmlLinks(html, locale)`.
package/dist/index.cjs CHANGED
@@ -162,6 +162,175 @@ function resolveBridgeOrigin(editorOrigin) {
162
162
  }
163
163
  return origin;
164
164
  }
165
+ function DefaultNotFound() {
166
+ return /* @__PURE__ */ jsxRuntime.jsxs(
167
+ "main",
168
+ {
169
+ style: {
170
+ minHeight: "60vh",
171
+ display: "flex",
172
+ flexDirection: "column",
173
+ alignItems: "center",
174
+ justifyContent: "center",
175
+ gap: "0.5rem",
176
+ textAlign: "center",
177
+ padding: "2rem"
178
+ },
179
+ children: [
180
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "2rem", fontWeight: 700, margin: 0 }, children: "404" }),
181
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, opacity: 0.7 }, children: "Page not found" })
182
+ ]
183
+ }
184
+ );
185
+ }
186
+ function createCmssyNotFound(config, blocks, options) {
187
+ if (!Array.isArray(blocks)) {
188
+ throw new Error(
189
+ "cmssy: createCmssyNotFound(config, blocks) requires a blocks array \u2014 pass your defineBlock(...) array"
190
+ );
191
+ }
192
+ const clientConfig = {
193
+ apiUrl: config.apiUrl,
194
+ workspaceSlug: config.workspaceSlug
195
+ };
196
+ const fallback = options?.fallback ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultNotFound, {});
197
+ return async function CmssyNotFound() {
198
+ try {
199
+ const siteConfig = await react.fetchSiteConfig(clientConfig);
200
+ const notFoundPageId = siteConfig?.notFoundPageId;
201
+ if (!notFoundPageId) return fallback;
202
+ const page = await react.fetchPageById(clientConfig, notFoundPageId);
203
+ if (!page || page.blocks.length === 0) return fallback;
204
+ let locale;
205
+ let defaultLocale;
206
+ let enabledLocales = config.enabledLocales;
207
+ if (config.resolveLocale) {
208
+ defaultLocale = config.defaultLocale ?? "en";
209
+ locale = await config.resolveLocale();
210
+ } else {
211
+ const siteLocales = await react.resolveSiteLocales(clientConfig);
212
+ defaultLocale = config.defaultLocale ?? siteLocales.defaultLocale;
213
+ enabledLocales = config.enabledLocales ?? siteLocales.locales;
214
+ locale = defaultLocale;
215
+ }
216
+ const resolvedForms = await react.resolveForms(
217
+ clientConfig,
218
+ page.blocks,
219
+ locale,
220
+ defaultLocale
221
+ );
222
+ const forms = Object.keys(resolvedForms).length > 0 ? resolvedForms : void 0;
223
+ const localeContext = {
224
+ current: locale,
225
+ default: defaultLocale,
226
+ enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
227
+ };
228
+ return /* @__PURE__ */ jsxRuntime.jsx(client.CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsxRuntime.jsx(
229
+ react.CmssyServerPage,
230
+ {
231
+ page,
232
+ blocks,
233
+ locale,
234
+ defaultLocale,
235
+ enabledLocales,
236
+ forms
237
+ }
238
+ ) });
239
+ } catch (err) {
240
+ if (typeof console !== "undefined") {
241
+ console.warn("[cmssy] not-found page render failed", err);
242
+ }
243
+ return fallback;
244
+ }
245
+ };
246
+ }
247
+
248
+ // src/seo-base-url.ts
249
+ function trimTrailingSlash(url) {
250
+ return url.replace(/\/+$/, "");
251
+ }
252
+ async function resolveSeoBaseUrl(config, option) {
253
+ if (typeof option === "function") return trimTrailingSlash(await option());
254
+ if (typeof option === "string" && option) return trimTrailingSlash(option);
255
+ if (config.siteUrl) return trimTrailingSlash(config.siteUrl);
256
+ const { headers: headers3 } = await import('next/headers');
257
+ const h = await headers3();
258
+ const host = h.get("host");
259
+ if (!host) return "";
260
+ const protocol = isLocalHost(host) ? "http" : "https";
261
+ return `${protocol}://${host}`;
262
+ }
263
+ function isLocalHost(host) {
264
+ const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
265
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname === "::1") {
266
+ return true;
267
+ }
268
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
269
+ if (!ipv4) return false;
270
+ const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
271
+ return a === 127 || a === 0 || a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31;
272
+ }
273
+
274
+ // src/create-cmssy-robots.ts
275
+ function createCmssyRobots(config, options = {}) {
276
+ return async function robots() {
277
+ const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
278
+ const rules = options.rules ?? {
279
+ userAgent: "*",
280
+ allow: "/",
281
+ disallow: options.disallow ?? ["/api/"]
282
+ };
283
+ const includeSitemap = options.sitemap !== false && Boolean(baseUrl);
284
+ return {
285
+ rules,
286
+ ...includeSitemap ? { sitemap: `${baseUrl}/sitemap.xml` } : {},
287
+ ...baseUrl ? { host: baseUrl } : {}
288
+ };
289
+ };
290
+ }
291
+ function localizedPath(slug, locale, defaultLocale) {
292
+ const base = slug === "/" ? "" : slug;
293
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
294
+ }
295
+ function createCmssySitemap(config, options = {}) {
296
+ const clientConfig = {
297
+ apiUrl: config.apiUrl,
298
+ workspaceSlug: config.workspaceSlug
299
+ };
300
+ return async function sitemap() {
301
+ let pages = [];
302
+ try {
303
+ pages = await react.fetchPages(clientConfig);
304
+ } catch (err) {
305
+ if (typeof console !== "undefined") {
306
+ console.warn("[cmssy] sitemap page fetch failed", err);
307
+ }
308
+ pages = [];
309
+ }
310
+ const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
311
+ const defaultLocale = config.defaultLocale ?? "en";
312
+ const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
313
+ const entries = pages.map((page) => {
314
+ const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
315
+ const entry = {
316
+ url: `${baseUrl}${localizedPath(page.slug, defaultLocale, defaultLocale)}`,
317
+ ...lastModified ? { lastModified: new Date(lastModified) } : {}
318
+ };
319
+ if (locales.length > 1) {
320
+ entry.alternates = {
321
+ languages: Object.fromEntries(
322
+ locales.map((locale) => [
323
+ locale,
324
+ `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`
325
+ ])
326
+ )
327
+ };
328
+ }
329
+ return entry;
330
+ });
331
+ return options.extra ? [...entries, ...options.extra] : entries;
332
+ };
333
+ }
165
334
  var MIN_SECRET_LENGTH = 16;
166
335
  function secretsMatch(a, b) {
167
336
  const ha = crypto$1.createHash("sha256").update(a).digest();
@@ -1343,8 +1512,11 @@ exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
1343
1512
  exports.createCmssyAuthRoute = createCmssyAuthRoute;
1344
1513
  exports.createCmssyCartRoute = createCmssyCartRoute;
1345
1514
  exports.createCmssyLocaleMiddleware = createCmssyLocaleMiddleware;
1515
+ exports.createCmssyNotFound = createCmssyNotFound;
1346
1516
  exports.createCmssyOrdersRoute = createCmssyOrdersRoute;
1347
1517
  exports.createCmssyPage = createCmssyPage;
1518
+ exports.createCmssyRobots = createCmssyRobots;
1519
+ exports.createCmssySitemap = createCmssySitemap;
1348
1520
  exports.createDraftRoute = createDraftRoute;
1349
1521
  exports.fetchProduct = fetchProduct;
1350
1522
  exports.fetchProducts = fetchProducts;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
3
  import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
4
4
  import { EditBridgeConfig } from '@cmssy/react/client';
5
+ import { MetadataRoute } from 'next';
5
6
  import { NextRequest, NextResponse } from 'next/server';
6
7
 
7
8
  interface CmssyAuthConfig {
@@ -13,6 +14,12 @@ interface CmssyNextConfig {
13
14
  workspaceSlug: string;
14
15
  draftSecret: string;
15
16
  editorOrigin: string | string[];
17
+ /**
18
+ * Canonical absolute site URL (e.g. https://cmssy.com), used by
19
+ * createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
20
+ * origin from the request `host` header at render time (multi-domain safe).
21
+ */
22
+ siteUrl?: string;
16
23
  auth?: CmssyAuthConfig;
17
24
  defaultLocale?: string;
18
25
  /** All languages enabled on the workspace; exposed to blocks via context.locale.enabled. */
@@ -42,6 +49,59 @@ interface CatchAllProps {
42
49
  }
43
50
  declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
44
51
 
52
+ interface CreateCmssyNotFoundOptions {
53
+ /**
54
+ * Rendered when no 404 page is configured in Settings, or the configured
55
+ * page has no published content. Defaults to a minimal built-in message.
56
+ */
57
+ fallback?: ReactNode;
58
+ }
59
+ /**
60
+ * Renders the workspace's configured 404 page (Settings → 404 page) as the
61
+ * body of Next's `app/not-found.tsx`, preserving the HTTP 404 status. Drop the
62
+ * returned component in as the default export of `app/not-found.tsx`:
63
+ *
64
+ * export default createCmssyNotFound(cmssy, blocks);
65
+ */
66
+ declare function createCmssyNotFound(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyNotFoundOptions): () => Promise<string | number | bigint | boolean | react_jsx_runtime.JSX.Element | Iterable<ReactNode> | null | undefined>;
67
+
68
+ interface SeoBaseUrlOption {
69
+ /**
70
+ * Override for the canonical origin. A string (e.g. https://cmssy.com) or a
71
+ * resolver. Falls back to `config.siteUrl`, then the request `host` header.
72
+ */
73
+ baseUrl?: string | (() => string | Promise<string>);
74
+ }
75
+
76
+ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
77
+ /** Path prefixes to disallow. Defaults to `["/api/"]`. */
78
+ disallow?: string[];
79
+ /** Override the generated rules entirely. */
80
+ rules?: MetadataRoute.Robots["rules"];
81
+ /** Reference `${baseUrl}/sitemap.xml`. Defaults to true. */
82
+ sitemap?: boolean;
83
+ }
84
+ /**
85
+ * Builds the default export for Next's `app/robots.ts`. Allows crawling, points
86
+ * to the sitemap, and reports the canonical host. Drop in as:
87
+ *
88
+ * export default createCmssyRobots(cmssy);
89
+ */
90
+ declare function createCmssyRobots(config: CmssyNextConfig, options?: CreateCmssyRobotsOptions): () => Promise<MetadataRoute.Robots>;
91
+
92
+ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
93
+ /** Extra static entries appended to the generated page list. */
94
+ extra?: MetadataRoute.Sitemap;
95
+ }
96
+ /**
97
+ * Builds the default export for Next's `app/sitemap.ts` from the workspace's
98
+ * published pages. Emits one entry per page with per-locale `alternates` when
99
+ * the config enables multiple locales. Drop in as:
100
+ *
101
+ * export default createCmssySitemap(cmssy);
102
+ */
103
+ declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
104
+
45
105
  type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
46
106
  defaultRedirect?: string;
47
107
  };
@@ -235,4 +295,4 @@ declare class CmssyWebhookError extends Error {
235
295
  */
236
296
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
237
297
 
238
- export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
298
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
3
  import { CmssyPageData, CmssyFormDefinition, BlockDefinition, CmssyClientConfig, CmssyProduct, CmssyOrder } from '@cmssy/react';
4
4
  import { EditBridgeConfig } from '@cmssy/react/client';
5
+ import { MetadataRoute } from 'next';
5
6
  import { NextRequest, NextResponse } from 'next/server';
6
7
 
7
8
  interface CmssyAuthConfig {
@@ -13,6 +14,12 @@ interface CmssyNextConfig {
13
14
  workspaceSlug: string;
14
15
  draftSecret: string;
15
16
  editorOrigin: string | string[];
17
+ /**
18
+ * Canonical absolute site URL (e.g. https://cmssy.com), used by
19
+ * createCmssyRobots / createCmssySitemap. When omitted the helpers derive the
20
+ * origin from the request `host` header at render time (multi-domain safe).
21
+ */
22
+ siteUrl?: string;
16
23
  auth?: CmssyAuthConfig;
17
24
  defaultLocale?: string;
18
25
  /** All languages enabled on the workspace; exposed to blocks via context.locale.enabled. */
@@ -42,6 +49,59 @@ interface CatchAllProps {
42
49
  }
43
50
  declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
44
51
 
52
+ interface CreateCmssyNotFoundOptions {
53
+ /**
54
+ * Rendered when no 404 page is configured in Settings, or the configured
55
+ * page has no published content. Defaults to a minimal built-in message.
56
+ */
57
+ fallback?: ReactNode;
58
+ }
59
+ /**
60
+ * Renders the workspace's configured 404 page (Settings → 404 page) as the
61
+ * body of Next's `app/not-found.tsx`, preserving the HTTP 404 status. Drop the
62
+ * returned component in as the default export of `app/not-found.tsx`:
63
+ *
64
+ * export default createCmssyNotFound(cmssy, blocks);
65
+ */
66
+ declare function createCmssyNotFound(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyNotFoundOptions): () => Promise<string | number | bigint | boolean | react_jsx_runtime.JSX.Element | Iterable<ReactNode> | null | undefined>;
67
+
68
+ interface SeoBaseUrlOption {
69
+ /**
70
+ * Override for the canonical origin. A string (e.g. https://cmssy.com) or a
71
+ * resolver. Falls back to `config.siteUrl`, then the request `host` header.
72
+ */
73
+ baseUrl?: string | (() => string | Promise<string>);
74
+ }
75
+
76
+ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
77
+ /** Path prefixes to disallow. Defaults to `["/api/"]`. */
78
+ disallow?: string[];
79
+ /** Override the generated rules entirely. */
80
+ rules?: MetadataRoute.Robots["rules"];
81
+ /** Reference `${baseUrl}/sitemap.xml`. Defaults to true. */
82
+ sitemap?: boolean;
83
+ }
84
+ /**
85
+ * Builds the default export for Next's `app/robots.ts`. Allows crawling, points
86
+ * to the sitemap, and reports the canonical host. Drop in as:
87
+ *
88
+ * export default createCmssyRobots(cmssy);
89
+ */
90
+ declare function createCmssyRobots(config: CmssyNextConfig, options?: CreateCmssyRobotsOptions): () => Promise<MetadataRoute.Robots>;
91
+
92
+ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
93
+ /** Extra static entries appended to the generated page list. */
94
+ extra?: MetadataRoute.Sitemap;
95
+ }
96
+ /**
97
+ * Builds the default export for Next's `app/sitemap.ts` from the workspace's
98
+ * published pages. Emits one entry per page with per-locale `alternates` when
99
+ * the config enables multiple locales. Drop in as:
100
+ *
101
+ * export default createCmssySitemap(cmssy);
102
+ */
103
+ declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
104
+
45
105
  type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
46
106
  defaultRedirect?: string;
47
107
  };
@@ -235,4 +295,4 @@ declare class CmssyWebhookError extends Error {
235
295
  */
236
296
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
237
297
 
238
- export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyPageOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
298
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, type CmssyAuthConfig, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySessionPayload, type CmssySessionUser, CmssyWebhookError, type CmssyWebhookEvent, type CmssyWebhookOrder, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, type FetchProductOptions, type FetchProductsOptions, type MyOrdersResult, SESSION_MAX_AGE_SECONDS, type SessionCookieOptions, type VerifyCmssyWebhookOptions, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { draftMode, headers, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
- import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
3
+ import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
4
4
  import { CmssyLocaleProvider } from '@cmssy/react/client';
5
- import { jsx } from 'react/jsx-runtime';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
6
  import { createHmac, createHash, timingSafeEqual } from 'crypto';
7
7
  import { NextResponse } from 'next/server';
8
8
  import { EncryptJWT, jwtDecrypt } from 'jose';
@@ -160,6 +160,175 @@ function resolveBridgeOrigin(editorOrigin) {
160
160
  }
161
161
  return origin;
162
162
  }
163
+ function DefaultNotFound() {
164
+ return /* @__PURE__ */ jsxs(
165
+ "main",
166
+ {
167
+ style: {
168
+ minHeight: "60vh",
169
+ display: "flex",
170
+ flexDirection: "column",
171
+ alignItems: "center",
172
+ justifyContent: "center",
173
+ gap: "0.5rem",
174
+ textAlign: "center",
175
+ padding: "2rem"
176
+ },
177
+ children: [
178
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "2rem", fontWeight: 700, margin: 0 }, children: "404" }),
179
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, opacity: 0.7 }, children: "Page not found" })
180
+ ]
181
+ }
182
+ );
183
+ }
184
+ function createCmssyNotFound(config, blocks, options) {
185
+ if (!Array.isArray(blocks)) {
186
+ throw new Error(
187
+ "cmssy: createCmssyNotFound(config, blocks) requires a blocks array \u2014 pass your defineBlock(...) array"
188
+ );
189
+ }
190
+ const clientConfig = {
191
+ apiUrl: config.apiUrl,
192
+ workspaceSlug: config.workspaceSlug
193
+ };
194
+ const fallback = options?.fallback ?? /* @__PURE__ */ jsx(DefaultNotFound, {});
195
+ return async function CmssyNotFound() {
196
+ try {
197
+ const siteConfig = await fetchSiteConfig(clientConfig);
198
+ const notFoundPageId = siteConfig?.notFoundPageId;
199
+ if (!notFoundPageId) return fallback;
200
+ const page = await fetchPageById(clientConfig, notFoundPageId);
201
+ if (!page || page.blocks.length === 0) return fallback;
202
+ let locale;
203
+ let defaultLocale;
204
+ let enabledLocales = config.enabledLocales;
205
+ if (config.resolveLocale) {
206
+ defaultLocale = config.defaultLocale ?? "en";
207
+ locale = await config.resolveLocale();
208
+ } else {
209
+ const siteLocales = await resolveSiteLocales(clientConfig);
210
+ defaultLocale = config.defaultLocale ?? siteLocales.defaultLocale;
211
+ enabledLocales = config.enabledLocales ?? siteLocales.locales;
212
+ locale = defaultLocale;
213
+ }
214
+ const resolvedForms = await resolveForms(
215
+ clientConfig,
216
+ page.blocks,
217
+ locale,
218
+ defaultLocale
219
+ );
220
+ const forms = Object.keys(resolvedForms).length > 0 ? resolvedForms : void 0;
221
+ const localeContext = {
222
+ current: locale,
223
+ default: defaultLocale,
224
+ enabled: enabledLocales && enabledLocales.length > 0 ? enabledLocales : Array.from(/* @__PURE__ */ new Set([defaultLocale, locale]))
225
+ };
226
+ return /* @__PURE__ */ jsx(CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsx(
227
+ CmssyServerPage,
228
+ {
229
+ page,
230
+ blocks,
231
+ locale,
232
+ defaultLocale,
233
+ enabledLocales,
234
+ forms
235
+ }
236
+ ) });
237
+ } catch (err) {
238
+ if (typeof console !== "undefined") {
239
+ console.warn("[cmssy] not-found page render failed", err);
240
+ }
241
+ return fallback;
242
+ }
243
+ };
244
+ }
245
+
246
+ // src/seo-base-url.ts
247
+ function trimTrailingSlash(url) {
248
+ return url.replace(/\/+$/, "");
249
+ }
250
+ async function resolveSeoBaseUrl(config, option) {
251
+ if (typeof option === "function") return trimTrailingSlash(await option());
252
+ if (typeof option === "string" && option) return trimTrailingSlash(option);
253
+ if (config.siteUrl) return trimTrailingSlash(config.siteUrl);
254
+ const { headers: headers3 } = await import('next/headers');
255
+ const h = await headers3();
256
+ const host = h.get("host");
257
+ if (!host) return "";
258
+ const protocol = isLocalHost(host) ? "http" : "https";
259
+ return `${protocol}://${host}`;
260
+ }
261
+ function isLocalHost(host) {
262
+ const hostname = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
263
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname === "::1") {
264
+ return true;
265
+ }
266
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
267
+ if (!ipv4) return false;
268
+ const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
269
+ return a === 127 || a === 0 || a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31;
270
+ }
271
+
272
+ // src/create-cmssy-robots.ts
273
+ function createCmssyRobots(config, options = {}) {
274
+ return async function robots() {
275
+ const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
276
+ const rules = options.rules ?? {
277
+ userAgent: "*",
278
+ allow: "/",
279
+ disallow: options.disallow ?? ["/api/"]
280
+ };
281
+ const includeSitemap = options.sitemap !== false && Boolean(baseUrl);
282
+ return {
283
+ rules,
284
+ ...includeSitemap ? { sitemap: `${baseUrl}/sitemap.xml` } : {},
285
+ ...baseUrl ? { host: baseUrl } : {}
286
+ };
287
+ };
288
+ }
289
+ function localizedPath(slug, locale, defaultLocale) {
290
+ const base = slug === "/" ? "" : slug;
291
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
292
+ }
293
+ function createCmssySitemap(config, options = {}) {
294
+ const clientConfig = {
295
+ apiUrl: config.apiUrl,
296
+ workspaceSlug: config.workspaceSlug
297
+ };
298
+ return async function sitemap() {
299
+ let pages = [];
300
+ try {
301
+ pages = await fetchPages(clientConfig);
302
+ } catch (err) {
303
+ if (typeof console !== "undefined") {
304
+ console.warn("[cmssy] sitemap page fetch failed", err);
305
+ }
306
+ pages = [];
307
+ }
308
+ const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
309
+ const defaultLocale = config.defaultLocale ?? "en";
310
+ const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
311
+ const entries = pages.map((page) => {
312
+ const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
313
+ const entry = {
314
+ url: `${baseUrl}${localizedPath(page.slug, defaultLocale, defaultLocale)}`,
315
+ ...lastModified ? { lastModified: new Date(lastModified) } : {}
316
+ };
317
+ if (locales.length > 1) {
318
+ entry.alternates = {
319
+ languages: Object.fromEntries(
320
+ locales.map((locale) => [
321
+ locale,
322
+ `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`
323
+ ])
324
+ )
325
+ };
326
+ }
327
+ return entry;
328
+ });
329
+ return options.extra ? [...entries, ...options.extra] : entries;
330
+ };
331
+ }
163
332
  var MIN_SECRET_LENGTH = 16;
164
333
  function secretsMatch(a, b) {
165
334
  const ha = createHash("sha256").update(a).digest();
@@ -1328,4 +1497,4 @@ function verifyCmssyWebhook(options) {
1328
1497
  return parsed;
1329
1498
  }
1330
1499
 
1331
- export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyOrdersRoute, createCmssyPage, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
1500
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, cmssyCspHeaders, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Next.js App Router bindings for cmssy headless sites (createCmssyPage + draft preview)",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -41,7 +41,7 @@
41
41
  "dist"
42
42
  ],
43
43
  "peerDependencies": {
44
- "@cmssy/react": "^0.3.0",
44
+ "@cmssy/react": "^0.5.0",
45
45
  "next": ">=15",
46
46
  "react": "^18.2.0 || ^19.0.0",
47
47
  "react-dom": "^18.2.0 || ^19.0.0"
@@ -54,7 +54,7 @@
54
54
  "tsup": "^8.3.0",
55
55
  "typescript": "^5.6.0",
56
56
  "vitest": "^2.1.0",
57
- "@cmssy/react": "0.3.0"
57
+ "@cmssy/react": "0.5.0"
58
58
  },
59
59
  "dependencies": {
60
60
  "jose": "^6.2.3"