@cmssy/next 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -284,18 +284,28 @@ function createCmssyRobots(config, options = {}) {
284
284
  return {
285
285
  rules,
286
286
  ...includeSitemap ? { sitemap: `${baseUrl}/sitemap.xml` } : {},
287
- ...baseUrl ? { host: baseUrl } : {}
287
+ ...options.host && baseUrl ? { host: baseUrl } : {}
288
288
  };
289
289
  };
290
290
  }
291
+
292
+ // src/seo-paths.ts
293
+ function resolveSeoLocales(config, siteConfig) {
294
+ const defaultLocale = config.defaultLocale ?? siteConfig?.defaultLanguage ?? "en";
295
+ const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : siteConfig?.enabledLanguages && siteConfig.enabledLanguages.length > 0 ? siteConfig.enabledLanguages : [defaultLocale];
296
+ return { defaultLocale, locales };
297
+ }
291
298
  function normalizeSlug(slug) {
292
299
  if (slug === "/" || slug === "") return "/";
293
300
  return slug.startsWith("/") ? slug : `/${slug}`;
294
301
  }
295
302
  function localizedPath(slug, locale, defaultLocale) {
296
- const normalized = slug === "/" ? "" : slug;
297
- return locale === defaultLocale ? normalized || "/" : `/${locale}${normalized}`;
303
+ const normalized = normalizeSlug(slug);
304
+ const base = normalized === "/" ? "" : normalized;
305
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
298
306
  }
307
+
308
+ // src/create-cmssy-sitemap.ts
299
309
  function createCmssySitemap(config, options = {}) {
300
310
  const clientConfig = {
301
311
  apiUrl: config.apiUrl,
@@ -311,15 +321,15 @@ function createCmssySitemap(config, options = {}) {
311
321
  }
312
322
  pages = [];
313
323
  }
314
- let notFoundPageId = null;
324
+ let siteConfig = null;
315
325
  try {
316
- notFoundPageId = (await react.fetchSiteConfig(clientConfig))?.notFoundPageId ?? null;
326
+ siteConfig = await react.fetchSiteConfig(clientConfig);
317
327
  } catch {
318
- notFoundPageId = null;
328
+ siteConfig = null;
319
329
  }
330
+ const notFoundPageId = siteConfig?.notFoundPageId ?? null;
320
331
  const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
321
- const defaultLocale = config.defaultLocale ?? "en";
322
- const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
332
+ const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
323
333
  const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
324
334
  const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
325
335
  const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
@@ -342,6 +352,67 @@ function createCmssySitemap(config, options = {}) {
342
352
  return options.extra ? [...entries, ...options.extra] : entries;
343
353
  };
344
354
  }
355
+ function pick(value, locale, defaultLocale) {
356
+ if (!value) return "";
357
+ if (typeof value === "string") return value;
358
+ return value[locale] || value[defaultLocale] || Object.values(value)[0] || "";
359
+ }
360
+ async function buildCmssyMetadata(config, path, options = {}) {
361
+ const clientConfig = {
362
+ apiUrl: config.apiUrl,
363
+ workspaceSlug: config.workspaceSlug
364
+ };
365
+ const [meta, siteConfig, baseUrl] = await Promise.all([
366
+ react.fetchPageMeta(clientConfig, path).catch(() => null),
367
+ react.fetchSiteConfig(clientConfig).catch(() => null),
368
+ resolveSeoBaseUrl(config, options.baseUrl)
369
+ ]);
370
+ const { defaultLocale, locales: enabledLocales } = resolveSeoLocales(
371
+ config,
372
+ siteConfig
373
+ );
374
+ const locale = await config.resolveLocale?.() ?? defaultLocale;
375
+ const slug = react.normalizeSlug(path);
376
+ const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
377
+ const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
378
+ const description = pick(meta?.seoDescription, locale, defaultLocale);
379
+ const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
380
+ const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
381
+ const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
382
+ const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
383
+ enabledLocales.map((l) => [
384
+ l,
385
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
386
+ ])
387
+ ) : void 0;
388
+ return {
389
+ ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
390
+ ...title ? { title } : {},
391
+ ...description ? { description } : {},
392
+ ...keywords ? { keywords } : {},
393
+ ...canonical || languages ? {
394
+ alternates: {
395
+ ...canonical ? { canonical } : {},
396
+ ...languages ? { languages } : {}
397
+ }
398
+ } : {},
399
+ openGraph: {
400
+ ...title ? { title } : {},
401
+ ...description ? { description } : {},
402
+ ...canonical ? { url: canonical } : {},
403
+ ...siteName ? { siteName } : {},
404
+ type: options.ogType ?? "website",
405
+ locale,
406
+ ...image ? { images: [{ url: image }] } : {}
407
+ },
408
+ twitter: {
409
+ card: image ? options.twitterCard ?? "summary_large_image" : "summary",
410
+ ...title ? { title } : {},
411
+ ...description ? { description } : {},
412
+ ...image ? { images: [image] } : {}
413
+ }
414
+ };
415
+ }
345
416
  var MIN_SECRET_LENGTH = 16;
