@botlr/next 0.1.0 → 0.3.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.js CHANGED
@@ -8,7 +8,11 @@ var BOTLR_ENV = {
8
8
  apiUrl: "BOTLR_API_URL",
9
9
  apiKey: "BOTLR_API_KEY",
10
10
  appId: "BOTLR_APP_ID",
11
- pat: "BOTLR_PAT"
11
+ pat: "BOTLR_PAT",
12
+ orgSlug: "BOTLR_ORG_SLUG",
13
+ projectSlug: "BOTLR_PROJECT_SLUG",
14
+ appInstanceSlug: "BOTLR_APP_INSTANCE_SLUG",
15
+ environment: "BOTLR_ENVIRONMENT"
12
16
  };
13
17
  var DEFAULT_API_URL = "http://localhost:8888";
14
18
  function env(name) {
@@ -28,6 +32,18 @@ function configFromEnv(overrides = {}) {
28
32
  personalToken: overrides.personalToken ?? env(BOTLR_ENV.pat)
29
33
  };
30
34
  }
35
+ function environmentFromEnv(fallback = "production") {
36
+ return env(BOTLR_ENV.environment) ?? env("ENVIRONMENT") ?? env("NODE_ENV") ?? fallback;
37
+ }
38
+ function scopeFromEnv(overrides) {
39
+ return {
40
+ orgSlug: env(BOTLR_ENV.orgSlug) ?? "",
41
+ projectSlug: env(BOTLR_ENV.projectSlug) ?? "",
42
+ appInstanceSlug: env(BOTLR_ENV.appInstanceSlug) ?? "",
43
+ appInstanceId: env(BOTLR_ENV.appId) ?? "",
44
+ ...overrides
45
+ };
46
+ }
31
47
 
32
48
  // src/locale.ts
33
49
  function localeFromAcceptLanguage(header, fallback = "en") {
@@ -48,7 +64,8 @@ async function getBotlrForRoute(params = {}) {
48
64
  previewToken: params.previewToken,
49
65
  locale,
50
66
  preview: params.preview,
51
- fetch: params.fetch
67
+ fetch: params.fetch,
68
+ retry: params.retry
52
69
  })
53
70
  );
54
71
  }
@@ -57,19 +74,559 @@ function createBotlrFromEnv(overrides = {}) {
57
74
  return createBotlrClient(configFromEnv(overrides));
58
75
  }
59
76
 
