@cmssy/next 4.0.1 → 4.2.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/dist/index.cjs CHANGED
@@ -586,25 +586,29 @@ function createCmssySitemap(config, options = {}) {
586
586
  const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
587
587
  const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
588
588
  const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
589
- const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
590
- const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
591
- const entry = {
592
- url: `${baseUrl}${localizedPath(page.slug, defaultLocale, defaultLocale)}`,
593
- ...lastModified ? { lastModified: new Date(lastModified) } : {}
594
- };
595
- if (locales.length > 1) {
596
- entry.alternates = {
597
- languages: Object.fromEntries(
598
- locales.map((locale) => [
599
- locale,
600
- `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`
601
- ])
602
- )
603
- };
589
+ const languagesFor = (slug) => locales.length > 1 ? {
590
+ languages: {
591
+ ...Object.fromEntries(
592
+ locales.map((locale) => [
593
+ locale,
594
+ `${baseUrl}${localizedPath(slug, locale, defaultLocale)}`
595
+ ])
596
+ ),
597
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
604
598
  }
605
- return entry;
599
+ } : void 0;
600
+ const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).flatMap((page) => {
601
+ const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
602
+ const alternates = languagesFor(page.slug);
603
+ return locales.map((locale) => ({
604
+ url: `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`,
605
+ ...lastModified ? { lastModified: new Date(lastModified) } : {},
606
+ ...alternates ? { alternates } : {}
607
+ }));
606
608
  });
607
- return options.extra ? [...entries, ...options.extra] : entries;
609
+ if (!options.extra) return entries;
610
+ const extra = typeof options.extra === "function" ? await options.extra({ baseUrl, defaultLocale, locales }) : options.extra;
611
+ return [...entries, ...extra];
608
612
  };
609
613
  }
