@cmssy/next 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -586,23 +586,25 @@ 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
609
  return options.extra ? [...entries, ...options.extra] : entries;
608
610
  };
@@ -618,8 +620,7 @@ async function buildCmssyMetadata(config, path, options = {}) {
618
620
  org: config.org,
619
621
  workspaceSlug: config.workspaceSlug
620
622
  };
621
- const [meta, siteConfig, baseUrl] = await Promise.all([
622
- react.fetchPageMeta(clientConfig, path).catch(() => null),
623
+ const [siteConfig, baseUrl] = await Promise.all([
623
624
  react.fetchSiteConfig(clientConfig).catch(() => null),
624
625
  resolveSeoBaseUrl(config, options.baseUrl)
625
626
  ]);
@@ -627,20 +628,30 @@ async function buildCmssyMetadata(config, path, options = {}) {
627
628
  config,
628
629
  siteConfig
629
630
  );
630
- const locale = await config.resolveLocale?.() ?? defaultLocale;
631
- const slug = react.normalizeSlug(path);
631
+ const segments = Array.isArray(path) ? path : path ? path.split("/").filter(Boolean) : void 0;
632
+ const fromPath = react.splitLocaleFromPath(segments, {
633
+ defaultLocale,
634
+ locales: enabledLocales
635
+ });
636
+ const locale = options.locale ?? (fromPath.locale !== defaultLocale ? fromPath.locale : await config.resolveLocale?.() ?? defaultLocale);
637
+ const slug = react.normalizeSlug(fromPath.path);
638
+ const meta = await react.fetchPageMeta(clientConfig, slug).catch(() => null);
632
639
  const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
633
640
  const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
634
641
  const description = pick(meta?.seoDescription, locale, defaultLocale);
635
642
  const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
636
643
  const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
637
644
  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;
645
+ const languages = baseUrl && enabledLocales.length > 1 ? {
646
+ ...Object.fromEntries(
647
+ enabledLocales.map((l) => [
648
+ l,
649
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
650
+ ])
651
+ ),
652
+ // What to serve a reader whose language none of these match.
653
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
654
+ } : void 0;
644
655
  return {
645
656
  ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
646
657
  ...title ? { title } : {},
package/dist/index.d.cts CHANGED
@@ -206,6 +206,12 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
206
206
  ogType?: string;
207
207
  /** Twitter card. Defaults to "summary_large_image" when an image exists. */
208
208
  twitterCard?: "summary" | "summary_large_image";
209
+ /**
210
+ * The language to render metadata in. Only needed when the language does not
211
+ * live in the URL (a per-domain or cookie strategy); with a locale prefix the
212
+ * path already says it.
213
+ */
214
+ locale?: string;
209
215
  }
210
216
  /**
211
217
  * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
@@ -213,10 +219,14 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
213
219
  * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
214
220
  * image). Use in a route's `generateMetadata`:
215
221
  *
216
- * export const generateMetadata = ({ params }) =>
222
+ * export const generateMetadata = async ({ params }) =>
217
223
  * buildCmssyMetadata(cmssy, (await params).path);
218
224
  *
219
- * `path` is the catch-all segments with the locale prefix already stripped.
225
+ * Pass the catch-all segments **as routed**, locale prefix and all: the prefix
226
+ * is what says which language this page is. Stripping it first (and leaving the
227
+ * language to `config.resolveLocale`) is how every localized page ends up with
228
+ * the default language's title - and a canonical pointing at the default
229
+ * language's URL, which tells Google the translation is a duplicate.
220
230
  */
221
231
  declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
222
232
 
package/dist/index.d.ts CHANGED
@@ -206,6 +206,12 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
206
206
  ogType?: string;
207
207
  /** Twitter card. Defaults to "summary_large_image" when an image exists. */
208
208
  twitterCard?: "summary" | "summary_large_image";
209
+ /**
210
+ * The language to render metadata in. Only needed when the language does not
211
+ * live in the URL (a per-domain or cookie strategy); with a locale prefix the
212
+ * path already says it.
213
+ */
214
+ locale?: string;
209
215
  }
210
216
  /**
211
217
  * Builds complete Next.js `Metadata` for a cmssy page from its SEO fields and
@@ -213,10 +219,14 @@ interface BuildCmssyMetadataOptions extends SeoBaseUrlOption {
213
219
  * `hreflang` alternates, and Open Graph / Twitter cards (with the branding OG
214
220
  * image). Use in a route's `generateMetadata`:
215
221
  *
216
- * export const generateMetadata = ({ params }) =>
222
+ * export const generateMetadata = async ({ params }) =>
217
223
  * buildCmssyMetadata(cmssy, (await params).path);
218
224
  *
219
- * `path` is the catch-all segments with the locale prefix already stripped.
225
+ * Pass the catch-all segments **as routed**, locale prefix and all: the prefix
226
+ * is what says which language this page is. Stripping it first (and leaving the
227
+ * language to `config.resolveLocale`) is how every localized page ends up with
228
+ * the default language's title - and a canonical pointing at the default
229
+ * language's URL, which tells Google the translation is a duplicate.
220
230
  */
221
231
  declare function buildCmssyMetadata(config: CmssyNextConfig, path?: string | string[], options?: BuildCmssyMetadataOptions): Promise<Metadata>;
222
232
 
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,23 +585,25 @@ 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
608
  return options.extra ? [...entries, ...options.extra] : entries;
607
609
  };