346
417
  function secretsMatch(a, b) {
347
418
  const ha = crypto$1.createHash("sha256").update(a).digest();
@@ -1518,6 +1589,7 @@ exports.CmssyWebhookError = CmssyWebhookError;
1518
1589
  exports.SESSION_MAX_AGE_SECONDS = SESSION_MAX_AGE_SECONDS;
1519
1590
  exports.applyCmssyCsp = applyCmssyCsp;
1520
1591
  exports.assertAuthConfig = assertAuthConfig;
1592
+ exports.buildCmssyMetadata = buildCmssyMetadata;
1521
1593
  exports.cmssyCspHeaders = cmssyCspHeaders;
1522
1594
  exports.createCmssyAuthMiddleware = createCmssyAuthMiddleware;
1523
1595
  exports.createCmssyAuthRoute = createCmssyAuthRoute;
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
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
+ import { MetadataRoute, Metadata } from 'next';
6
6
  import { NextRequest, NextResponse } from 'next/server';
7
7
 
8
8
  interface CmssyAuthConfig {
@@ -80,10 +80,16 @@ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
80
80
  rules?: MetadataRoute.Robots["rules"];
81
81
  /** Reference `${baseUrl}/sitemap.xml`. Defaults to true. */
82
82
  sitemap?: boolean;
83
+ /**
84
+ * Emit the non-standard `Host:` directive (a Yandex extension). Google
85
+ * ignores it and reports a warning in Search Console, so it is off by
86
+ * default. Enable only when targeting Yandex.
87
+ */
88
+ host?: boolean;
83
89
  }
84
90
  /**
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:
91
+ * Builds the default export for Next's `app/robots.ts`. Allows crawling and
92
+ * points to the sitemap. Drop in as:
87
93
  *
88
94
  * export default createCmssyRobots(cmssy);
89
95
  */
@@ -108,6 +114,27 @@ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
108
114
  */
109
115
  declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
110
116
 
117
+ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
118
+ /** Override the Open Graph / Twitter image (defaults to workspace branding). */
119
+ image?: string;
120
+ /** Open Graph type. Defaults to "website". */
121
+ ogType?: string;
122
+ /** Twitter card. Defaults to "summary_large_image" when an image exists. */
123
+ twitterCard?: "summary" | "summary_large_image";
124
+ }
125
+ /**
126
+ * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
127
+ * the workspace branding: title/description/keywords, canonical + per-locale
128
+ * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
129
+ * image). Use in a route's `generateMetadata`:
130
+ *
131
+ * export const generateMetadata = ({ params }) =>
132
+ * buildCmssyMetadata(cmssy, (await params).path);
133
+ *
134
+ * `path` is the catch-all segments with the locale prefix already stripped.
135
+ */
136
+ declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
137
+
111
138
  type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
112
139
  defaultRedirect?: string;
113
140
  };
