busabase-cms-sdk 0.1.3
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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/content-BXgsWQjQ.js +1175 -0
- package/dist/fumadocs.d.ts +24 -0
- package/dist/fumadocs.js +188 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +14 -0
- package/dist/integration/index.d.ts +251 -0
- package/dist/integration/index.js +268 -0
- package/dist/next.d.ts +34 -0
- package/dist/next.js +107 -0
- package/dist/routing-C3lkVAZ8.js +91 -0
- package/dist/routing-VU56uuJ0.d.ts +321 -0
- package/dist/types-QKFW2jvT.d.ts +205 -0
- package/package.json +84 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { l as readCmsOrFallback, r as createCmsPathHelpers, s as normalizeCmsPath } from "../routing-C3lkVAZ8.js";
|
|
2
|
+
import { createCachedBusabaseCms } from "../next.js";
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { cache } from "react";
|
|
5
|
+
//#region src/integration/config.ts
|
|
6
|
+
/** Next data-cache `revalidate` used when an app does not override it. */
|
|
7
|
+
const DEFAULT_CMS_REVALIDATE_SECONDS = 300;
|
|
8
|
+
const trimValue = (value) => value?.trim() || null;
|
|
9
|
+
/**
|
|
10
|
+
* Pure `BUSABASE_CMS_*` env-var gate. All three of host/key/space are required: you genuinely
|
|
11
|
+
* cannot talk to a Busabase space without an API key, so a partial configuration is treated as
|
|
12
|
+
* "off" rather than as a guaranteed-failing read on every request.
|
|
13
|
+
*
|
|
14
|
+
* Re-reads the environment on every call — deliberately never memoized, unlike the client.
|
|
15
|
+
*/
|
|
16
|
+
const readCmsEnvConfig = (defaultBaseSlugs, env = process.env) => {
|
|
17
|
+
const baseUrl = trimValue(env.BUSABASE_CMS_BASE_URL);
|
|
18
|
+
const apiKey = trimValue(env.BUSABASE_CMS_API_KEY);
|
|
19
|
+
const spaceId = trimValue(env.BUSABASE_CMS_SPACE_ID);
|
|
20
|
+
if (!baseUrl || !apiKey || !spaceId) return null;
|
|
21
|
+
return {
|
|
22
|
+
baseUrl,
|
|
23
|
+
apiKey,
|
|
24
|
+
spaceId,
|
|
25
|
+
folderId: trimValue(env.BUSABASE_CMS_FOLDER_ID),
|
|
26
|
+
baseSlugs: {
|
|
27
|
+
posts: trimValue(env.BUSABASE_CMS_POSTS_BASE_SLUG) ?? defaultBaseSlugs.posts,
|
|
28
|
+
pages: trimValue(env.BUSABASE_CMS_PAGES_BASE_SLUG) ?? defaultBaseSlugs.pages,
|
|
29
|
+
categories: trimValue(env.BUSABASE_CMS_CATEGORIES_BASE_SLUG) ?? defaultBaseSlugs.categories,
|
|
30
|
+
tags: trimValue(env.BUSABASE_CMS_TAGS_BASE_SLUG) ?? defaultBaseSlugs.tags
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/integration/client.ts
|
|
36
|
+
/**
|
|
37
|
+
* Explicit Next data-cache key prefix. Two reasons it is never left to the SDK's default:
|
|
38
|
+
*
|
|
39
|
+
* 1. App identity — the SDK's own default prefix (see `resolveBusabaseCmsCacheKeyPrefix`) is
|
|
40
|
+
* derived purely from the CMS target (host / space / folder / profile / Base slugs) and
|
|
41
|
+
* carries no app identity at all. `cacheNamespace` (unique per app) is the first element
|
|
42
|
+
* here, so no two apps can ever share an entry even when they read the very same Bases.
|
|
43
|
+
* 2. Re-pointing safety — folder id, schema profile and all four Base slugs are part of the
|
|
44
|
+
* key, so changing any of them at deploy time starts from a cold cache instead of serving
|
|
45
|
+
* content read out of a different Base.
|
|
46
|
+
*
|
|
47
|
+
* The API key is deliberately NOT part of the key (it is a secret, and it does not change
|
|
48
|
+
* which records are addressed).
|
|
49
|
+
*/
|
|
50
|
+
const buildCmsCacheKeyPrefix = (config, resolved) => [
|
|
51
|
+
"busabase-cms-sdk",
|
|
52
|
+
config.cacheNamespace,
|
|
53
|
+
resolved.baseUrl,
|
|
54
|
+
resolved.spaceId,
|
|
55
|
+
resolved.folderId ?? "no-folder",
|
|
56
|
+
config.schemaProfile,
|
|
57
|
+
resolved.baseSlugs.posts,
|
|
58
|
+
resolved.baseSlugs.pages,
|
|
59
|
+
resolved.baseSlugs.categories,
|
|
60
|
+
resolved.baseSlugs.tags
|
|
61
|
+
];
|
|
62
|
+
/** `revalidateTag` targets per collection, defaulting to `<cacheNamespace>:cms-<collection>`. */
|
|
63
|
+
const resolveCmsCacheTags = (config) => ({
|
|
64
|
+
posts: config.cache?.tags?.posts ?? [`${config.cacheNamespace}:cms-posts`],
|
|
65
|
+
pages: config.cache?.tags?.pages ?? [`${config.cacheNamespace}:cms-pages`],
|
|
66
|
+
categories: config.cache?.tags?.categories ?? [`${config.cacheNamespace}:cms-categories`],
|
|
67
|
+
tags: config.cache?.tags?.tags ?? [`${config.cacheNamespace}:cms-tags`]
|
|
68
|
+
});
|
|
69
|
+
const createCmsClientProvider = (config) => {
|
|
70
|
+
const getCmsConfig = () => readCmsEnvConfig(config.baseSlugs);
|
|
71
|
+
/**
|
|
72
|
+
* When this is false, every `*OrFallback` read skips calling the CMS entirely (see the
|
|
73
|
+
* `readCmsOrFallback(enabled ? op : null, ...)` calls in posts/pages/taxonomy) — zero network
|
|
74
|
+
* attempts, identical to pure-local-MDX behavior.
|
|
75
|
+
*/
|
|
76
|
+
const isBusabaseCmsEnabled = () => getCmsConfig() !== null;
|
|
77
|
+
let cachedCms;
|
|
78
|
+
const getCms = () => {
|
|
79
|
+
if (cachedCms !== void 0) return cachedCms;
|
|
80
|
+
const resolved = getCmsConfig();
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
cachedCms = null;
|
|
83
|
+
return cachedCms;
|
|
84
|
+
}
|
|
85
|
+
cachedCms = createCachedBusabaseCms({
|
|
86
|
+
config: {
|
|
87
|
+
baseUrl: resolved.baseUrl,
|
|
88
|
+
apiKey: resolved.apiKey,
|
|
89
|
+
spaceId: resolved.spaceId
|
|
90
|
+
},
|
|
91
|
+
folderId: resolved.folderId ?? void 0,
|
|
92
|
+
lazyCreate: Boolean(resolved.folderId),
|
|
93
|
+
schemaProfile: config.schemaProfile,
|
|
94
|
+
baseSlugs: resolved.folderId ? void 0 : resolved.baseSlugs,
|
|
95
|
+
invalidRecords: config.invalidRecords,
|
|
96
|
+
onInvalidRecord: config.onInvalidRecord
|
|
97
|
+
}, {
|
|
98
|
+
revalidate: config.cache?.revalidate ?? 300,
|
|
99
|
+
tags: resolveCmsCacheTags(config),
|
|
100
|
+
keyPrefix: buildCmsCacheKeyPrefix(config, resolved)
|
|
101
|
+
});
|
|
102
|
+
return cachedCms;
|
|
103
|
+
};
|
|
104
|
+
const requireCms = () => {
|
|
105
|
+
const cms = getCms();
|
|
106
|
+
if (!cms) throw new Error(`[busabase-cms] ${config.appLabel}: BUSABASE_CMS_BASE_URL, BUSABASE_CMS_API_KEY and BUSABASE_CMS_SPACE_ID must all be set before reading CMS content`);
|
|
107
|
+
return cms;
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
getCms,
|
|
111
|
+
requireCms,
|
|
112
|
+
getCmsConfig,
|
|
113
|
+
isBusabaseCmsEnabled
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/integration/pages.ts
|
|
118
|
+
const createCmsPageReads = ({ getCms, requireCms }, appLabel) => ({
|
|
119
|
+
listBusabaseLandingPages: async () => requireCms().pages.list(),
|
|
120
|
+
getBusabaseLandingPageByPath: async (path) => requireCms().pages.getByPath(path),
|
|
121
|
+
listBusabaseLandingPagesOrFallback: async () => {
|
|
122
|
+
const cms = getCms();
|
|
123
|
+
return readCmsOrFallback(cms ? () => cms.pages.list() : null, [], `list ${appLabel} landing pages`);
|
|
124
|
+
},
|
|
125
|
+
getBusabaseLandingPageByPathOrFallback: async (path) => {
|
|
126
|
+
const cms = getCms();
|
|
127
|
+
return readCmsOrFallback(cms ? () => cms.pages.getByPath(path) : null, null, `get ${appLabel} landing page ${path}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const createCmsPageHelpers = ({ integration, generatePageMetadata }) => {
|
|
131
|
+
const { buildCmsPath, getBusabaseLandingPageByPathOrFallback, isCmsContentForLocale, parseCmsPath } = integration;
|
|
132
|
+
const getCmsPageForRequest = async (lang, path) => {
|
|
133
|
+
const canonicalPath = buildCmsPath(lang, path);
|
|
134
|
+
if (!canonicalPath) return null;
|
|
135
|
+
const page = await getBusabaseLandingPageByPathOrFallback(canonicalPath);
|
|
136
|
+
return page && isCmsContentForLocale(page, lang) ? page : null;
|
|
137
|
+
};
|
|
138
|
+
const generateCmsPageMetadata = (page, lang) => {
|
|
139
|
+
const parsed = parseCmsPath(page.path);
|
|
140
|
+
if (!parsed || parsed.locale !== lang) return {};
|
|
141
|
+
return generatePageMetadata({
|
|
142
|
+
title: page.seoTitle ?? page.title,
|
|
143
|
+
description: page.seoDescription ?? "",
|
|
144
|
+
path: parsed.pathWithoutLocale,
|
|
145
|
+
lang,
|
|
146
|
+
type: "website"
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
getCmsPageForRequest,
|
|
151
|
+
generateCmsPageMetadata
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/integration/posts.ts
|
|
156
|
+
const createCmsPostReads = ({ getCms, requireCms }, appLabel) => ({
|
|
157
|
+
listBusabaseBlogPosts: async () => requireCms().posts.list(),
|
|
158
|
+
getBusabaseBlogPostByPath: async (path) => requireCms().posts.getByPath(path),
|
|
159
|
+
listBusabaseBlogPostsOrFallback: async () => {
|
|
160
|
+
const cms = getCms();
|
|
161
|
+
return readCmsOrFallback(cms ? () => cms.posts.list() : null, [], `list ${appLabel} blog posts`);
|
|
162
|
+
},
|
|
163
|
+
getBusabaseBlogPostByPathOrFallback: async (path) => {
|
|
164
|
+
const cms = getCms();
|
|
165
|
+
return readCmsOrFallback(cms ? () => cms.posts.getByPath(path) : null, null, `get ${appLabel} blog post ${path}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* Merge Post card sources in priority order, using canonical paths as identity — the earliest
|
|
170
|
+
* source to claim a path wins. Lets an app render CMS Posts and bundled MDX posts in one grid
|
|
171
|
+
* without a CMS post and its local original showing up twice.
|
|
172
|
+
*/
|
|
173
|
+
const mergeBlogCardsByPath = (...sources) => {
|
|
174
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const source of sources) for (const post of source) {
|
|
176
|
+
const path = normalizeCmsPath(post.url);
|
|
177
|
+
if (path && !byPath.has(path)) byPath.set(path, {
|
|
178
|
+
...post,
|
|
179
|
+
url: path
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return [...byPath.values()].sort((a, b) => new Date(b.date ?? 0).getTime() - new Date(a.date ?? 0).getTime());
|
|
183
|
+
};
|
|
184
|
+
const createCmsPostResolver = ({ integration, localSource }) => {
|
|
185
|
+
const { buildCmsPath, getBusabaseBlogPostByPathOrFallback, isCmsContentForLocale, parseCmsPath } = integration;
|
|
186
|
+
const getCachedCmsPost = cache(async (locale, slugPath) => {
|
|
187
|
+
const path = buildCmsPath(locale, ["blog", ...slugPath.split("/").filter(Boolean)]);
|
|
188
|
+
const post = path ? await getBusabaseBlogPostByPathOrFallback(path) : null;
|
|
189
|
+
return post && isCmsContentForLocale(post, locale) ? post : null;
|
|
190
|
+
});
|
|
191
|
+
const defaultDependencies = {
|
|
192
|
+
getCmsPost: (locale, slugs) => getCachedCmsPost(locale, slugs.join("/")),
|
|
193
|
+
getLocalPost: (locale, slugs) => localSource.getPage(slugs, locale)
|
|
194
|
+
};
|
|
195
|
+
const resolveForLocale = async (locale, slugs, dependencies) => {
|
|
196
|
+
const cmsPost = await dependencies.getCmsPost(locale, slugs);
|
|
197
|
+
if (cmsPost) return {
|
|
198
|
+
source: "busabase",
|
|
199
|
+
content: cmsPost,
|
|
200
|
+
canonicalPath: parseCmsPath(cmsPost.path)?.pathWithoutLocale ?? `/blog/${slugs.join("/")}`
|
|
201
|
+
};
|
|
202
|
+
const localPost = dependencies.getLocalPost(locale, slugs);
|
|
203
|
+
if (localPost) return {
|
|
204
|
+
source: "local",
|
|
205
|
+
content: localPost,
|
|
206
|
+
canonicalPath: `/blog/${slugs.join("/")}`
|
|
207
|
+
};
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Resolve a Post detail page at the requested locale, falling back to English when the
|
|
212
|
+
* requested locale has no CMS or local-MDX content at that slug — a post not yet translated
|
|
213
|
+
* falls back to English rather than 404ing.
|
|
214
|
+
*/
|
|
215
|
+
const resolvePostPageWithDependencies = async (requestedLocale, slugPath, dependencies) => {
|
|
216
|
+
const slugs = slugPath.split("/").filter(Boolean);
|
|
217
|
+
const requested = await resolveForLocale(requestedLocale, slugs, dependencies);
|
|
218
|
+
const resolved = requested ?? (requestedLocale === "en" ? null : await resolveForLocale("en", slugs, dependencies));
|
|
219
|
+
return resolved ? {
|
|
220
|
+
...resolved,
|
|
221
|
+
requestedLocale,
|
|
222
|
+
contentLocale: requested ? requestedLocale : "en",
|
|
223
|
+
isLocaleFallback: !requested
|
|
224
|
+
} : null;
|
|
225
|
+
};
|
|
226
|
+
const resolvePostPageUncached = (requestedLocale, slugPath) => resolvePostPageWithDependencies(requestedLocale, slugPath, defaultDependencies);
|
|
227
|
+
return {
|
|
228
|
+
resolvePostPage: cache(resolvePostPageUncached),
|
|
229
|
+
resolvePostPageWithDependencies
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/integration/taxonomy.ts
|
|
234
|
+
const createCmsTaxonomyReads = ({ getCms, requireCms }, appLabel) => ({
|
|
235
|
+
listBusabaseCategories: async () => requireCms().categories.list(),
|
|
236
|
+
listBusabaseCategoriesOrFallback: async () => {
|
|
237
|
+
const cms = getCms();
|
|
238
|
+
return readCmsOrFallback(cms ? () => cms.categories.list() : null, [], `list ${appLabel} categories`);
|
|
239
|
+
},
|
|
240
|
+
listBusabaseTags: async () => requireCms().tags.list(),
|
|
241
|
+
listBusabaseTagsOrFallback: async () => {
|
|
242
|
+
const cms = getCms();
|
|
243
|
+
return readCmsOrFallback(cms ? () => cms.tags.list() : null, [], `list ${appLabel} tags`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/integration/index.ts
|
|
248
|
+
const createCmsIntegration = (config) => {
|
|
249
|
+
const cmsPathHelpers = createCmsPathHelpers({
|
|
250
|
+
supportedLocales: config.supportedLocales,
|
|
251
|
+
defaultLocale: config.defaultLocale
|
|
252
|
+
});
|
|
253
|
+
const provider = createCmsClientProvider(config);
|
|
254
|
+
return {
|
|
255
|
+
cmsPathHelpers,
|
|
256
|
+
buildCmsPath: cmsPathHelpers.buildPath,
|
|
257
|
+
parseCmsPath: cmsPathHelpers.parsePath,
|
|
258
|
+
isCmsContentForLocale: cmsPathHelpers.isForLocale,
|
|
259
|
+
buildCmsTaxonomyArchivePath: cmsPathHelpers.buildTaxonomyArchivePath,
|
|
260
|
+
isBusabaseCmsEnabled: provider.isBusabaseCmsEnabled,
|
|
261
|
+
getCmsConfig: provider.getCmsConfig,
|
|
262
|
+
...createCmsPostReads(provider, config.appLabel),
|
|
263
|
+
...createCmsPageReads(provider, config.appLabel),
|
|
264
|
+
...createCmsTaxonomyReads(provider, config.appLabel)
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
//#endregion
|
|
268
|
+
export { DEFAULT_CMS_REVALIDATE_SECONDS, buildCmsCacheKeyPrefix, createCmsClientProvider, createCmsIntegration, createCmsPageHelpers, createCmsPageReads, createCmsPostReads, createCmsPostResolver, createCmsTaxonomyReads, mergeBlogCardsByPath, readCmsEnvConfig, resolveCmsCacheTags };
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { m as BusabaseCmsOptions, p as BusabaseCms, r as CmsPathHelpers } from "./routing-VU56uuJ0.js";
|
|
2
|
+
import "server-only";
|
|
3
|
+
import { MetadataRoute } from "next";
|
|
4
|
+
//#region src/next.d.ts
|
|
5
|
+
interface BusabaseCmsCacheOptions {
|
|
6
|
+
revalidate?: number | false;
|
|
7
|
+
tags?: {
|
|
8
|
+
posts?: string[];
|
|
9
|
+
pages?: string[];
|
|
10
|
+
categories?: string[];
|
|
11
|
+
tags?: string[];
|
|
12
|
+
};
|
|
13
|
+
keyPrefix?: string[];
|
|
14
|
+
}
|
|
15
|
+
declare const resolveBusabaseCmsCacheKeyPrefix: (options: BusabaseCmsOptions, cache: BusabaseCmsCacheOptions) => string[];
|
|
16
|
+
declare const createCachedBusabaseCms: (options?: BusabaseCmsOptions, cache?: BusabaseCmsCacheOptions) => BusabaseCms;
|
|
17
|
+
type MinimalCmsItem = {
|
|
18
|
+
locale: string;
|
|
19
|
+
path: string;
|
|
20
|
+
updatedAt?: string;
|
|
21
|
+
};
|
|
22
|
+
interface CmsSitemapEntryOptions<T extends MinimalCmsItem> {
|
|
23
|
+
changeFrequency?: MetadataRoute.Sitemap[number]["changeFrequency"];
|
|
24
|
+
/** Static priority, or derive one per item from its own fields and canonical path. */
|
|
25
|
+
priority?: number | ((item: T, canonicalPath: string) => number);
|
|
26
|
+
}
|
|
27
|
+
/** Sitemap entries for Blog Posts — only paths under the `blog` segment are included. */
|
|
28
|
+
declare const buildCmsBlogSitemapEntries: <T extends MinimalCmsItem>(posts: readonly T[], helpers: CmsPathHelpers, baseUrl: string, options?: CmsSitemapEntryOptions<T>, fallbackDate?: Date) => MetadataRoute.Sitemap;
|
|
29
|
+
/** Sitemap entries for Pages — every valid-for-locale record is included, no path filter. */
|
|
30
|
+
declare const buildCmsPageSitemapEntries: <T extends MinimalCmsItem>(pages: readonly T[], helpers: CmsPathHelpers, baseUrl: string, options?: CmsSitemapEntryOptions<T>, fallbackDate?: Date) => MetadataRoute.Sitemap;
|
|
31
|
+
/** Merge sitemap sources, keeping the first entry seen for any duplicate URL. */
|
|
32
|
+
declare const dedupeCmsSitemapEntries: (...sources: readonly MetadataRoute.Sitemap[]) => MetadataRoute.Sitemap;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { BusabaseCmsCacheOptions, CmsSitemapEntryOptions, buildCmsBlogSitemapEntries, buildCmsPageSitemapEntries, createCachedBusabaseCms, dedupeCmsSitemapEntries, resolveBusabaseCmsCacheKeyPrefix };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { a as createBusabaseCms } from "./content-BXgsWQjQ.js";
|
|
2
|
+
import { resolveConfig } from "busabase-sdk";
|
|
3
|
+
import "server-only";
|
|
4
|
+
import { unstable_cache } from "next/cache.js";
|
|
5
|
+
//#region src/next.ts
|
|
6
|
+
const resolveBusabaseCmsCacheKeyPrefix = (options, cache) => {
|
|
7
|
+
if (cache.keyPrefix && cache.keyPrefix.length > 0) return cache.keyPrefix;
|
|
8
|
+
if (options.source || options.client) throw new Error("createCachedBusabaseCms requires cache.keyPrefix when using a custom source or client");
|
|
9
|
+
const config = resolveConfig(options.config);
|
|
10
|
+
if (config.headers || config.apiKey && !config.spaceId) throw new Error("createCachedBusabaseCms requires cache.keyPrefix when the target space cannot be represented without secrets");
|
|
11
|
+
const schemaProfile = options.schemaProfile ?? "standard";
|
|
12
|
+
return [
|
|
13
|
+
"busabase-cms-sdk",
|
|
14
|
+
config.baseUrl,
|
|
15
|
+
config.spaceId ?? "default-space",
|
|
16
|
+
...options.folderId ? [options.folderId, schemaProfile] : [
|
|
17
|
+
"no-folder",
|
|
18
|
+
schemaProfile,
|
|
19
|
+
options.baseSlugs?.posts ?? "busabase-cms-posts",
|
|
20
|
+
options.baseSlugs?.pages ?? "busabase-cms-pages",
|
|
21
|
+
options.baseSlugs?.categories ?? "busabase-cms-categories",
|
|
22
|
+
options.baseSlugs?.tags ?? "busabase-cms-tags"
|
|
23
|
+
]
|
|
24
|
+
];
|
|
25
|
+
};
|
|
26
|
+
const createCachedBusabaseCms = (options = {}, cache = {}) => {
|
|
27
|
+
const keyPrefix = resolveBusabaseCmsCacheKeyPrefix(options, cache);
|
|
28
|
+
const cms = createBusabaseCms(options);
|
|
29
|
+
const revalidate = cache.revalidate ?? 300;
|
|
30
|
+
const cachedList = (list, kind, slug) => unstable_cache(list, [
|
|
31
|
+
...keyPrefix,
|
|
32
|
+
kind,
|
|
33
|
+
slug
|
|
34
|
+
], {
|
|
35
|
+
revalidate,
|
|
36
|
+
tags: cache.tags?.[kind] ?? [`busabase-cms:${kind}`]
|
|
37
|
+
});
|
|
38
|
+
const listPosts = cachedList(cms.posts.list, "posts", options.baseSlugs?.posts ?? "busabase-cms-posts");
|
|
39
|
+
const listPages = cachedList(cms.pages.list, "pages", options.baseSlugs?.pages ?? "busabase-cms-pages");
|
|
40
|
+
const listCategories = cachedList(cms.categories.list, "categories", options.baseSlugs?.categories ?? "busabase-cms-categories");
|
|
41
|
+
const listTags = cachedList(cms.tags.list, "tags", options.baseSlugs?.tags ?? "busabase-cms-tags");
|
|
42
|
+
const cachedGetByArg = (get, kind, slug) => unstable_cache(get, [
|
|
43
|
+
...keyPrefix,
|
|
44
|
+
kind,
|
|
45
|
+
slug,
|
|
46
|
+
"by-arg"
|
|
47
|
+
], {
|
|
48
|
+
revalidate,
|
|
49
|
+
tags: cache.tags?.[kind] ?? [`busabase-cms:${kind}`]
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
posts: {
|
|
53
|
+
list: listPosts,
|
|
54
|
+
getByPath: cachedGetByArg(cms.posts.getByPath, "posts", options.baseSlugs?.posts ?? "busabase-cms-posts")
|
|
55
|
+
},
|
|
56
|
+
pages: {
|
|
57
|
+
list: listPages,
|
|
58
|
+
getByPath: cachedGetByArg(cms.pages.getByPath, "pages", options.baseSlugs?.pages ?? "busabase-cms-pages")
|
|
59
|
+
},
|
|
60
|
+
categories: {
|
|
61
|
+
list: listCategories,
|
|
62
|
+
getBySlug: cachedGetByArg(cms.categories.getBySlug, "categories", options.baseSlugs?.categories ?? "busabase-cms-categories")
|
|
63
|
+
},
|
|
64
|
+
tags: {
|
|
65
|
+
list: listTags,
|
|
66
|
+
getBySlug: cachedGetByArg(cms.tags.getBySlug, "tags", options.baseSlugs?.tags ?? "busabase-cms-tags")
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const validDateOr = (value, fallback) => {
|
|
71
|
+
if (!value) return fallback;
|
|
72
|
+
const parsed = new Date(value);
|
|
73
|
+
return Number.isNaN(parsed.getTime()) ? fallback : parsed;
|
|
74
|
+
};
|
|
75
|
+
const toCmsSitemapEntries = (items, helpers, baseUrl, isMatch, { changeFrequency = "monthly", priority = .7 }, fallbackDate) => items.flatMap((item) => {
|
|
76
|
+
const parsed = helpers.parsePath(item.path);
|
|
77
|
+
if (!parsed || !isMatch(parsed.canonicalPath) || !helpers.isForLocale(item, parsed.locale)) return [];
|
|
78
|
+
return [{
|
|
79
|
+
url: `${baseUrl}${parsed.canonicalPath}`,
|
|
80
|
+
lastModified: validDateOr(item.updatedAt, fallbackDate),
|
|
81
|
+
changeFrequency,
|
|
82
|
+
priority: typeof priority === "function" ? priority(item, parsed.canonicalPath) : priority
|
|
83
|
+
}];
|
|
84
|
+
});
|
|
85
|
+
/** Sitemap entries for Blog Posts — only paths under the `blog` segment are included. */
|
|
86
|
+
const buildCmsBlogSitemapEntries = (posts, helpers, baseUrl, options = {}, fallbackDate = /* @__PURE__ */ new Date()) => toCmsSitemapEntries(posts, helpers, baseUrl, (canonicalPath) => helpers.isBlogPostPath(canonicalPath), {
|
|
87
|
+
changeFrequency: "weekly",
|
|
88
|
+
priority: .7,
|
|
89
|
+
...options
|
|
90
|
+
}, fallbackDate);
|
|
91
|
+
/** Sitemap entries for Pages — every valid-for-locale record is included, no path filter. */
|
|
92
|
+
const buildCmsPageSitemapEntries = (pages, helpers, baseUrl, options = {}, fallbackDate = /* @__PURE__ */ new Date()) => toCmsSitemapEntries(pages, helpers, baseUrl, () => true, {
|
|
93
|
+
changeFrequency: "monthly",
|
|
94
|
+
priority: .8,
|
|
95
|
+
...options
|
|
96
|
+
}, fallbackDate);
|
|
97
|
+
/** Merge sitemap sources, keeping the first entry seen for any duplicate URL. */
|
|
98
|
+
const dedupeCmsSitemapEntries = (...sources) => {
|
|
99
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const source of sources) for (const entry of source) {
|
|
101
|
+
const key = entry.url.replace(/\/+$/, "") || entry.url;
|
|
102
|
+
if (!byUrl.has(key)) byUrl.set(key, entry);
|
|
103
|
+
}
|
|
104
|
+
return [...byUrl.values()];
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
107
|
+
export { buildCmsBlogSitemapEntries, buildCmsPageSitemapEntries, createCachedBusabaseCms, dedupeCmsSitemapEntries, resolveBusabaseCmsCacheKeyPrefix };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/fallback.ts
|
|
2
|
+
/**
|
|
3
|
+
* The "read from Busabase, fall back to bundled content on any failure" wrapper — every app
|
|
4
|
+
* consuming busabase-cms re-implemented this same try/catch/console.warn a couple of times
|
|
5
|
+
* (once for list-shaped reads, once for single-record reads). One generic version, one place
|
|
6
|
+
* to change the log format.
|
|
7
|
+
*/
|
|
8
|
+
const readCmsOrFallback = async (operation, fallback, label) => {
|
|
9
|
+
if (!operation) return fallback;
|
|
10
|
+
try {
|
|
11
|
+
return await operation();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.warn(`[busabase-cms] ${label} failed; using bundled content`, error);
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/routing.ts
|
|
19
|
+
const normalizeSegment = (segment) => {
|
|
20
|
+
try {
|
|
21
|
+
const decoded = decodeURIComponent(segment);
|
|
22
|
+
if (!decoded || decoded === "." || decoded === ".." || /[\\/\0]/.test(decoded)) return null;
|
|
23
|
+
return decoded;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Normalize a stored or requested path into a stable, decoded canonical key. */
|
|
29
|
+
const normalizeCmsPath = (path) => {
|
|
30
|
+
if (!path.startsWith("/") || path.includes("?") || path.includes("#")) return null;
|
|
31
|
+
const segments = [];
|
|
32
|
+
for (const rawSegment of path.split("/").filter(Boolean)) {
|
|
33
|
+
const segment = normalizeSegment(rawSegment);
|
|
34
|
+
if (segment === null) return null;
|
|
35
|
+
segments.push(segment);
|
|
36
|
+
}
|
|
37
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
38
|
+
};
|
|
39
|
+
/** Parse locale ownership from a canonical CMS path. The default locale is unprefixed. */
|
|
40
|
+
const parseCmsCanonicalPath = (path, { supportedLocales, defaultLocale = "en" }) => {
|
|
41
|
+
if (!supportedLocales.includes(defaultLocale)) return null;
|
|
42
|
+
const canonicalPath = normalizeCmsPath(path);
|
|
43
|
+
if (!canonicalPath || canonicalPath === "/") return null;
|
|
44
|
+
const segments = canonicalPath.slice(1).split("/");
|
|
45
|
+
const first = segments[0];
|
|
46
|
+
if (first === defaultLocale) return null;
|
|
47
|
+
const hasLocalePrefix = supportedLocales.includes(first);
|
|
48
|
+
const locale = hasLocalePrefix ? first : defaultLocale;
|
|
49
|
+
const contentSegments = hasLocalePrefix ? segments.slice(1) : segments;
|
|
50
|
+
if (contentSegments.length === 0) return null;
|
|
51
|
+
const pathWithoutLocale = `/${contentSegments.join("/")}`;
|
|
52
|
+
return {
|
|
53
|
+
canonicalPath: locale === defaultLocale ? pathWithoutLocale : `/${locale}${pathWithoutLocale}`,
|
|
54
|
+
locale,
|
|
55
|
+
pathWithoutLocale,
|
|
56
|
+
segments: contentSegments
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const buildCmsCanonicalPath = (locale, path, options) => {
|
|
60
|
+
if (!options.supportedLocales.includes(locale)) return null;
|
|
61
|
+
const contentPath = Array.isArray(path) ? path.join("/") : path;
|
|
62
|
+
const localePrefix = locale === (options.defaultLocale ?? "en") ? "" : `/${locale}`;
|
|
63
|
+
return parseCmsCanonicalPath(`${localePrefix}/${contentPath}`, options)?.canonicalPath ?? null;
|
|
64
|
+
};
|
|
65
|
+
const isCmsContentForLocale = (item, locale, options) => {
|
|
66
|
+
const parsed = parseCmsCanonicalPath(item.path, options);
|
|
67
|
+
return item.locale === locale && parsed?.locale === locale;
|
|
68
|
+
};
|
|
69
|
+
const isCmsBlogPostPath = (path, options) => {
|
|
70
|
+
const parsed = parseCmsCanonicalPath(path, options);
|
|
71
|
+
return parsed?.segments[0] === "blog" && parsed.segments.length > 1;
|
|
72
|
+
};
|
|
73
|
+
/** Build a locale-aware archive URL for a Category or Tag record. */
|
|
74
|
+
const buildCmsTaxonomyArchivePath = (kind, taxonomy, options) => buildCmsCanonicalPath(taxonomy.locale, [kind, taxonomy.slug], options);
|
|
75
|
+
const createCmsPathHelpers = (options) => ({
|
|
76
|
+
options,
|
|
77
|
+
normalizePath: normalizeCmsPath,
|
|
78
|
+
parsePath: (path) => parseCmsCanonicalPath(path, options),
|
|
79
|
+
buildPath: (locale, path) => buildCmsCanonicalPath(locale, path, options),
|
|
80
|
+
isForLocale: (item, locale) => isCmsContentForLocale(item, locale, options),
|
|
81
|
+
isValidContent: (item) => isCmsContentForLocale(item, item.locale, options),
|
|
82
|
+
isBlogPostPath: (path) => isCmsBlogPostPath(path, options),
|
|
83
|
+
buildTaxonomyArchivePath: (kind, taxonomy) => buildCmsTaxonomyArchivePath(kind, taxonomy, options)
|
|
84
|
+
});
|
|
85
|
+
/** Select Posts related to one taxonomy record without mixing locales. */
|
|
86
|
+
const filterCmsPostsByTaxonomy = (posts, kind, taxonomy) => {
|
|
87
|
+
const relationKey = kind === "categories" ? "categoryIds" : "tagIds";
|
|
88
|
+
return posts.filter((post) => post.locale === taxonomy.locale && post[relationKey].includes(taxonomy.id));
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { isCmsBlogPostPath as a, parseCmsCanonicalPath as c, filterCmsPostsByTaxonomy as i, readCmsOrFallback as l, buildCmsTaxonomyArchivePath as n, isCmsContentForLocale as o, createCmsPathHelpers as r, normalizeCmsPath as s, buildCmsCanonicalPath as t };
|