610
614
  function pick(value, locale, defaultLocale) {
@@ -618,8 +622,7 @@ async function buildCmssyMetadata(config, path, options = {}) {
618
622
  org: config.org,
619
623
  workspaceSlug: config.workspaceSlug
620
624
  };
621
- const [meta, siteConfig, baseUrl] = await Promise.all([
622
- react.fetchPageMeta(clientConfig, path).catch(() => null),
625
+ const [siteConfig, baseUrl] = await Promise.all([
623
626
  react.fetchSiteConfig(clientConfig).catch(() => null),
624
627
  resolveSeoBaseUrl(config, options.baseUrl)
625
628
  ]);
@@ -627,20 +630,30 @@ async function buildCmssyMetadata(config, path, options = {}) {
627
630
  config,
628
631
  siteConfig
629
632
  );
630
- const locale = await config.resolveLocale?.() ?? defaultLocale;
631
- const slug = react.normalizeSlug(path);
633
+ const segments = Array.isArray(path) ? path : path ? path.split("/").filter(Boolean) : void 0;
634
+ const fromPath = react.splitLocaleFromPath(segments, {
635
+ defaultLocale,
636
+ locales: enabledLocales
637
+ });
638
+ const locale = options.locale ?? (fromPath.locale !== defaultLocale ? fromPath.locale : await config.resolveLocale?.() ?? defaultLocale);
639
+ const slug = react.normalizeSlug(fromPath.path);
640
+ const meta = await react.fetchPageMeta(clientConfig, slug).catch(() => null);
632
641
  const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
633
642
  const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
634
643
  const description = pick(meta?.seoDescription, locale, defaultLocale);
635
644
  const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
636
645
  const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
637
646
  const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
638
- const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
639
- enabledLocales.map((l) => [
640
- l,
641
- `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
642
- ])
643
- ) : void 0;
647
+ const languages = baseUrl && enabledLocales.length > 1 ? {
648
+ ...Object.fromEntries(
649
+ enabledLocales.map((l) => [
650
+ l,
651
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
652
+ ])
653
+ ),
654
+ // What to serve a reader whose language none of these match.
655
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
656
+ } : void 0;
644
657
  return {
645
658
  ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
646
659
  ...title ? { title } : {},
package/dist/index.d.cts CHANGED
@@ -180,9 +180,20 @@ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
180
180
  */
181
181
  declare function createCmssyRobots(config: CmssyNextConfig, options?: CreateCmssyRobotsOptions): () => Promise<MetadataRoute.Robots>;
182
182
 
183
+ /** What an `extra` resolver needs to build URLs that agree with the page ones. */
184
+ interface CmssySitemapContext {
185
+ baseUrl: string;
186
+ defaultLocale: string;
187
+ locales: string[];
188
+ }
183
189
  interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
184
- /** Extra static entries appended to the generated page list. */
185
- extra?: MetadataRoute.Sitemap;
190
+ /**
191
+ * Entries appended to the generated page list - the URLs a workspace's PAGES
192
+ * cannot express, like a product or a category rendered from model records.
193
+ * Pass a resolver to build them at request time; it gets the same baseUrl and
194
+ * locales the page entries use, so the two cannot disagree.
195
+ */
196
+ extra?: MetadataRoute.Sitemap | ((context: CmssySitemapContext) => MetadataRoute.Sitemap | Promise<MetadataRoute.Sitemap>);
186
197
  /**
187
198
  * Additional page slugs to omit. The workspace's configured 404 page
188
199
  * (Settings → 404 page) is excluded automatically via its id, so this is
@@ -206,6 +217,12 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
206
217
  ogType?: string;
207
218
  /** Twitter card. Defaults to "summary_large_image" when an image exists. */
208
219
  twitterCard?: "summary" | "summary_large_image";
220
+ /**
221
+ * The language to render metadata in. Only needed when the language does not
222
+ * live in the URL (a per-domain or cookie strategy); with a locale prefix the
223
+ * path already says it.
224
+ */
225
+ locale?: string;
209
226
  }
210
227
  /**
211
228
  * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
@@ -213,10 +230,14 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
213
230
  * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
214
231
  * image). Use in a route's `generateMetadata`:
215
232
  *
216
- * export const generateMetadata = ({ params }) =>
233
+ * export const generateMetadata = async ({ params }) =>
217
234
  * buildCmssyMetadata(cmssy, (await params).path);
218
235
  *
219
- * `path` is the catch-all segments with the locale prefix already stripped.
236
+ * Pass the catch-all segments **as routed**, locale prefix and all: the prefix
237
+ * is what says which language this page is. Stripping it first (and leaving the
238
+ * language to `config.resolveLocale`) is how every localized page ends up with
239
+ * the default language's title - and a canonical pointing at the default
240
+ * language's URL, which tells Google the translation is a duplicate.
220
241
  */
221
242
  declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
222
243
 
@@ -356,4 +377,4 @@ declare class CmssyWebhookError extends Error {
356
377
  */
357
378
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
358
379
 
359
- export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_EDIT_PATH_PREFIX, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CMSSY_SESSION_COOKIE, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyEnvConfig, type CmssyNextConfig, type CmssyOrdersRouteHandlers, CmssyWebhookError, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, DEFAULT_CMSSY_EDITOR_ORIGINS, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, cmssyEditRewrite, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyEditMiddleware, createCmssyEditPage, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, defineCmssyConfig, fetchOrderByToken, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
380
+ export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_EDIT_PATH_PREFIX, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CMSSY_SESSION_COOKIE, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyEnvConfig, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySitemapContext, CmssyWebhookError, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, DEFAULT_CMSSY_EDITOR_ORIGINS, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, cmssyEditRewrite, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyEditMiddleware, createCmssyEditPage, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, defineCmssyConfig, fetchOrderByToken, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.d.ts CHANGED
@@ -180,9 +180,20 @@ interface CreateCmssyRobotsOptions extends SeoBaseUrlOption {
180
180
  */
181
181
  declare function createCmssyRobots(config: CmssyNextConfig, options?: CreateCmssyRobotsOptions): () => Promise<MetadataRoute.Robots>;
182
182
 
183
+ /** What an `extra` resolver needs to build URLs that agree with the page ones. */
184
+ interface CmssySitemapContext {
185
+ baseUrl: string;
186
+ defaultLocale: string;
187
+ locales: string[];
188
+ }
183
189
  interface CreateCmssySitemapOptions extends SeoBaseUrlOption {
184
- /** Extra static entries appended to the generated page list. */
185
- extra?: MetadataRoute.Sitemap;
190
+ /**
191
+ * Entries appended to the generated page list - the URLs a workspace's PAGES
192
+ * cannot express, like a product or a category rendered from model records.
193
+ * Pass a resolver to build them at request time; it gets the same baseUrl and
194
+ * locales the page entries use, so the two cannot disagree.
195
+ */
196
+ extra?: MetadataRoute.Sitemap | ((context: CmssySitemapContext) => MetadataRoute.Sitemap | Promise<MetadataRoute.Sitemap>);
186
197
  /**
187
198
  * Additional page slugs to omit. The workspace's configured 404 page
188
199
  * (Settings → 404 page) is excluded automatically via its id, so this is
@@ -206,6 +217,12 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
206
217
  ogType?: string;
207
218
  /** Twitter card. Defaults to "summary_large_image" when an image exists. */
208
219
  twitterCard?: "summary" | "summary_large_image";
220
+ /**
221
+ * The language to render metadata in. Only needed when the language does not
222
+ * live in the URL (a per-domain or cookie strategy); with a locale prefix the
223
+ * path already says it.
224
+ */
225
+ locale?: string;
209
226
  }
210
227
  /**
211
228
  * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
@@ -213,10 +230,14 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
213
230
  * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
214
231
  * image). Use in a route's `generateMetadata`:
215
232
  *
216
- * export const generateMetadata = ({ params }) =>
233
+ * export const generateMetadata = async ({ params }) =>
217
234
  * buildCmssyMetadata(cmssy, (await params).path);
218
235
  *
219
- * `path` is the catch-all segments with the locale prefix already stripped.
236
+ * Pass the catch-all segments **as routed**, locale prefix and all: the prefix
237
+ * is what says which language this page is. Stripping it first (and leaving the
238
+ * language to `config.resolveLocale`) is how every localized page ends up with
239
+ * the default language's title - and a canonical pointing at the default
240
+ * language's URL, which tells Google the translation is a duplicate.
220
241
  */
221
242
  declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
222
243
 
@@ -356,4 +377,4 @@ declare class CmssyWebhookError extends Error {
356
377
  */
357
378
  declare function verifyCmssyWebhook(options: VerifyCmssyWebhookOptions): CmssyWebhookEvent;
358
379
 
359
- export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_EDIT_PATH_PREFIX, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CMSSY_SESSION_COOKIE, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyEnvConfig, type CmssyNextConfig, type CmssyOrdersRouteHandlers, CmssyWebhookError, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, DEFAULT_CMSSY_EDITOR_ORIGINS, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, cmssyEditRewrite, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyEditMiddleware, createCmssyEditPage, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, defineCmssyConfig, fetchOrderByToken, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
380
+ export { type BuildCmssyMetadataOptions, CMSSY_CART_COOKIE, CMSSY_EDIT_HEADER, CMSSY_EDIT_PATH_PREFIX, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CMSSY_SESSION_COOKIE, type CmssyAuthMiddleware, type CmssyAuthRouteHandlers, type CmssyCartRouteHandlers, type CmssyCspOptions, type CmssyDraftRouteConfig, type CmssyEditorProps, type CmssyEnvConfig, type CmssyNextConfig, type CmssyOrdersRouteHandlers, type CmssySitemapContext, CmssyWebhookError, type CreateCmssyNotFoundOptions, type CreateCmssyPageOptions, type CreateCmssyRobotsOptions, type CreateCmssySitemapOptions, DEFAULT_CMSSY_EDITOR_ORIGINS, SESSION_MAX_AGE_SECONDS, applyCmssyCsp, assertAuthConfig, buildCmssyMetadata, cmssyCspHeaders, cmssyEditRewrite, createCmssyAuthMiddleware, createCmssyAuthRoute, createCmssyCartRoute, createCmssyEditMiddleware, createCmssyEditPage, createCmssyLocaleMiddleware, createCmssyNotFound, createCmssyOrdersRoute, createCmssyPage, createCmssyRobots, createCmssySitemap, createDraftRoute, defineCmssyConfig, fetchOrderByToken, fetchProduct, fetchProducts, getCmssyAccessToken, getCmssyLocale, getCmssyUser, isAccessExpired, isCmssyEditMode, isCmssyEditRequest, localeForPathname, openSession, resolveEditorOrigin, resolveLocaleFromPathname, sealSession, sessionCookieOptions, splitCmssyLocale, verifyCmssyWebhook };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { headers, draftMode, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
- import { createCmssyClient, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest, resolveApiUrl } from '@cmssy/react';
3
+ import { createCmssyClient, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, normalizeSlug as normalizeSlug$1, fetchPageMeta, resolveWorkspaceId, graphqlRequest, resolveApiUrl } from '@cmssy/react';
4
4
  export { DEFAULT_CMSSY_API_URL, evaluateFieldConditionGroup, resolveApiUrl } from '@cmssy/react';
5
5
  import { CmssyLocaleProvider } from '@cmssy/react/client';
6
6
  import { EncryptJWT, jwtDecrypt } from 'jose';
@@ -585,25 +585,29 @@ function createCmssySitemap(config, options = {}) {
585
585
  const baseUrl = await resolveSeoBaseUrl(config, options.baseUrl);
586
586
  const { defaultLocale, locales } = resolveSeoLocales(config, siteConfig);
587
587
  const excluded = new Set((options.excludeSlugs ?? []).map(normalizeSlug));
588
- const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).map((page) => {
589
- const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
590
- const entry = {
591
- url: `${baseUrl}${localizedPath(page.slug, defaultLocale, defaultLocale)}`,
592
- ...lastModified ? { lastModified: new Date(lastModified) } : {}
593
- };
594
- if (locales.length > 1) {
595
- entry.alternates = {
596
- languages: Object.fromEntries(
597
- locales.map((locale) => [
598
- locale,
599
- `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`
600
- ])
601
- )
602
- };
588
+ const languagesFor = (slug) => locales.length > 1 ? {
589
+ languages: {
590
+ ...Object.fromEntries(
591
+ locales.map((locale) => [
592
+ locale,
593
+ `${baseUrl}${localizedPath(slug, locale, defaultLocale)}`
594
+ ])
595
+ ),
596
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
603
597
  }
604
- return entry;
598
+ } : void 0;
599
+ const entries = pages.map((page) => ({ ...page, slug: normalizeSlug(page.slug) })).filter((page) => page.id !== notFoundPageId && !excluded.has(page.slug)).flatMap((page) => {
600
+ const lastModified = page.updatedAt ?? page.publishedAt ?? void 0;
601
+ const alternates = languagesFor(page.slug);
602
+ return locales.map((locale) => ({
603
+ url: `${baseUrl}${localizedPath(page.slug, locale, defaultLocale)}`,
604
+ ...lastModified ? { lastModified: new Date(lastModified) } : {},
605
+ ...alternates ? { alternates } : {}
606
+ }));
605
607
  });
606
- return options.extra ? [...entries, ...options.extra] : entries;
608
+ if (!options.extra) return entries;
609
+ const extra = typeof options.extra === "function" ? await options.extra({ baseUrl, defaultLocale, locales }) : options.extra;
610
+ return [...entries, ...extra];
607
611
  };
608
612
  }
609
613
  function pick(value, locale, defaultLocale) {
@@ -617,8 +621,7 @@ async function buildCmssyMetadata(config, path, options = {}) {
617
621
  org: config.org,
618
622
  workspaceSlug: config.workspaceSlug
619
623
  };
620
- const [meta, siteConfig, baseUrl] = await Promise.all([
621
- fetchPageMeta(clientConfig, path).catch(() => null),
624
+ const [siteConfig, baseUrl] = await Promise.all([
622
625
  fetchSiteConfig(clientConfig).catch(() => null),
623
626
  resolveSeoBaseUrl(config, options.baseUrl)
624
627
  ]);
@@ -626,20 +629,30 @@ async function buildCmssyMetadata(config, path, options = {}) {
626
629
  config,
627
630
  siteConfig
628
631
  );
629
- const locale = await config.resolveLocale?.() ?? defaultLocale;
630
- const slug = normalizeSlug$1(path);
632
+ const segments = Array.isArray(path) ? path : path ? path.split("/").filter(Boolean) : void 0;
633
+ const fromPath = splitLocaleFromPath(segments, {
634
+ defaultLocale,
635
+ locales: enabledLocales
636
+ });
637
+ const locale = options.locale ?? (fromPath.locale !== defaultLocale ? fromPath.locale : await config.resolveLocale?.() ?? defaultLocale);
638
+ const slug = normalizeSlug$1(fromPath.path);
639
+ const meta = await fetchPageMeta(clientConfig, slug).catch(() => null);
631
640
  const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
632
641
  const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
633
642
  const description = pick(meta?.seoDescription, locale, defaultLocale);
634
643
  const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
635
644
  const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
636
645
  const canonical = baseUrl ? `${baseUrl}${localizedPath(slug, locale, defaultLocale)}` : void 0;
637
- const languages = baseUrl && enabledLocales.length > 1 ? Object.fromEntries(
638
- enabledLocales.map((l) => [
639
- l,
640
- `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
641
- ])
642
- ) : void 0;
646
+ const languages = baseUrl && enabledLocales.length > 1 ? {
647
+ ...Object.fromEntries(
648
+ enabledLocales.map((l) => [
649
+ l,
650
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
651
+ ])
652
+ ),
653
+ // What to serve a reader whose language none of these match.
654
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
655
+ } : void 0;
643
656
  return {
644
657
  ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
645
658
  ...title ? { title } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "4.0.1",
3
+ "version": "4.2.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": "^4.0.1",
44
+ "@cmssy/react": "^4.2.0",
45
45
  "next": ">=15",
46
46
  "react": "^18.2.0 || ^19.0.0",
47
47
  "react-dom": "^18.2.0 || ^19.0.0"
@@ -55,7 +55,7 @@
55
55
  "tsup": "^8.3.0",
56
56
  "typescript": "^5.6.0",
57
57
  "vitest": "^2.1.0",
58
- "@cmssy/react": "4.0.1"
58
+ "@cmssy/react": "4.2.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "jose": "^6.2.3",