@@ -301,4 +328,4 @@ declare class CmssyWebhookError extends Error {
301
328
  */
302
329
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
303
330
 
304
- 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 };
331
+ export { type BuildCmssyMetadataOptions, 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, buildCmssyMetadata, 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
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
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
+ import { MetadataRoute, Metadata } from 'next';
6
6
  import { NextRequest, NextResponse } from 'next/server';
7
7
 
8
8
  interface CmssyAuthConfig {
@@ -80,10 +80,16 @@ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
80
80
  rules?: MetadataRoute.Robots["rules"];
81
81
  /** Reference `${baseUrl}/sitemap.xml`. Defaults to true. */
82
82
  sitemap?: boolean;
83
+ /**
84
+ * Emit the non-standard `Host:` directive (a Yandex extension). Google
85
+ * ignores it and reports a warning in Search Console, so it is off by
86
+ * default. Enable only when targeting Yandex.
87
+ */
88
+ host?: boolean;
83
89
  }
84
90
  /**
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:
91
+ * Builds the default export for Next's `app/robots.ts`. Allows crawling and
92
+ * points to the sitemap. Drop in as:
87
93
  *
88
94
  * export default createCmssyRobots(cmssy);
89
95
  */
@@ -108,6 +114,27 @@ interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
108
114
  */
109
115
  declare function createCmssySitemap(config: CmssyNextConfig, options?: CreateCmssySitemapOptions): () => Promise<MetadataRoute.Sitemap>;
110
116
 
117
+ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
118
+ /** Override the Open Graph / Twitter image (defaults to workspace branding). */
119
+ image?: string;
120
+ /** Open Graph type. Defaults to "website". */
121
+ ogType?: string;
122
+ /** Twitter card. Defaults to "summary_large_image" when an image exists. */
123
+ twitterCard?: "summary" | "summary_large_image";
124
+ }
125
+ /**
126
+ * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
127
+ * the workspace branding: title/description/keywords, canonical + per-locale
128
+ * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
129
+ * image). Use in a route's `generateMetadata`:
130
+ *
131
+ * export const generateMetadata = ({ params }) =>
132
+ * buildCmssyMetadata(cmssy, (await params).path);
133
+ *
134
+ * `path` is the catch-all segments with the locale prefix already stripped.
135
+ */
136
+ declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
137
+
111
138
  type CmssyDraftRouteConfig = Pick<CmssyNextConfig, "draftSecret"> & {
112
139
  defaultRedirect?: string;
113
140
  };