77
+ // src/resolve-page.ts
78
+ import { cache as cache2 } from "react";
79
+ var DEFAULT_CONTENT_SECTIONS = { article: "article" };
80
+ async function resolvePage(client, slug, locale, options = {}) {
81
+ try {
82
+ const result = await client.resolveUrl(slug, locale);
83
+ if (result.kind === "404") return { kind: "404" };
84
+ if (result.kind === "redirect" && result.redirect) {
85
+ return { kind: "redirect", destination: result.redirect.destination };
86
+ }
87
+ if (!result.data) return { kind: "404" };
88
+ const data = result.data;
89
+ const layout = data.layout ?? {};
90
+ const content = data.content ?? {};
91
+ const contentType = content["type"] ?? data.type ?? null;
92
+ if (contentType && contentType !== "page") {
93
+ const sectionType = (options.contentSections ?? DEFAULT_CONTENT_SECTIONS)[contentType];
94
+ if (sectionType) {
95
+ const section = {
96
+ _key: `${sectionType}-${content["id"] ?? data.id}`,
97
+ mode: "inline",
98
+ sectionType,
99
+ props: content,
100
+ status: "published"
101
+ };
102
+ layout["main"] = [section, ...layout["main"] ?? []];
103
+ } else {
104
+ console.warn("Unknown content type:", contentType);
105
+ }
106
+ }
107
+ return {
108
+ kind: "page",
109
+ pageData: {
110
+ id: data.id,
111
+ name: data.name,
112
+ slug: data.slug || "",
113
+ full_slug: data.full_slug || "",
114
+ layout,
115
+ status: data.status,
116
+ theme: data.theme,
117
+ page_template: data.page_template,
118
+ seo_meta: data.seo_meta,
119
+ alternate_translations: result.alternate_translations ?? [],
120
+ breadcrumbs: result.breadcrumbs ?? []
121
+ }
122
+ };
123
+ } catch (error) {
124
+ return {
125
+ kind: "error",
126
+ message: error instanceof Error ? error.message : "Unknown error",
127
+ cause: error
128
+ };
129
+ }
130
+ }
131
+ function createPageResolver(client, options = {}) {
132
+ return cache2((slug, locale) => resolvePage(client, slug, locale, options));
133
+ }
134
+
135
+ // src/page-metadata.ts
136
+ function buildPageMetadata(pageData, options = {}) {
137
+ const seo = pageData.seo_meta || {};
138
+ const title = seo.title || pageData.name;
139
+ const alternates = pageData.alternate_translations || [];
140
+ const siteUrl = options.siteUrl?.replace(/\/+$/, "");
141
+ const folder = options.folder ? `/${options.folder.replace(/^\/+|\/+$/g, "")}` : "";
142
+ const metadata = { title };
143
+ if (seo.description) metadata.description = seo.description;
144
+ if (seo.keywords) metadata.keywords = seo.keywords;
145
+ const ogImage = seo.ogImage?.url || seo.ogImage?.filename;
146
+ const description = seo.ogDescription || seo.description;
147
+ metadata.openGraph = {
148
+ title: seo.ogTitle || title,
149
+ ...description ? { description } : {},
150
+ ...seo.ogType ? { type: seo.ogType } : {},
151
+ ...ogImage ? { images: [ogImage] } : {}
152
+ };
153
+ if (seo.twitterCard) {
154
+ metadata.twitter = {
155
+ card: seo.twitterCard,
156
+ title: seo.ogTitle || title,
157
+ ...description ? { description } : {},
158
+ ...ogImage ? { images: [ogImage] } : {}
159
+ };
160
+ }
161
+ if (seo.noindex !== void 0 || seo.nofollow !== void 0 || seo.robots) {
162
+ metadata.robots = seo.robots ?? { index: !seo.noindex, follow: !seo.nofollow };
163
+ }
164
+ if (seo.canonical) {
165
+ metadata.alternates = { canonical: seo.canonical };
166
+ } else if (siteUrl) {
167
+ const canonical = pageData.full_slug ? `${siteUrl}${folder}/${pageData.full_slug}` : `${siteUrl}${folder}`;
168
+ metadata.alternates = { canonical };
169
+ }
170
+ if (siteUrl && alternates.length > 0) {
171
+ const languages = {};
172
+ for (const alt of alternates) {
173
+ languages[alt.locale] = `${siteUrl}${alt.full_slug}`;
174
+ }
175
+ metadata.alternates = { ...metadata.alternates, languages };
176
+ }
177
+ return metadata;
178
+ }
179
+
180
+ // src/render-cms-page.tsx
181
+ import parse from "html-react-parser";
182
+ import {
183
+ SectionRenderer,
184
+ filterVisibleNodesRecursive
185
+ } from "@botlr/react";
186
+
187
+ // src/theme-css.ts
188
+ var COLOR_KEYS = [
189
+ "primary",
190
+ "primaryForeground",
191
+ "secondary",
192
+ "secondaryForeground",
193
+ "accent",
194
+ "accentForeground",
195
+ "background",
196
+ "foreground",
197
+ "muted",
198
+ "mutedForeground",
199
+ "border",
200
+ "input",
201
+ "ring",
202
+ "destructive",
203
+ "destructiveForeground",
204
+ "success",
205
+ "successForeground",
206
+ "warning",
207
+ "warningForeground",
208
+ "info",
209
+ "infoForeground"
210
+ ];
211
+ var COMMON_GROUPS = [
212
+ [["typography", "fontFamily"], "--font", ["heading", "body", "ui", "code", "sans", "serif", "mono"]],
213
+ [["typography", "fontSize"], "--font-size", ["h1", "h2", "h3", "h4", "h5", "h6", "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl"]],
214
+ [["typography", "fontWeight"], "--font-weight", ["normal", "medium", "semibold", "bold"]],
215
+ [["typography", "lineHeight"], "--line-height", ["tight", "normal", "relaxed"]],
216
+ [["spacing"], "--spacing", ["xs", "sm", "md", "lg", "xl", "2xl"]],
217
+ [["borderRadius"], "--radius", ["none", "sm", "md", "lg", "xl", "full"]],
218
+ [["shadows"], "--shadow", ["sm", "md", "lg", "xl"]],
219
+ [["layout"], "-", ["containerMaxWidth", "sidebarWidth", "headerHeight", "footerHeight"]]
220
+ ];
221
+ var kebab = (key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
222
+ function colorVars(colors) {
223
+ if (!colors) return [];
224
+ const vars = [];
225
+ for (const key of COLOR_KEYS) {
226
+ const value = colors[key];
227
+ if (value) vars.push(`--color-${kebab(key)}: ${value};`);
228
+ }
229
+ return vars;
230
+ }
231
+ function pick(settings, path) {
232
+ let current = settings;
233
+ for (const segment of path) {
234
+ current = current?.[segment];
235
+ if (!current) return void 0;
236
+ }
237
+ return current;
238
+ }
239
+ function themeSettingsToCss(settings) {
240
+ if (!settings) return "";
241
+ const s = settings;
242
+ const lightVars = colorVars(s.colors?.light);
243
+ const darkVars = colorVars(s.colors?.dark);
244
+ const commonVars = [];
245
+ for (const [path, prefix, keys] of COMMON_GROUPS) {
246
+ const group = pick(settings, path);
247
+ if (!group) continue;
248
+ for (const key of keys) {
249
+ const value = group[key];
250
+ if (value) commonVars.push(`${prefix}-${kebab(key)}: ${value};`);
251
+ }
252
+ }
253
+ const cssBlocks = [];
254
+ if (commonVars.length > 0) {
255
+ cssBlocks.push(`:root {
256
+ ${commonVars.join("\n ")}
257
+ }`);
258
+ }
259
+ if (lightVars.length > 0) {
260
+ cssBlocks.push(`:root {
261
+ ${lightVars.join("\n ")}
262
+ }`);
263
+ }
264
+ if (darkVars.length > 0) {
265
+ cssBlocks.push(`@media (prefers-color-scheme: dark) {
266
+ :root {
267
+ ${darkVars.join("\n ")}
268
+ }
269
+ }`);
270
+ cssBlocks.push(`.dark {
271
+ ${darkVars.join("\n ")}
272
+ }`);
273
+ }
274
+ return cssBlocks.join("\n\n");
275
+ }
276
+
277
+ // src/render-cms-page.tsx
278
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
279
+ var DEFAULT_TEMPLATE = `<!DOCTYPE html>
280
+ <html lang="en">
281
+ <head>
282
+ <meta charset="UTF-8">
283
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
284
+ <title>__PAGE_TITLE__</title>
285
+ <style>
286
+ * { margin: 0; padding: 0; box-sizing: border-box; }
287
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
288
+ </style>
289
+ __THEME_CSS__
290
+ </head>
291
+ <body>
292
+ <main>
293
+ __REGION_MAIN__
294
+ </main>
295
+ </body>
296
+ </html>`;
297
+ function extractBodyContent(html) {
298
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
299
+ return bodyMatch?.[1] ? bodyMatch[1].trim() : html;
300
+ }
301
+ function collectSharedSectionIds(nodes) {
302
+ const ids = [];
303
+ for (const node of nodes) {
304
+ if ("mode" in node && node.mode === "shared" && node.sharedSectionId) {
305
+ ids.push(node.sharedSectionId);
306
+ }
307
+ if ("type" in node && node.type === "grid-container" && Array.isArray(node.columns)) {
308
+ for (const column of node.columns) {
309
+ if (Array.isArray(column.sections)) ids.push(...collectSharedSectionIds(column.sections));
310
+ }
311
+ }
312
+ }
313
+ return ids;
314
+ }
315
+ function prepareTemplate(templateHtml, pageData, templateCss, locale) {
316
+ let prepared = templateHtml.replace(/\{\{page_title\}\}/g, pageData.name).replace(/\{\{page_lang\}\}/g, locale).replace(/\{\{page_description\}\}/g, "");
317
+ prepared = prepared.replace(/\{\{\{theme_css\}\}\}/g, templateCss ? `<style>${templateCss}</style>` : "");
318
+ prepared = prepared.replace(/\{\{\{([A-Z_]+)\}\}\}/g, "__REGION_$1__");
319
+ prepared = prepared.replace(/\{\{#if\s+([A-Z_]+)\}\}/g, "");
320
+ prepared = prepared.replace(/\{\{\/if\}\}/g, "");
321
+ return extractBodyContent(prepared);
322
+ }
323
+ function mergeTemplateLayout(pageData) {
324
+ const merged = {};
325
+ const regions = /* @__PURE__ */ new Set([...Object.keys(pageData.layout), ...Object.keys(pageData.page_template?.layout ?? {})]);
326
+ for (const region of regions) {
327
+ merged[region] = [...pageData.page_template?.layout?.[region] ?? [], ...pageData.layout[region] ?? []];
328
+ }
329
+ return merged;
330
+ }
331
+ var errorBox = (heading, message) => /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
332
+ /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.5rem", fontWeight: 700, marginBottom: "0.5rem" }, children: heading }),
333
+ /* @__PURE__ */ jsx("p", { style: { opacity: 0.7 }, children: message })
334
+ ] }) });
335
+ async function renderCmsPage({
336
+ pageData,
337
+ scope,
338
+ componentRegistry,
339
+ mode = "published",
340
+ locale = scope.locale,
341
+ environment = environmentFromEnv(),
342
+ client,
343
+ fetchSharedSection,
344
+ showModeBadge = false,
345
+ renderError
346
+ }) {
347
+ let templateHtml;
348
+ let templateCss = "";
349
+ if (pageData.page_template?.template_html) {
350
+ templateHtml = pageData.page_template.template_html;
351
+ templateCss = pageData.page_template.template_css || "";
352
+ } else if (pageData.theme?.default_template_html) {
353
+ templateHtml = pageData.theme.default_template_html;
354
+ templateCss = pageData.theme.default_template_css || "";
355
+ } else {
356
+ templateHtml = DEFAULT_TEMPLATE;
357
+ }
358
+ const preparedTemplate = prepareTemplate(templateHtml, pageData, templateCss, locale);
359
+ const pageScope = {
360
+ id: pageData.id,
361
+ kind: "page",
362
+ name: pageData.name,
363
+ slug: pageData.slug || "",
364
+ breadcrumbs: pageData.breadcrumbs || []
365
+ };
366
+ const visibilityContext = {
367
+ pageId: pageData.id,
368
+ currentPath: "/" + (pageData.slug || ""),
369
+ locale,
370
+ environment
371
+ };
372
+ const filteredLayout = {};
373
+ for (const [region, sections] of Object.entries(mergeTemplateLayout(pageData))) {
374
+ filteredLayout[region] = filterVisibleNodesRecursive(sections, visibilityContext);
375
+ }
376
+ const fetchShared = fetchSharedSection ?? (client ? (id) => client.getPageBySlug(id, locale) : void 0);
377
+ const sharedSectionIds = Object.values(filteredLayout).flatMap(collectSharedSectionIds);
378
+ const sharedSectionsMap = /* @__PURE__ */ new Map();
379
+ if (sharedSectionIds.length > 0) {
380
+ if (!fetchShared) {
381
+ throw new Error("@botlr/next: renderCmsPage needs `client` or `fetchSharedSection` to render shared sections");
382
+ }
383
+ const results = await Promise.all(
384
+ [...new Set(sharedSectionIds)].map(async (id) => {
385
+ try {
386
+ const data = await fetchShared(id);
387
+ const main = data?.layout?.["main"];
388
+ if (Array.isArray(main) && main.length > 0) return { id, sections: main };
389
+ } catch (err) {
390
+ console.error(`Error fetching shared section ${id}:`, err);
391
+ }
392
+ return null;
393
+ })
394
+ );
395
+ for (const result of results) {
396
+ if (result) sharedSectionsMap.set(result.id, result.sections);
397
+ }
398
+ }
399
+ const parseOptions = {
400
+ replace: (domNode) => {
401
+ if (!("data" in domNode) || typeof domNode.data !== "string") return;
402
+ const regionMatch = domNode.data.match(/__REGION_([A-Z_]+)__/);
403
+ if (!regionMatch?.[1]) return;
404
+ const regionKey = regionMatch[1].toLowerCase();
405
+ const regionSections = filteredLayout[regionKey] ?? [];
406
+ if (regionSections.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
407
+ const templateSectionKeys = new Set(
408
+ (pageData.page_template?.layout?.[regionKey] ?? []).map((section) => section._key)
409
+ );
410
+ return /* @__PURE__ */ jsx(Fragment, { children: regionSections.map((section) => /* @__PURE__ */ jsx(
411
+ SectionRenderer,
412
+ {
413
+ section,
414
+ componentRegistry,
415
+ mode,
416
+ sharedSectionsData: sharedSectionsMap,
417
+ scope,
418
+ pageScope,
419
+ fromTemplate: templateSectionKeys.has(section._key)
420
+ },
421
+ section._key
422
+ )) });
423
+ }
424
+ };
425
+ const themeSettingsCss = themeSettingsToCss(pageData.theme?.settings);
426
+ try {
427
+ const renderedContent = parse(preparedTemplate, parseOptions);
428
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
429
+ themeSettingsCss && /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: themeSettingsCss } }),
430
+ templateCss && /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: templateCss } }),
431
+ renderedContent,
432
+ showModeBadge && /* @__PURE__ */ jsx(
433
+ "div",
434
+ {
435
+ style: {
436
+ position: "fixed",
437
+ bottom: "1rem",
438
+ right: "1rem",
439
+ padding: "0.5rem 1rem",
440
+ borderRadius: "0.5rem",
441
+ background: "#111",
442
+ color: "#fff",
443
+ fontSize: "0.875rem",
444
+ fontWeight: 500,
445
+ boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
446
+ zIndex: 50
447
+ },
448
+ children: mode === "draft" ? "Draft preview" : "Published preview"
449
+ }
450
+ )
451
+ ] });
452
+ } catch (error) {
453
+ console.error("Error rendering template:", error);
454
+ if (renderError) return /* @__PURE__ */ jsx(Fragment, { children: renderError(error) });
455
+ return errorBox("Template Error", error instanceof Error ? error.message : "Failed to render template");
456
+ }
457
+ }
458
+
459
+ // src/static-text-file.ts
460
+ import { filterVisibleNodesRecursive as filterVisibleNodesRecursive2 } from "@botlr/react";
461
+ function collectSections(nodes, sectionType) {
462
+ const result = [];
463
+ for (const node of nodes) {
464
+ if ("mode" in node && node.mode === "inline" && node.sectionType === sectionType) {
465
+ result.push(node);
466
+ } else if ("type" in node && node.type === "grid-container" && Array.isArray(node.columns)) {
467
+ for (const column of node.columns) {
468
+ result.push(...collectSections(column.sections ?? [], sectionType));
469
+ }
470
+ }
471
+ }
472
+ return result;
473
+ }
474
+ var TEXT_PLAIN = { "Content-Type": "text/plain; charset=utf-8" };
475
+ async function serveStaticTextFile(client, slug, defaultContent, options = {}) {
476
+ const locale = options.locale ?? client.config.locale ?? "en";
477
+ const environment = options.environment ?? environmentFromEnv();
478
+ const sectionType = options.sectionType ?? "text";
479
+ const propKeys = options.propKeys ?? ["content", "body", "text"];
480
+ try {
481
+ const page = await client.getPageBySlug(slug, locale);
482
+ if (page?.layout) {
483
+ const visibilityContext = { locale, environment };
484
+ const sections = [];
485
+ for (const region of Object.values(page.layout)) {
486
+ if (Array.isArray(region)) {
487
+ sections.push(...collectSections(filterVisibleNodesRecursive2(region, visibilityContext), sectionType));
488
+ }
489
+ }
490
+ const content = sections.map((section) => {
491
+ const props = "props" in section ? section.props ?? {} : {};
492
+ const value = propKeys.map((key) => props[key]).find((v) => typeof v === "string" && v);
493
+ return typeof value === "string" ? value : "";
494
+ }).filter(Boolean).join("\n\n");
495
+ if (content) return new Response(content, { headers: TEXT_PLAIN });
496
+ }
497
+ } catch {
498
+ }
499
+ return new Response(defaultContent, { headers: TEXT_PLAIN });
500
+ }
501
+ function defaultRobotsContent(environment = environmentFromEnv()) {
502
+ return environment === "production" ? "User-agent: *\nAllow: /\n" : "User-agent: *\nDisallow: /\n";
503
+ }
504
+
505
+ // src/locale-page.tsx
506
+ import { notFound, permanentRedirect, redirect } from "next/navigation";
507
+ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
508
+ async function pageSlugFromParams(params) {
509
+ const slug = (await params).slug ?? [];
510
+ return slug.length > 0 ? slug.join("/") : "index";
511
+ }
512
+ function createLocalePage(config) {
513
+ const { locale, client, componentRegistry } = config;
514
+ const folder = config.folder ? `/${config.folder.replace(/^\/+|\/+$/g, "")}` : "";
515
+ const scope = scopeFromEnv({ locale, localePrefix: folder.replace(/^\//, ""), ...config.scope });
516
+ const resolvePage2 = createPageResolver(client, config.resolveOptions);
517
+ const sendRedirect = config.redirect === "temporary" ? redirect : permanentRedirect;
518
+ async function generateStaticParams() {
519
+ const response = await client.getPageSlugList({ locale, limit: config.staticParamsLimit ?? 1e3 });
520
+ return (response.data ?? []).map((path) => ({ slug: path ? path.split("/") : [] }));
521
+ }
522
+ async function generateMetadata({ params }) {
523
+ const resolved = await resolvePage2(await pageSlugFromParams(params), locale);
524
+ if (resolved.kind !== "page") return {};
525
+ return buildPageMetadata(resolved.pageData, { siteUrl: config.siteUrl, folder });
526
+ }
527
+ async function Page({ params }) {
528
+ const slug = await pageSlugFromParams(params);
529
+ const resolved = await resolvePage2(slug, locale);
530
+ if (resolved.kind === "error") {
531
+ const mode = config.resolveErrors ?? (process.env.NODE_ENV === "development" ? "render" : "throw");
532
+ if (mode === "throw") {
533
+ throw new Error(`@botlr/next: could not resolve "${slug}" (${locale}): ${resolved.message}`, {
534
+ cause: resolved.cause
535
+ });
536
+ }
537
+ if (config.renderError) return /* @__PURE__ */ jsx2(Fragment2, { children: config.renderError(resolved.message) });
538
+ return errorBox("Error Loading Page", resolved.message);
539
+ }
540
+ if (resolved.kind === "404") notFound();
541
+ if (resolved.kind === "redirect") sendRedirect(`${folder}${resolved.destination}`);
542
+ const { pageData } = resolved;
543
+ if (pageData.status !== "published") notFound();
544
+ return renderCmsPage({
545
+ pageData,
546
+ scope,
547
+ componentRegistry,
548
+ mode: "published",
549
+ locale,
550
+ environment: config.environment ?? environmentFromEnv(),
551
+ client
552
+ });
553
+ }
554
+ return { generateStaticParams, generateMetadata, Page, resolvePage: resolvePage2, scope };
555
+ }
556
+ function createLocaleAliasPage(targetFolder) {
557
+ return async function AliasPage({ params }) {
558
+ const slug = (await params).slug ?? [];
559
+ permanentRedirect(`${targetFolder}/${slug.join("/")}`);
560
+ };
561
+ }
562
+
563
+ // src/rate-limit.ts
564
+ var store = /* @__PURE__ */ new Map();
565
+ function rateLimit(key, config) {
566
+ const now = Date.now();
567
+ const windowMs = config.window * 1e3;
568
+ let entry = store.get(key);
569
+ if (!entry || now >= entry.resetAt) {
570
+ entry = { count: 1, resetAt: now + windowMs };
571
+ store.set(key, entry);
572
+ return { allowed: true, remaining: config.limit - 1, resetAt: entry.resetAt };
573
+ }
574
+ entry.count++;
575
+ const allowed = entry.count <= config.limit;
576
+ return {
577
+ allowed,
578
+ remaining: Math.max(0, config.limit - entry.count),
579
+ resetAt: entry.resetAt
580
+ };
581
+ }
582
+ function getClientIp(request) {
583
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown";
584
+ }
585
+
60
586
  // src/index.ts
61
587
  export * from "@botlr/react";
62
- import { BotlrError, BotlrConflictError, createBotlrClient as createBotlrClient2 } from "@botlr/sdk";
588
+ import {
589
+ BotlrError,
590
+ BotlrConflictError,
591
+ createBotlrClient as createBotlrClient2,
592
+ getAssetUrl,
593
+ createAssetUrl,
594
+ getAssetId,
595
+ getAssetFilename,
596
+ isAssetData
597
+ } from "@botlr/sdk";
63
598
  export {
64
599
  BOTLR_ENV,
65
600
  BotlrConflictError,
66
601
  BotlrError,
67
602
  DEFAULT_API_URL,
603
+ buildPageMetadata,
604
+ collectSharedSectionIds,
68
605
  configFromEnv,
606
+ createAssetUrl,
69
607
  createBotlrClient2 as createBotlrClient,
70
608
  createBotlrFromEnv,
609
+ createLocaleAliasPage,
610
+ createLocalePage,
611
+ createPageResolver,
612
+ defaultRobotsContent,
613
+ environmentFromEnv,
614
+ getAssetFilename,
615
+ getAssetId,
616
+ getAssetUrl,
71
617
  getBotlrForRoute,
72
618
  getCachedBotlr,
73
- localeFromAcceptLanguage
619
+ getClientIp,
620
+ isAssetData,
621
+ localeFromAcceptLanguage,
622
+ mergeTemplateLayout,
623
+ pageSlugFromParams,
624
+ prepareTemplate,
625
+ rateLimit,
626
+ renderCmsPage,
627
+ resolvePage,
628
+ scopeFromEnv,
629
+ serveStaticTextFile,
630
+ themeSettingsToCss
74
631
  };
75
632
  //# sourceMappingURL=index.js.map