@cmssy/next 4.0.0 → 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
@@ -398,7 +398,7 @@ function resolveBridgeOrigin(editorOrigin) {
398
398
  }
399
399
  return origins.length === 1 ? origins[0] : origins;
400
400
  }
401
- var CMSSY_EDIT_PATH_PREFIX = "/__cmssy/edit";
401
+ var CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
402
402
  async function cmssyEditRewrite(request2, config) {
403
403
  const { pathname, searchParams } = request2.nextUrl;
404
404
  if (pathname.startsWith(CMSSY_EDIT_PATH_PREFIX)) return null;
@@ -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
@@ -101,7 +101,7 @@ interface CatchAllProps {
101
101
  declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
102
102
  /**
103
103
  * Editor page for the middleware-rewritten edit route
104
- * (`app/__cmssy/edit/[[...path]]/page.tsx`, `dynamic = "force-dynamic"`).
104
+ * (`app/cmssy-edit/[[...path]]/page.tsx`, `dynamic = "force-dynamic"`).
105
105
  * Re-verifies the `cmssyEdit` + `cmssySecret` pair itself, so a direct hit
106
106
  * on the route path (bypassing the middleware) cannot mount the editor.
107
107
  */
@@ -111,11 +111,11 @@ declare function createCmssyEditPage(config: CmssyNextConfig, blocks: BlockDefin
111
111
  * Where the middleware rewrites editor-preview traffic. Mount the matching
112
112
  * dynamic route in the consumer app:
113
113
  *
114
- * app/__cmssy/edit/[[...path]]/page.tsx
114
+ * app/cmssy-edit/[[...path]]/page.tsx
115
115
  * export const dynamic = "force-dynamic";
116
116
  * export default createCmssyEditPage(cmssy, blocks, { editor: Editor });
117
117
  */
118
- declare const CMSSY_EDIT_PATH_PREFIX = "/__cmssy/edit";
118
+ declare const CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
119
119
  /**
120
120
  * Rewrite VERIFIED editor requests (`cmssyEdit=1` + a `cmssySecret` matching
121
121
  * the site's draft secret, CMS-948) onto the dedicated dynamic edit route, so
@@ -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
@@ -101,7 +101,7 @@ interface CatchAllProps {
101
101
  declare function createCmssyPage(config: CmssyNextConfig, blocks: BlockDefinition[], options?: CreateCmssyPageOptions): ({ params, searchParams, }: CatchAllProps) => Promise<react_jsx_runtime.JSX.Element>;
102
102
  /**
103
103
  * Editor page for the middleware-rewritten edit route
104
- * (`app/__cmssy/edit/[[...path]]/page.tsx`, `dynamic = "force-dynamic"`).
104
+ * (`app/cmssy-edit/[[...path]]/page.tsx`, `dynamic = "force-dynamic"`).
105
105
  * Re-verifies the `cmssyEdit` + `cmssySecret` pair itself, so a direct hit
106
106
  * on the route path (bypassing the middleware) cannot mount the editor.
107
107
  */
@@ -111,11 +111,11 @@ declare function createCmssyEditPage(config: CmssyNextConfig, blocks: BlockDefin
111
111
  * Where the middleware rewrites editor-preview traffic. Mount the matching
112
112
  * dynamic route in the consumer app:
113
113
  *
114
- * app/__cmssy/edit/[[...path]]/page.tsx
114
+ * app/cmssy-edit/[[...path]]/page.tsx
115
115
  * export const dynamic = "force-dynamic";
116
116
  * export default createCmssyEditPage(cmssy, blocks, { editor: Editor });
117
117
  */
118
- declare const CMSSY_EDIT_PATH_PREFIX = "/__cmssy/edit";
118
+ declare const CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
119
119
  /**
120
120
  * Rewrite VERIFIED editor requests (`cmssyEdit=1` + a `cmssySecret` matching
121
121
  * the site's draft secret, CMS-948) onto the dedicated dynamic edit route, so
@@ -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';
@@ -397,7 +397,7 @@ function resolveBridgeOrigin(editorOrigin) {
397
397
  }
398
398
  return origins.length === 1 ? origins[0] : origins;
399
399
  }
400
- var CMSSY_EDIT_PATH_PREFIX = "/__cmssy/edit";
400
+ var CMSSY_EDIT_PATH_PREFIX = "/cmssy-edit";
401
401
  async function cmssyEditRewrite(request2, config) {
402
402
  const { pathname, searchParams } = request2.nextUrl;
403
403
  if (pathname.startsWith(CMSSY_EDIT_PATH_PREFIX)) return null;
@@ -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.0",
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.0",
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.0"
58
+ "@cmssy/react": "4.1.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "jose": "^6.2.3",