@@ -617,8 +619,7 @@ async function buildCmssyMetadata(config, path, options = {}) {
617
619
  org: config.org,
618
620
  workspaceSlug: config.workspaceSlug
619
621
  };
620
- const [meta, siteConfig, baseUrl] = await Promise.all([
621
- fetchPageMeta(clientConfig, path).catch(() => null),
622
+ const [siteConfig, baseUrl] = await Promise.all([
622
623
  fetchSiteConfig(clientConfig).catch(() => null),
623
624
  resolveSeoBaseUrl(config, options.baseUrl)
624
625
  ]);
@@ -626,20 +627,30 @@ async function buildCmssyMetadata(config, path, options = {}) {
626
627
  config,
627
628
  siteConfig
628
629
  );
629
- const locale = await config.resolveLocale?.() ?? defaultLocale;
630
- const slug = normalizeSlug$1(path);
630
+ const segments = Array.isArray(path) ? path : path ? path.split("/").filter(Boolean) : void 0;
631
+ const fromPath = splitLocaleFromPath(segments, {
632
+ defaultLocale,
633
+ locales: enabledLocales
634
+ });
635
+ const locale = options.locale ?? (fromPath.locale !== defaultLocale ? fromPath.locale : await config.resolveLocale?.() ?? defaultLocale);
636
+ const slug = normalizeSlug$1(fromPath.path);
637
+ const meta = await fetchPageMeta(clientConfig, slug).catch(() => null);
631
638
  const siteName = pick(siteConfig?.siteName, locale, defaultLocale) || siteConfig?.branding?.brandName || void 0;
632
639
  const title = pick(meta?.seoTitle, locale, defaultLocale) || pick(meta?.displayName, locale, defaultLocale) || siteName || "";
633
640
  const description = pick(meta?.seoDescription, locale, defaultLocale);
634
641
  const keywords = meta?.seoKeywords?.length ? meta.seoKeywords : void 0;
635
642
  const image = options.image ?? siteConfig?.branding?.ogImageUrl ?? void 0;
636
643
  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;
644
+ const languages = baseUrl && enabledLocales.length > 1 ? {
645
+ ...Object.fromEntries(
646
+ enabledLocales.map((l) => [
647
+ l,
648
+ `${baseUrl}${localizedPath(slug, l, defaultLocale)}`
649
+ ])
650
+ ),
651
+ // What to serve a reader whose language none of these match.
652
+ "x-default": `${baseUrl}${localizedPath(slug, defaultLocale, defaultLocale)}`
653
+ } : void 0;
643
654
  return {
644
655
  ...baseUrl ? { metadataBase: new URL(baseUrl) } : {},
645
656
  ...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.1.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.1.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.1.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "jose": "^6.2.3",