@@ -301,4 +328,4 @@ declare class CmssyWebhookError extends Error {
301
328
  */
302
329
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
303
330
 
304
- 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 };
331
+ export { type BuildCmssyMetadataOptions, 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, buildCmssyMetadata, 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,6 +1,6 @@
1
1
  import { draftMode, headers, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
- import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
3
+ import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
4
4
  import { CmssyLocaleProvider } from '@cmssy/react/client';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
  import { createHmac, createHash, timingSafeEqual } from 'crypto';
@@ -282,18 +282,28 @@ function createCmssyRobots(config, options = {}) {
282
282
  return {
283
283
  rules,
284
284
  ...includeSitemap ? { sitemap: `${baseUrl}/sitemap.xml` } : {},
285
- ...baseUrl ? { host: baseUrl } : {}
285
+ ...options.host && baseUrl ? { host: baseUrl } : {}
286
286
  };
287
287
  };
288
288
  }
289
+
290
+ // src/seo-paths.ts
291
+ function resolveSeoLocales(config, siteConfig) {
292
+ const defaultLocale = config.defaultLocale ?? siteConfig?.defaultLanguage ?? "en";
293
+ const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : siteConfig?.enabledLanguages && siteConfig.enabledLanguages.length > 0 ? siteConfig.enabledLanguages : [defaultLocale];
294
+ return { defaultLocale, locales };
295
+ }
289
296
  function normalizeSlug(slug) {
290
297
  if (slug === "/" || slug === "") return "/";
291
298
  return slug.startsWith("/") ? slug : `/${slug}`;
292
299
  }
293
300
  function localizedPath(slug, locale, defaultLocale) {
294
- const normalized = slug === "/" ? "" : slug;
295
- return locale === defaultLocale ? normalized || "/" : `/${locale}${normalized}`;
301
+ const normalized = normalizeSlug(slug);
302
+ const base = normalized === "/" ? "" : normalized;
303
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
296
304
  }
305
+
306
+ // src/create-cmssy-sitemap.ts
297
307
  function createCmssySitemap(config, options = {}) {
298
308
  const clientConfig = {
299
309
  apiUrl: config.apiUrl,
@@ -309,15 +319,15 @@ function createCmssySitemap(config, options = {}) {
309
319
  }
310
320
  pages = [];
311
321
  }
312
- let notFoundPageId = null;
322
+ let siteConfig = null;
313
323
  try {
314
- notFoundPageId = (await fetchSiteConfig(clientConfig))?.notFoundPageId ?? null;
324
+ siteConfig = await fetchSiteConfig(clientConfig);
315
325
  } catch {
316
- notFoundPageId = null;
326
+ siteConfig = null;
317
327
  }
328
+ const notFoundPageId = siteConfig?.notFoundPageId ?? null;
318
329
  const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
319
- const defaultLocale = config.defaultLocale ?? "en";
320
- const locales = config.enabledLocales && config.enabledLocales.length > 0 ? config.enabledLocales : [defaultLocale];
330
+ const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
321
331
  const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
322
332
  const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
323
333
  const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
@@ -340,6 +350,67 @@ function createCmssySitemap(config, options = {}) {
340
350
  return options.extra ? [...entries, ...options.extra] : entries;
341
351
  };
342
352
  }
353
+ function pick(value, locale, defaultLocale) {
354
+ if (!value) return "";
355
+ if (typeof value === "string") return value;
356
+ return value[locale] || value[defaultLocale] || Object.values(value)[0] || "";
357
+ }
358
+ async function buildCmssyMetadata(config, path, options = {}) {
359
+ const clientConfig = {
360
+ apiUrl: config.apiUrl,
361
+ workspaceSlug: config.workspaceSlug
362
+ };
363
+ const [meta, siteConfig, baseUrl] = await Promise.all([
364
+ fetchPageMeta(clientConfig, path).catch(() => null),
365
+ fetchSiteConfig(clientConfig).catch(() => null),
366
+ resolveSeoBaseUrl(config, options.baseUrl)
367
+ ]);
368
+ const { defaultLocale, locales: enabledLocales } = resolveSeoLocales(
369
+ config,
370
+ siteConfig
371
+ );
372
+ const locale = await config.resolveLocale?.() ?? defaultLocale;
373
+ const slug = normalizeSlug$1(path);
374
+ const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
375
+ const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
376
+ const description = pick(meta?.seoDescription, locale, defaultLocale);
377
+ const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
378
+ const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
379
+ const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
380
+ const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
381
+ enabledLocales.map((l) => [
382
+ l,
383
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
384
+ ])
385
+ ) : void 0;
386
+ return {
387
+ ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
388
+ ...title ? { title } : {},
389
+ ...description ? { description } : {},
390
+ ...keywords ? { keywords } : {},
391
+ ...canonical || languages ? {
392
+ alternates: {
393
+ ...canonical ? { canonical } : {},
394
+ ...languages ? { languages } : {}
395
+ }
396
+ } : {},
397
+ openGraph: {
398
+ ...title ? { title } : {},
399
+ ...description ? { description } : {},
400
+ ...canonical ? { url: canonical } : {},
401
+ ...siteName ? { siteName } : {},
402
+ type: options.ogType ?? "website",
403
+ locale,
404
+ ...image ? { images: [{ url: image }] } : {}
405
+ },
406
+ twitter: {
407
+ card: image ? options.twitterCard ?? "summary_large_image" : "summary",
408
+ ...title ? { title } : {},
409
+ ...description ? { description } : {},
410
+ ...image ? { images: [image] } : {}
411
+ }
412
+ };
413
+ }
343
414
  var MIN_SECRET_LENGTH = 16;
344
415
  function secretsMatch(a, b) {
345
416
  const ha = createHash("sha256").update(a).digest();
@@ -1508,4 +1579,4 @@ function verifyCmssyWebhook(options) {
1508
1579
  return parsed;
1509
1580
  }
1510
1581
 
1511
- 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 };
1582
+ export { CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_LOCALE_HEADER, CMSSY_SESSION_COOKIE, CmssyWebhookError, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, 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.5.2",
3
+ "version": "0.5.4",
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.5.2",
44
+ "@cmssy/react": "^0.5.4",
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.5.2"
57
+ "@cmssy/react": "0.5.4"
58
58
  },
59
59
  "dependencies": {
60
60
  "jose": "^6.2.3"