busabase-cms-sdk 0.1.4 → 0.1.5

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.
@@ -126,38 +126,73 @@ interface CmsPageMetadataOptions {
126
126
  lang: string;
127
127
  type: "website" | "article";
128
128
  }
129
+ /**
130
+ * All the locale resolver actually reads off a Page: its canonical path and its locale. Kept
131
+ * deliberately narrower than `PageVO` so an app whose own Page VO omits SDK fields it does not
132
+ * store (Buda drops `hero`/`features`/`faqs`) still flows through these helpers with its own
133
+ * type intact, instead of being silently widened back to `PageVO`.
134
+ */
135
+ type CmsPageIdentity = Pick<PageVO, "path" | "locale">;
136
+ /**
137
+ * A Page resolved for one request, carrying the locale that actually supplied the content.
138
+ * `isLocaleFallback` is what a route reads to decide whether to render a "not translated yet,
139
+ * showing English" notice. Structurally mirrors `ResolvedCmsPostPage` on the Post side.
140
+ */
141
+ interface ResolvedCmsPage<TPage extends CmsPageIdentity = PageVO> {
142
+ page: TPage;
143
+ requestedLocale: string;
144
+ contentLocale: string;
145
+ isLocaleFallback: boolean;
146
+ }
129
147
  /** The slice of a `CmsIntegration` the Page helpers depend on. */
