@cmssy/core 9.9.0 → 10.0.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.
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ // src/data/http.ts
4
+ var CmssyRequestError = class extends Error {
5
+ status;
6
+ constructor(message, status) {
7
+ super(message);
8
+ this.name = "CmssyRequestError";
9
+ this.status = status;
10
+ }
11
+ };
12
+ var DEFAULT_RETRY_STATUSES = [429, 503];
13
+ function retryAfterMs(response) {
14
+ const raw = response.headers?.get("retry-after");
15
+ if (!raw) return null;
16
+ const seconds = Number(raw);
17
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
18
+ const date = Date.parse(raw);
19
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
20
+ return null;
21
+ }
22
+ function sleep(ms, signal) {
23
+ return new Promise((resolve, reject) => {
24
+ if (signal?.aborted) {
25
+ reject(new Error("cmssy: request aborted"));
26
+ return;
27
+ }
28
+ const timer = setTimeout(() => {
29
+ signal?.removeEventListener("abort", onAbort);
30
+ resolve();
31
+ }, ms);
32
+ function onAbort() {
33
+ clearTimeout(timer);
34
+ reject(new Error("cmssy: request aborted"));
35
+ }
36
+ signal?.addEventListener("abort", onAbort, { once: true });
37
+ });
38
+ }
39
+ async function fetchWithRetry(doFetch, url, init, retry) {
40
+ if (retry === false || retry === void 0) {
41
+ return doFetch(url, init);
42
+ }
43
+ const maxRetries = retry.maxRetries ?? 3;
44
+ const baseDelayMs = retry.baseDelayMs ?? 300;
45
+ const maxDelayMs = retry.maxDelayMs ?? 3e3;
46
+ const retryStatuses = retry.retryStatuses ?? DEFAULT_RETRY_STATUSES;
47
+ let response = await doFetch(url, init);
48
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
49
+ if (response.ok || !retryStatuses.includes(response.status)) {
50
+ return response;
51
+ }
52
+ const backoff = baseDelayMs * 2 ** attempt;
53
+ const wait = Math.min(retryAfterMs(response) ?? backoff, maxDelayMs);
54
+ await sleep(wait, init.signal);
55
+ response = await doFetch(url, init);
56
+ }
57
+ return response;
58
+ }
59
+ async function postGraphql(url, query, variables, options) {
60
+ const doFetch = options.fetch ?? globalThis.fetch;
61
+ if (typeof doFetch !== "function") {
62
+ throw new Error(
63
+ "cmssy: no fetch implementation available - pass options.fetch"
64
+ );
65
+ }
66
+ const response = await fetchWithRetry(
67
+ doFetch,
68
+ url,
69
+ {
70
+ method: "POST",
71
+ headers: { "content-type": "application/json", ...options.headers },
72
+ body: JSON.stringify({ query, variables }),
73
+ signal: options.signal
74
+ },
75
+ options.retry
76
+ );
77
+ if (!response.ok) {
78
+ let detail = "";
79
+ try {
80
+ const body = await response.json();
81
+ if (body.errors && body.errors.length > 0) {
82
+ detail = ` - ${body.errors.map((error) => error.message ?? "GraphQL error").join("; ")}`;
83
+ }
84
+ } catch {
85
+ detail = "";
86
+ }
87
+ throw new CmssyRequestError(
88
+ `cmssy: ${options.label} failed (${response.status})${detail}`,
89
+ response.status
90
+ );
91
+ }
92
+ let json;
93
+ try {
94
+ json = await response.json();
95
+ } catch {
96
+ throw new Error(`cmssy: invalid JSON response from the ${options.label}`);
97
+ }
98
+ if (json.errors && json.errors.length > 0) {
99
+ const message = json.errors.map((error) => error.message ?? "GraphQL error").join("; ");
100
+ throw new Error(`cmssy: ${options.label} error - ${message}`);
101
+ }
102
+ return json.data;
103
+ }
104
+
105
+ // src/content/content-client.ts
106
+ var DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
107
+ function resolveApiUrl(apiUrl) {
108
+ const explicit = apiUrl?.trim();
109
+ if (explicit) return explicit;
110
+ const env = globalThis.process?.env;
111
+ const fromEnv = env?.CMSSY_API_URL?.trim() ?? "";
112
+ return fromEnv.length > 0 ? fromEnv : DEFAULT_CMSSY_API_URL;
113
+ }
114
+ function resolvePublicUrl(config) {
115
+ const base = resolveApiUrl(config.apiUrl).replace(/\/graphql\/?$/, "");
116
+ return `${base}/public/${config.org}/${config.workspaceSlug}/graphql`;
117
+ }
118
+
119
+ // src/data/graphql-request.ts
120
+ async function graphqlRequest(config, query, variables, options = {}, label = "request") {
121
+ const url = options.public ? resolvePublicUrl(config) : resolveApiUrl(config.apiUrl);
122
+ return postGraphql(url, query, variables, {
123
+ fetch: options.fetch,
124
+ signal: options.signal,
125
+ headers: options.headers,
126
+ retry: options.retry,
127
+ label
128
+ });
129
+ }
130
+
131
+ // src/data/queries.ts
132
+ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
133
+ public {
134
+ siteConfig(workspaceSlug: $workspaceSlug) {
135
+ id
136
+ workspaceId
137
+ siteName
138
+ defaultLanguage
139
+ enabledLanguages
140
+ enabledFeatures
141
+ notFoundPageId
142
+ previewUrl
143
+ branding {
144
+ brandName
145
+ logoUrl
146
+ faviconUrl
147
+ ogImageUrl
148
+ }
149
+ }
150
+ }
151
+ }`;
152
+
153
+ // src/data/site-locales.ts
154
+ var TTL_MS = 6e4;
155
+ var MAX_ENTRIES = 64;
156
+ var cache = /* @__PURE__ */ new Map();
157
+ function localesFromSiteConfig(siteConfig) {
158
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
159
+ const enabled = siteConfig?.enabledLanguages ?? [];
160
+ return {
161
+ defaultLocale,
162
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
163
+ };
164
+ }
165
+ async function resolveSiteLocales(config, options) {
166
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
167
+ const cached = cache.get(key);
168
+ if (cached && cached.expires > Date.now()) return cached.value;
169
+ cache.delete(key);
170
+ let value;
171
+ try {
172
+ const data = await graphqlRequest(
173
+ config,
174
+ SITE_CONFIG_QUERY,
175
+ { workspaceSlug: config.workspaceSlug },
176
+ { ...options, public: true, retry: options?.retry ?? {} },
177
+ "site config"
178
+ );
179
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
180
+ } catch {
181
+ value = { defaultLocale: "en", locales: ["en"] };
182
+ }
183
+ if (cache.size >= MAX_ENTRIES) cache.clear();
184
+ cache.set(key, { value, expires: Date.now() + TTL_MS });
185
+ return value;
186
+ }
187
+ function splitLocaleFromPath(path, siteLocales) {
188
+ const first = path?.[0];
189
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
190
+ return { locale: first, path: path.slice(1) };
191
+ }
192
+ return { locale: siteLocales.defaultLocale, path };
193
+ }
194
+
195
+ // src/locale.ts
196
+ var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
197
+ async function localeForPathname(config, pathname) {
198
+ const siteLocales = await resolveSiteLocales(config);
199
+ const segments = pathname.split("/").filter(Boolean);
200
+ return splitLocaleFromPath(segments, siteLocales).locale;
201
+ }
202
+ async function splitCmssyLocale(config, path) {
203
+ const siteLocales = await resolveSiteLocales(config);
204
+ return splitLocaleFromPath(path, siteLocales);
205
+ }
206
+ async function localeForPath(config, path) {
207
+ const siteLocales = await resolveSiteLocales(config);
208
+ const segments = Array.isArray(path) ? path.filter(Boolean) : path.split("/").filter(Boolean);
209
+ return splitLocaleFromPath(segments, siteLocales).locale;
210
+ }
211
+
212
+ // src/data/localize-href.ts
213
+ var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
214
+ function isExternalHref(href) {
215
+ const value = href.trim();
216
+ if (!value) return true;
217
+ if (value.startsWith("#")) return true;
218
+ return PROTOCOL_OR_RELATIVE.test(value);
219
+ }
220
+ function stripLeadingLocale(path, locale) {
221
+ const segments = path.split("/");
222
+ const first = segments[1];
223
+ if (first && first !== locale.default && locale.enabled.includes(first)) {
224
+ segments.splice(1, 1);
225
+ const rest = segments.join("/");
226
+ return rest === "" ? "/" : rest;
227
+ }
228
+ return path;
229
+ }
230
+ function addLocalePrefix(path, target, locale) {
231
+ if (target === locale.default) return path;
232
+ if (path === "/") return `/${target}`;
233
+ return `/${target}${path}`;
234
+ }
235
+ function localizeHref(href, locale) {
236
+ const value = href.trim();
237
+ if (isExternalHref(value)) return href;
238
+ const boundary = value.search(/[?#]/);
239
+ const path = boundary === -1 ? value : value.slice(0, boundary);
240
+ const suffix = boundary === -1 ? "" : value.slice(boundary);
241
+ if (!path.startsWith("/")) return href;
242
+ const bare = stripLeadingLocale(path, locale);
243
+ return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
244
+ }
245
+ function buildLocaleSwitchHref(target, pathname, locale) {
246
+ const path = pathname && pathname.startsWith("/") ? pathname : "/";
247
+ const bare = stripLeadingLocale(path, locale);
248
+ return addLocalePrefix(bare, target, locale);
249
+ }
250
+ var ANCHOR_HREF = /(<a\b(?:"[^"]*"|'[^']*'|[^>])*?\shref=)(["'])(.*?)\2/gi;
251
+ function localizeHtmlLinks(html, locale) {
252
+ return html.replace(
253
+ ANCHOR_HREF,
254
+ (_match, prefix, quote, url) => `${prefix}${quote}${localizeHref(url, locale)}${quote}`
255
+ );
256
+ }
257
+
258
+ // src/seo-paths.ts
259
+ function normalizeSlug(slug) {
260
+ if (slug === "/" || slug === "") return "/";
261
+ return slug.startsWith("/") ? slug : `/${slug}`;
262
+ }
263
+ function localizedPath(slug, locale, defaultLocale) {
264
+ const normalized = normalizeSlug(slug);
265
+ const base = normalized === "/" ? "" : normalized;
266
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
267
+ }
268
+
269
+ exports.CMSSY_LOCALE_HEADER = CMSSY_LOCALE_HEADER;
270
+ exports.buildLocaleSwitchHref = buildLocaleSwitchHref;
271
+ exports.localeForPath = localeForPath;
272
+ exports.localeForPathname = localeForPathname;
273
+ exports.localesFromSiteConfig = localesFromSiteConfig;
274
+ exports.localizeHref = localizeHref;
275
+ exports.localizeHtmlLinks = localizeHtmlLinks;
276
+ exports.localizedPath = localizedPath;
277
+ exports.resolveSiteLocales = resolveSiteLocales;
278
+ exports.splitCmssyLocale = splitCmssyLocale;
279
+ exports.splitLocaleFromPath = splitLocaleFromPath;
@@ -0,0 +1,56 @@
1
+ import { CmssySiteLocales, CmssyClientConfig, CmssyLocaleContext } from '@cmssy/types';
2
+ export { CmssySiteLocales } from '@cmssy/types';
3
+ import { G as GraphqlRequestOptions } from '../graphql-request-NQxSMt6v.cjs';
4
+ import '../content-client-D0EdiqbQ.cjs';
5
+
6
+ /**
7
+ * Maps a workspace site config to its locale set. The single place that
8
+ * decides the default and enabled languages - the router and the SEO helpers
9
+ * must agree, so both go through here.
10
+ */
11
+ declare function localesFromSiteConfig(siteConfig: {
12
+ defaultLanguage?: string | null;
13
+ enabledLanguages?: string[];
14
+ } | null): CmssySiteLocales;
15
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
16
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
17
+ locale: string;
18
+ path: string[] | undefined;
19
+ };
20
+
21
+ /**
22
+ * Rewrites an internal href to carry the active locale as a path prefix.
23
+ * External hrefs, fragments and relative paths are returned untouched. Already
24
+ * prefixed hrefs are normalized so the prefix never doubles.
25
+ */
26
+ declare function localizeHref(href: string, locale: CmssyLocaleContext): string;
27
+ /**
28
+ * Builds the href that switches the current `pathname` to `target` locale,
29
+ * preserving the rest of the path. Used by a language switcher.
30
+ */
31
+ declare function buildLocaleSwitchHref(target: string, pathname: string, locale: CmssyLocaleContext): string;
32
+ /**
33
+ * Rewrites every `<a href>` inside an HTML string with {@link localizeHref}.
34
+ * For rich-text content stored in CMS blocks where links are raw markup.
35
+ */
36
+ declare function localizeHtmlLinks(html: string, locale: CmssyLocaleContext): string;
37
+
38
+ declare const CMSSY_LOCALE_HEADER = "x-cmssy-locale";
39
+ declare function localeForPathname(config: CmssyClientConfig, pathname: string): Promise<string>;
40
+ declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | undefined): Promise<{
41
+ locale: string;
42
+ path: string[] | undefined;
43
+ }>;
44
+ /**
45
+ * Resolve the locale a path asks for. The prefix IS the language, so a routed
46
+ * path is all it takes - no request, no headers, static-safe.
47
+ */
48
+ declare function localeForPath(config: CmssyClientConfig, path: string | string[]): Promise<string>;
49
+
50
+ /**
51
+ * Maps a page slug to its path for a locale: the default locale gets no
52
+ * prefix, others get `/${locale}`. The homepage ("/") stays "/" (or "/${locale}").
53
+ */
54
+ declare function localizedPath(slug: string, locale: string, defaultLocale: string): string;
55
+
56
+ export { CMSSY_LOCALE_HEADER, buildLocaleSwitchHref, localeForPath, localeForPathname, localesFromSiteConfig, localizeHref, localizeHtmlLinks, localizedPath, resolveSiteLocales, splitCmssyLocale, splitLocaleFromPath };
@@ -0,0 +1,56 @@
1
+ import { CmssySiteLocales, CmssyClientConfig, CmssyLocaleContext } from '@cmssy/types';
2
+ export { CmssySiteLocales } from '@cmssy/types';
3
+ import { G as GraphqlRequestOptions } from '../graphql-request-CDh6IRok.js';
4
+ import '../content-client-D0EdiqbQ.js';
5
+
6
+ /**
7
+ * Maps a workspace site config to its locale set. The single place that
8
+ * decides the default and enabled languages - the router and the SEO helpers
9
+ * must agree, so both go through here.
10
+ */
11
+ declare function localesFromSiteConfig(siteConfig: {
12
+ defaultLanguage?: string | null;
13
+ enabledLanguages?: string[];
14
+ } | null): CmssySiteLocales;
15
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
16
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
17
+ locale: string;
18
+ path: string[] | undefined;
19
+ };
20
+
21
+ /**
22
+ * Rewrites an internal href to carry the active locale as a path prefix.
23
+ * External hrefs, fragments and relative paths are returned untouched. Already
24
+ * prefixed hrefs are normalized so the prefix never doubles.
25
+ */
26
+ declare function localizeHref(href: string, locale: CmssyLocaleContext): string;
27
+ /**
28
+ * Builds the href that switches the current `pathname` to `target` locale,
29
+ * preserving the rest of the path. Used by a language switcher.
30
+ */
31
+ declare function buildLocaleSwitchHref(target: string, pathname: string, locale: CmssyLocaleContext): string;
32
+ /**
33
+ * Rewrites every `<a href>` inside an HTML string with {@link localizeHref}.
34
+ * For rich-text content stored in CMS blocks where links are raw markup.
35
+ */
36
+ declare function localizeHtmlLinks(html: string, locale: CmssyLocaleContext): string;
37
+
38
+ declare const CMSSY_LOCALE_HEADER = "x-cmssy-locale";
39
+ declare function localeForPathname(config: CmssyClientConfig, pathname: string): Promise<string>;
40
+ declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | undefined): Promise<{
41
+ locale: string;
42
+ path: string[] | undefined;
43
+ }>;
44
+ /**
45
+ * Resolve the locale a path asks for. The prefix IS the language, so a routed
46
+ * path is all it takes - no request, no headers, static-safe.
47
+ */
48
+ declare function localeForPath(config: CmssyClientConfig, path: string | string[]): Promise<string>;
49
+
50
+ /**
51
+ * Maps a page slug to its path for a locale: the default locale gets no
52
+ * prefix, others get `/${locale}`. The homepage ("/") stays "/" (or "/${locale}").
53
+ */
54
+ declare function localizedPath(slug: string, locale: string, defaultLocale: string): string;
55
+
56
+ export { CMSSY_LOCALE_HEADER, buildLocaleSwitchHref, localeForPath, localeForPathname, localesFromSiteConfig, localizeHref, localizeHtmlLinks, localizedPath, resolveSiteLocales, splitCmssyLocale, splitLocaleFromPath };
@@ -0,0 +1,267 @@
1
+ // src/data/http.ts
2
+ var CmssyRequestError = class extends Error {
3
+ status;
4
+ constructor(message, status) {
5
+ super(message);
6
+ this.name = "CmssyRequestError";
7
+ this.status = status;
8
+ }
9
+ };
10
+ var DEFAULT_RETRY_STATUSES = [429, 503];
11
+ function retryAfterMs(response) {
12
+ const raw = response.headers?.get("retry-after");
13
+ if (!raw) return null;
14
+ const seconds = Number(raw);
15
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
16
+ const date = Date.parse(raw);
17
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
18
+ return null;
19
+ }
20
+ function sleep(ms, signal) {
21
+ return new Promise((resolve, reject) => {
22
+ if (signal?.aborted) {
23
+ reject(new Error("cmssy: request aborted"));
24
+ return;
25
+ }
26
+ const timer = setTimeout(() => {
27
+ signal?.removeEventListener("abort", onAbort);
28
+ resolve();
29
+ }, ms);
30
+ function onAbort() {
31
+ clearTimeout(timer);
32
+ reject(new Error("cmssy: request aborted"));
33
+ }
34
+ signal?.addEventListener("abort", onAbort, { once: true });
35
+ });
36
+ }
37
+ async function fetchWithRetry(doFetch, url, init, retry) {
38
+ if (retry === false || retry === void 0) {
39
+ return doFetch(url, init);
40
+ }
41
+ const maxRetries = retry.maxRetries ?? 3;
42
+ const baseDelayMs = retry.baseDelayMs ?? 300;
43
+ const maxDelayMs = retry.maxDelayMs ?? 3e3;
44
+ const retryStatuses = retry.retryStatuses ?? DEFAULT_RETRY_STATUSES;
45
+ let response = await doFetch(url, init);
46
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
47
+ if (response.ok || !retryStatuses.includes(response.status)) {
48
+ return response;
49
+ }
50
+ const backoff = baseDelayMs * 2 ** attempt;
51
+ const wait = Math.min(retryAfterMs(response) ?? backoff, maxDelayMs);
52
+ await sleep(wait, init.signal);
53
+ response = await doFetch(url, init);
54
+ }
55
+ return response;
56
+ }
57
+ async function postGraphql(url, query, variables, options) {
58
+ const doFetch = options.fetch ?? globalThis.fetch;
59
+ if (typeof doFetch !== "function") {
60
+ throw new Error(
61
+ "cmssy: no fetch implementation available - pass options.fetch"
62
+ );
63
+ }
64
+ const response = await fetchWithRetry(
65
+ doFetch,
66
+ url,
67
+ {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json", ...options.headers },
70
+ body: JSON.stringify({ query, variables }),
71
+ signal: options.signal
72
+ },
73
+ options.retry
74
+ );
75
+ if (!response.ok) {
76
+ let detail = "";
77
+ try {
78
+ const body = await response.json();
79
+ if (body.errors && body.errors.length > 0) {
80
+ detail = ` - ${body.errors.map((error) => error.message ?? "GraphQL error").join("; ")}`;
81
+ }
82
+ } catch {
83
+ detail = "";
84
+ }
85
+ throw new CmssyRequestError(
86
+ `cmssy: ${options.label} failed (${response.status})${detail}`,
87
+ response.status
88
+ );
89
+ }
90
+ let json;
91
+ try {
92
+ json = await response.json();
93
+ } catch {
94
+ throw new Error(`cmssy: invalid JSON response from the ${options.label}`);
95
+ }
96
+ if (json.errors && json.errors.length > 0) {
97
+ const message = json.errors.map((error) => error.message ?? "GraphQL error").join("; ");
98
+ throw new Error(`cmssy: ${options.label} error - ${message}`);
99
+ }
100
+ return json.data;
101
+ }
102
+
103
+ // src/content/content-client.ts
104
+ var DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
105
+ function resolveApiUrl(apiUrl) {
106
+ const explicit = apiUrl?.trim();
107
+ if (explicit) return explicit;
108
+ const env = globalThis.process?.env;
109
+ const fromEnv = env?.CMSSY_API_URL?.trim() ?? "";
110
+ return fromEnv.length > 0 ? fromEnv : DEFAULT_CMSSY_API_URL;
111
+ }
112
+ function resolvePublicUrl(config) {
113
+ const base = resolveApiUrl(config.apiUrl).replace(/\/graphql\/?$/, "");
114
+ return `${base}/public/${config.org}/${config.workspaceSlug}/graphql`;
115
+ }
116
+
117
+ // src/data/graphql-request.ts
118
+ async function graphqlRequest(config, query, variables, options = {}, label = "request") {
119
+ const url = options.public ? resolvePublicUrl(config) : resolveApiUrl(config.apiUrl);
120
+ return postGraphql(url, query, variables, {
121
+ fetch: options.fetch,
122
+ signal: options.signal,
123
+ headers: options.headers,
124
+ retry: options.retry,
125
+ label
126
+ });
127
+ }
128
+
129
+ // src/data/queries.ts
130
+ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
131
+ public {
132
+ siteConfig(workspaceSlug: $workspaceSlug) {
133
+ id
134
+ workspaceId
135
+ siteName
136
+ defaultLanguage
137
+ enabledLanguages
138
+ enabledFeatures
139
+ notFoundPageId
140
+ previewUrl
141
+ branding {
142
+ brandName
143
+ logoUrl
144
+ faviconUrl
145
+ ogImageUrl
146
+ }
147
+ }
148
+ }
149
+ }`;
150
+
151
+ // src/data/site-locales.ts
152
+ var TTL_MS = 6e4;
153
+ var MAX_ENTRIES = 64;
154
+ var cache = /* @__PURE__ */ new Map();
155
+ function localesFromSiteConfig(siteConfig) {
156
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
157
+ const enabled = siteConfig?.enabledLanguages ?? [];
158
+ return {
159
+ defaultLocale,
160
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
161
+ };
162
+ }
163
+ async function resolveSiteLocales(config, options) {
164
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
165
+ const cached = cache.get(key);
166
+ if (cached && cached.expires > Date.now()) return cached.value;
167
+ cache.delete(key);
168
+ let value;
169
+ try {
170
+ const data = await graphqlRequest(
171
+ config,
172
+ SITE_CONFIG_QUERY,
173
+ { workspaceSlug: config.workspaceSlug },
174
+ { ...options, public: true, retry: options?.retry ?? {} },
175
+ "site config"
176
+ );
177
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
178
+ } catch {
179
+ value = { defaultLocale: "en", locales: ["en"] };
180
+ }
181
+ if (cache.size >= MAX_ENTRIES) cache.clear();
182
+ cache.set(key, { value, expires: Date.now() + TTL_MS });
183
+ return value;
184
+ }
185
+ function splitLocaleFromPath(path, siteLocales) {
186
+ const first = path?.[0];
187
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
188
+ return { locale: first, path: path.slice(1) };
189
+ }
190
+ return { locale: siteLocales.defaultLocale, path };
191
+ }
192
+
193
+ // src/locale.ts
194
+ var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
195
+ async function localeForPathname(config, pathname) {
196
+ const siteLocales = await resolveSiteLocales(config);
197
+ const segments = pathname.split("/").filter(Boolean);
198
+ return splitLocaleFromPath(segments, siteLocales).locale;
199
+ }
200
+ async function splitCmssyLocale(config, path) {
201
+ const siteLocales = await resolveSiteLocales(config);
202
+ return splitLocaleFromPath(path, siteLocales);
203
+ }
204
+ async function localeForPath(config, path) {
205
+ const siteLocales = await resolveSiteLocales(config);
206
+ const segments = Array.isArray(path) ? path.filter(Boolean) : path.split("/").filter(Boolean);
207
+ return splitLocaleFromPath(segments, siteLocales).locale;
208
+ }
209
+
210
+ // src/data/localize-href.ts
211
+ var PROTOCOL_OR_RELATIVE = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
212
+ function isExternalHref(href) {
213
+ const value = href.trim();
214
+ if (!value) return true;
215
+ if (value.startsWith("#")) return true;
216
+ return PROTOCOL_OR_RELATIVE.test(value);
217
+ }
218
+ function stripLeadingLocale(path, locale) {
219
+ const segments = path.split("/");
220
+ const first = segments[1];
221
+ if (first && first !== locale.default && locale.enabled.includes(first)) {
222
+ segments.splice(1, 1);
223
+ const rest = segments.join("/");
224
+ return rest === "" ? "/" : rest;
225
+ }
226
+ return path;
227
+ }
228
+ function addLocalePrefix(path, target, locale) {
229
+ if (target === locale.default) return path;
230
+ if (path === "/") return `/${target}`;
231
+ return `/${target}${path}`;
232
+ }
233
+ function localizeHref(href, locale) {
234
+ const value = href.trim();
235
+ if (isExternalHref(value)) return href;
236
+ const boundary = value.search(/[?#]/);
237
+ const path = boundary === -1 ? value : value.slice(0, boundary);
238
+ const suffix = boundary === -1 ? "" : value.slice(boundary);
239
+ if (!path.startsWith("/")) return href;
240
+ const bare = stripLeadingLocale(path, locale);
241
+ return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
242
+ }
243
+ function buildLocaleSwitchHref(target, pathname, locale) {
244
+ const path = pathname && pathname.startsWith("/") ? pathname : "/";
245
+ const bare = stripLeadingLocale(path, locale);
246
+ return addLocalePrefix(bare, target, locale);
247
+ }
248
+ var ANCHOR_HREF = /(<a\b(?:"[^"]*"|'[^']*'|[^>])*?\shref=)(["'])(.*?)\2/gi;
249
+ function localizeHtmlLinks(html, locale) {
250
+ return html.replace(
251
+ ANCHOR_HREF,
252
+ (_match, prefix, quote, url) => `${prefix}${quote}${localizeHref(url, locale)}${quote}`
253
+ );
254
+ }
255
+
256
+ // src/seo-paths.ts
257
+ function normalizeSlug(slug) {
258
+ if (slug === "/" || slug === "") return "/";
259
+ return slug.startsWith("/") ? slug : `/${slug}`;
260
+ }
261
+ function localizedPath(slug, locale, defaultLocale) {
262
+ const normalized = normalizeSlug(slug);
263
+ const base = normalized === "/" ? "" : normalized;
264
+ return locale === defaultLocale ? base || "/" : `/${locale}${base}`;
265
+ }
266
+
267
+ export { CMSSY_LOCALE_HEADER, buildLocaleSwitchHref, localeForPath, localeForPathname, localesFromSiteConfig, localizeHref, localizeHtmlLinks, localizedPath, resolveSiteLocales, splitCmssyLocale, splitLocaleFromPath };