@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 +8 -4
- package/dist/index.cjs +172 -0
- package/dist/index.d.cts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +172 -3
- package/package.json +3 -3
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
|
|
80
|
-
export const config = { matcher: ["/((?!_next
|
|
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
|
+
"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.
|
|
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.
|
|
57
|
+
"@cmssy/react": "0.5.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"jose": "^6.2.3"
|