130
- interface CmsPageHelpersIntegration {
148
+ interface CmsPageHelpersIntegration<TPage extends CmsPageIdentity = PageVO> {
131
149
  buildCmsPath: (locale: string, path: string | readonly string[]) => string | null;
132
150
  parseCmsPath: (path: string) => CmsCanonicalPath | null;
133
151
  isCmsContentForLocale: (item: {
134
152
  locale: string;
135
153
  path: string;
136
154
  }, locale: string) => boolean;
137
- getBusabaseLandingPageByPathOrFallback: (path: string) => Promise<PageVO | null>;
155
+ getBusabaseLandingPageByPathOrFallback: (path: string) => Promise<TPage | null>;
138
156
  /** Status-aware variant, for callers that turn a missing page into a 404. */
139
- readBusabaseLandingPageByPath: (path: string) => Promise<CmsRead<PageVO | null>>;
157
+ readBusabaseLandingPageByPath: (path: string) => Promise<CmsRead<TPage | null>>;
140
158
  }
141
- interface CmsPageHelpersOptions<TMetadata> {
142
- integration: CmsPageHelpersIntegration;
159
+ interface CmsPageHelpersOptions<TMetadata, TPage extends CmsPageIdentity = PageVO> {
160
+ integration: CmsPageHelpersIntegration<TPage>;
143
161
  /**
144
162
  * The app's own metadata helper. Injected and typed structurally so the SDK does not depend
145
163
  * on any app's site config.
146
164
  */
147
165
  generatePageMetadata: (options: CmsPageMetadataOptions) => TMetadata;
148
166
  }
149
- interface CmsPageHelpers<TMetadata> {
150
- getCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<PageVO | null>;
167
+ interface CmsPageHelpers<TMetadata, TPage extends CmsPageIdentity = PageVO> {
168
+ /**
169
+ * STRICT per-locale lookup: no English fallback, `null` when this exact locale has no Page.
170
+ * Callers that run their own cross-locale cascade (Buda's use-case resolver cascades CMS →
171
+ * bundled ICP → editorial before falling back) must use this — handing them the fallback
172
+ * variant makes every locale look translated, which silently suppresses the "showing English"
173
+ * notice and mislabels the content locale.
174
+ */
175
+ getCmsPageForLocale: (locale: string, path: string | readonly string[]) => Promise<TPage | null>;
176
+ /**
177
+ * The resolved Page only. Falls back to English when the requested locale has no translation,
178
+ * so an untranslated Page renders in English instead of 404ing — the same rule the Post
179
+ * resolver applies. Use `resolveCmsPageForRequest` when the route needs to know it fell back.
180
+ */
181
+ getCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<TPage | null>;
182
+ /** Same resolution as `getCmsPageForRequest`, plus which locale supplied the content. */
183
+ resolveCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<ResolvedCmsPage<TPage> | null>;
151
184
  /**
152
185
  * Status-aware variant. A CMS Page has no local-MDX equivalent, so a catch-all
153
186
  * route cannot otherwise tell "no page at this path" (a correct, cacheable
154
187
  * 404) from "the CMS is unreachable" (a 404 that would be cached as though the
155
188
  * page had been deleted).
156
189
  */
157
- readCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<CmsRead<PageVO | null>>;
190
+ readCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<CmsRead<TPage | null>>;
191
+ /** Status-aware variant of `resolveCmsPageForRequest`. */
192
+ readResolvedCmsPageForRequest: (lang: string, path: string | readonly string[]) => Promise<CmsRead<ResolvedCmsPage<TPage> | null>>;
158
193
  generateCmsPageMetadata: (page: PageVO, lang: string) => TMetadata | Record<string, never>;
159
194
  }
160
- declare const createCmsPageHelpers: <TMetadata>({ integration, generatePageMetadata }: CmsPageHelpersOptions<TMetadata>) => CmsPageHelpers<TMetadata>;
195
+ declare const createCmsPageHelpers: <TMetadata, TPage extends CmsPageIdentity = PageVO>({ integration, generatePageMetadata }: CmsPageHelpersOptions<TMetadata, TPage>) => CmsPageHelpers<TMetadata, TPage>;
161
196
  //#endregion
162
197
  //#region src/integration/posts.d.ts
163
198
  interface CmsPostReads {
@@ -267,4 +302,4 @@ interface CmsIntegration extends CmsPostReads, CmsPageReads, CmsTaxonomyReads {
267
302
  }
268
303
  declare const createCmsIntegration: (config: CmsIntegrationConfig) => CmsIntegration;
269
304
  //#endregion
270
- export { type BlogCardContent, type CmsClientProvider, CmsIntegration, type CmsIntegrationBaseSlugs, type CmsIntegrationCacheConfig, type CmsIntegrationCacheTags, type CmsIntegrationConfig, type CmsPageHelpers, type CmsPageHelpersIntegration, type CmsPageHelpersOptions, type CmsPageMetadataOptions, type CmsPageReads, type CmsPostReads, type CmsPostResolver, type CmsPostResolverDependencies, type CmsPostResolverIntegration, type CmsPostResolverOptions, type CmsTaxonomyReads, DEFAULT_CMS_REVALIDATE_SECONDS, type LocalPostSourceLike, type ResolvedCmsConfig, type ResolvedCmsPostPage, buildCmsCacheKeyPrefix, createCmsClientProvider, createCmsIntegration, createCmsPageHelpers, createCmsPageReads, createCmsPostReads, createCmsPostResolver, createCmsTaxonomyReads, mergeBlogCardsByPath, readCmsEnvConfig, resolveCmsCacheTags };
305
+ export { type BlogCardContent, type CmsClientProvider, CmsIntegration, type CmsIntegrationBaseSlugs, type CmsIntegrationCacheConfig, type CmsIntegrationCacheTags, type CmsIntegrationConfig, type CmsPageHelpers, type CmsPageHelpersIntegration, type CmsPageHelpersOptions, type CmsPageIdentity, type CmsPageMetadataOptions, type CmsPageReads, type CmsPostReads, type CmsPostResolver, type CmsPostResolverDependencies, type CmsPostResolverIntegration, type CmsPostResolverOptions, type CmsTaxonomyReads, DEFAULT_CMS_REVALIDATE_SECONDS, type LocalPostSourceLike, type ResolvedCmsConfig, type ResolvedCmsPage, type ResolvedCmsPostPage, buildCmsCacheKeyPrefix, createCmsClientProvider, createCmsIntegration, createCmsPageHelpers, createCmsPageReads, createCmsPostReads, createCmsPostResolver, createCmsTaxonomyReads, mergeBlogCardsByPath, readCmsEnvConfig, resolveCmsCacheTags };
@@ -131,16 +131,21 @@ const createCmsPageReads = ({ getCms, requireCms }, appLabel) => ({
131
131
  return readCmsOrFallback(cms ? () => cms.pages.getByPath(path) : null, null, `get ${appLabel} landing page ${path}`);
132
132
  }
133
133
  });
134
+ /**
135
+ * The locale every other locale falls back to when it has no translation of a Page.
136
+ * Mirrors the Post resolver in ./posts.ts — see `resolvePostPageWithDependencies`.
137
+ */
138
+ const FALLBACK_LOCALE = "en";
134
139
  const createCmsPageHelpers = ({ integration, generatePageMetadata }) => {
135
140
  const { buildCmsPath, getBusabaseLandingPageByPathOrFallback, isCmsContentForLocale, parseCmsPath, readBusabaseLandingPageByPath } = integration;
136
- const getCmsPageForRequest = async (lang, path) => {
137
- const canonicalPath = buildCmsPath(lang, path);
141
+ const getForLocale = async (locale, path) => {
142
+ const canonicalPath = buildCmsPath(locale, path);
138
143
  if (!canonicalPath) return null;
139
144
  const page = await getBusabaseLandingPageByPathOrFallback(canonicalPath);
140
- return page && isCmsContentForLocale(page, lang) ? page : null;
145
+ return page && isCmsContentForLocale(page, locale) ? page : null;
141
146
  };
142
- const readCmsPageForRequest = async (lang, path) => {
143
- const canonicalPath = buildCmsPath(lang, path);
147
+ const readForLocale = async (locale, path) => {
148
+ const canonicalPath = buildCmsPath(locale, path);
144
149
  if (!canonicalPath) return {
145
150
  status: "ok",
146
151
  data: null
@@ -150,23 +155,71 @@ const createCmsPageHelpers = ({ integration, generatePageMetadata }) => {
150
155
  const page = read.data;
151
156
  return {
152
157
  status: "ok",
153
- data: page && isCmsContentForLocale(page, lang) ? page : null
158
+ data: page && isCmsContentForLocale(page, locale) ? page : null
159
+ };
160
+ };
161
+ const resolved = (page, requestedLocale, contentLocale) => ({
162
+ page,
163
+ requestedLocale,
164
+ contentLocale,
165
+ isLocaleFallback: contentLocale !== requestedLocale
166
+ });
167
+ const resolveCmsPageForRequest = async (lang, path) => {
168
+ if (!buildCmsPath(lang, path)) return null;
169
+ const requested = await getForLocale(lang, path);
170
+ if (requested) return resolved(requested, lang, lang);
171
+ if (lang === FALLBACK_LOCALE) return null;
172
+ const fallback = await getForLocale(FALLBACK_LOCALE, path);
173
+ return fallback ? resolved(fallback, lang, FALLBACK_LOCALE) : null;
174
+ };
175
+ const getCmsPageForRequest = async (lang, path) => (await resolveCmsPageForRequest(lang, path))?.page ?? null;
176
+ const readResolvedCmsPageForRequest = async (lang, path) => {
177
+ if (!buildCmsPath(lang, path)) return {
178
+ status: "ok",
179
+ data: null
180
+ };
181
+ const requested = await readForLocale(lang, path);
182
+ if (requested.status !== "ok") return requested;
183
+ if (requested.data) return {
184
+ status: "ok",
185
+ data: resolved(requested.data, lang, lang)
186
+ };
187
+ if (lang === FALLBACK_LOCALE) return {
188
+ status: "ok",
189
+ data: null
190
+ };
191
+ const fallback = await readForLocale(FALLBACK_LOCALE, path);
192
+ if (fallback.status !== "ok") return fallback;
193
+ return {
194
+ status: "ok",
195
+ data: fallback.data ? resolved(fallback.data, lang, FALLBACK_LOCALE) : null
154
196
  };
155
197
  };
198
+ const readCmsPageForRequest = async (lang, path) => {
199
+ const read = await readResolvedCmsPageForRequest(lang, path);
200
+ return read.status === "ok" ? {
201
+ status: "ok",
202
+ data: read.data?.page ?? null
203
+ } : read;
204
+ };
156
205
  const generateCmsPageMetadata = (page, lang) => {
157
206
  const parsed = parseCmsPath(page.path);
158
- if (!parsed || parsed.locale !== lang) return {};
207
+ if (!parsed) return {};
208
+ const contentLocale = isCmsContentForLocale(page, lang) ? lang : parsed.locale;
159
209
  return generatePageMetadata({
160
210
  title: page.seoTitle ?? page.title,
161
211
  description: page.seoDescription ?? "",
162
212
  path: parsed.pathWithoutLocale,
163
- lang,
213
+ lang: contentLocale,
164
214
  type: "website"
165
215
  });
166
216
  };
167
217
  return {
218
+ getCmsPageForLocale: getForLocale,
168
219
  getCmsPageForRequest,
220
+ resolveCmsPageForRequest,
169
221
  readCmsPageForRequest,
222
+ readResolvedCmsPageForRequest,
170
223
  generateCmsPageMetadata
171
224
  };
172
225
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-cms-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Typed Posts, Pages, Categories, and Tags for Busabase CMS, Next.js, and Fumadocs.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/busabase/busabase/tree/main/packages/busabase-cms-sdk",
@@ -47,7 +47,7 @@
47
47
  "sanitize-html": "^2.17.6",
48
48
  "server-only": "^0.0.1",
49
49
  "zod": "^4.3.6",
50
- "busabase-sdk": "^0.50.0"
50
+ "busabase-sdk": "^0.53.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "next": ">=15 <17",