@postedin/cms-client 0.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.
Files changed (73) hide show
  1. package/README.md +66 -0
  2. package/bin/dissect/cli.mjs +138 -0
  3. package/bin/dissect/dissect.mjs +290 -0
  4. package/bin/profile/build.mjs +106 -0
  5. package/bin/profile/fetch-log.mjs +298 -0
  6. package/bin/profile/format.mjs +90 -0
  7. package/bin/profile/interference-summary.mjs +558 -0
  8. package/bin/profile/interference.mjs +604 -0
  9. package/bin/profile/measure.mjs +137 -0
  10. package/bin/profile/report.mjs +90 -0
  11. package/bin/profile/site-env.mjs +16 -0
  12. package/bin/profile/summarize.mjs +429 -0
  13. package/dist/browser.d.ts +145 -0
  14. package/dist/browser.js +11 -0
  15. package/dist/browser.js.map +1 -0
  16. package/dist/chunk-6V54ITTK.js +197 -0
  17. package/dist/chunk-6V54ITTK.js.map +1 -0
  18. package/dist/chunk-MNZ7DIGC.js +51 -0
  19. package/dist/chunk-MNZ7DIGC.js.map +1 -0
  20. package/dist/form-proxy/upload-policy.d.ts +40 -0
  21. package/dist/form-proxy/upload-policy.js +17 -0
  22. package/dist/form-proxy/upload-policy.js.map +1 -0
  23. package/dist/index.d.ts +570 -0
  24. package/dist/index.js +1636 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/payload-types.d.ts +8985 -0
  27. package/dist/payload-types.js +1 -0
  28. package/dist/payload-types.js.map +1 -0
  29. package/package.json +74 -0
  30. package/src/api.ts +387 -0
  31. package/src/blog-listing.ts +75 -0
  32. package/src/browser.ts +24 -0
  33. package/src/client.ts +144 -0
  34. package/src/cms-to-href.ts +70 -0
  35. package/src/cms.ts +86 -0
  36. package/src/collections/appearance.ts +94 -0
  37. package/src/collections/areas.ts +29 -0
  38. package/src/collections/authors.ts +27 -0
  39. package/src/collections/banners.ts +14 -0
  40. package/src/collections/categories.ts +111 -0
  41. package/src/collections/forms.ts +29 -0
  42. package/src/collections/header-footer.ts +19 -0
  43. package/src/collections/image-links.ts +14 -0
  44. package/src/collections/media.ts +18 -0
  45. package/src/collections/options.ts +10 -0
  46. package/src/collections/pages.ts +83 -0
  47. package/src/collections/posts.ts +249 -0
  48. package/src/collections/project.ts +16 -0
  49. package/src/collections/questions.ts +35 -0
  50. package/src/collections/seo.ts +10 -0
  51. package/src/collections/tags.ts +25 -0
  52. package/src/collections/team-members.ts +79 -0
  53. package/src/config-time.ts +98 -0
  54. package/src/context.ts +12 -0
  55. package/src/decode-html.ts +8 -0
  56. package/src/form-proxy/cms-client.ts +95 -0
  57. package/src/form-proxy/cms-errors.ts +73 -0
  58. package/src/form-proxy/cms-write.ts +44 -0
  59. package/src/form-proxy/http.ts +96 -0
  60. package/src/form-proxy/index.ts +73 -0
  61. package/src/form-proxy/rate-limit.ts +46 -0
  62. package/src/form-proxy/submissions.ts +88 -0
  63. package/src/form-proxy/types.ts +23 -0
  64. package/src/form-proxy/upload-policy.ts +92 -0
  65. package/src/form-proxy/uploads.ts +81 -0
  66. package/src/home-page.ts +83 -0
  67. package/src/index.ts +68 -0
  68. package/src/loader.ts +83 -0
  69. package/src/locales.ts +80 -0
  70. package/src/payload-types.ts +10854 -0
  71. package/src/placeholder.ts +9 -0
  72. package/src/resolve-menu-items.ts +184 -0
  73. package/src/routes.ts +184 -0
package/dist/index.js ADDED
@@ -0,0 +1,1636 @@
1
+ import {
2
+ defineCmsToHref,
3
+ defineLocales,
4
+ defineRoutes
5
+ } from "./chunk-6V54ITTK.js";
6
+ import {
7
+ MAX_REQUEST_BYTES,
8
+ MAX_UPLOAD_BYTES
9
+ } from "./chunk-MNZ7DIGC.js";
10
+
11
+ // src/api.ts
12
+ import { stringify } from "qs-esm";
13
+
14
+ // src/placeholder.ts
15
+ function getRandomPlaceholderUrl(width = 600, height = 400) {
16
+ const pastelColors = ["ebfaf5", "c3c8cd", "faf0e6", "f5f5eb", "f0ebfa"];
17
+ const randomIndex = Math.floor(Math.random() * pastelColors.length);
18
+ const randomColor = pastelColors[randomIndex];
19
+ return `https://placehold.co/${width}x${height}/${randomColor}/${randomColor}`;
20
+ }
21
+
22
+ // src/api.ts
23
+ var CMS_API_KEY_COLLECTION = "api-keys";
24
+ var DEPTH = 2;
25
+ var LIMIT = 1e3;
26
+ var PAGE_CONCURRENCY = 4;
27
+ async function mapWithConcurrency(items, limit, worker) {
28
+ const results = new Array(items.length);
29
+ let next = 0;
30
+ const runner = async () => {
31
+ while (next < items.length) {
32
+ const index = next++;
33
+ results[index] = await worker(items[index], index);
34
+ }
35
+ };
36
+ await Promise.all(
37
+ Array.from({ length: Math.min(limit, items.length) }, runner)
38
+ );
39
+ return results;
40
+ }
41
+ function applyLocale(query, locale, fallback) {
42
+ if (!locale) {
43
+ return query;
44
+ }
45
+ query.locale = locale;
46
+ if (fallback === "null") {
47
+ query["fallback-locale"] = "null";
48
+ }
49
+ return query;
50
+ }
51
+ function createApi(options) {
52
+ const pageConcurrency = options.pageConcurrency ?? PAGE_CONCURRENCY;
53
+ function buildHeaders() {
54
+ const headers = {
55
+ Authorization: `${CMS_API_KEY_COLLECTION} API-Key ${options.apiKey}`
56
+ };
57
+ if (options.protectionBypassSecret) {
58
+ headers["x-vercel-protection-bypass"] = options.protectionBypassSecret;
59
+ }
60
+ return headers;
61
+ }
62
+ function apiUrl(url, query = {}, scoped = true) {
63
+ if (scoped) {
64
+ query.where = {
65
+ and: [
66
+ { "project.slug": { equals: options.projectSlug } },
67
+ ...query.where ? [query.where] : []
68
+ ]
69
+ };
70
+ }
71
+ return options.apiUrl + url + stringify(query, { addQueryPrefix: true });
72
+ }
73
+ function getUploadsPublicUrl(endpoint) {
74
+ return `${options.uploadsBaseUrl}/${endpoint}`;
75
+ }
76
+ const hasUploadsBucket = !!options.uploadsBaseUrl;
77
+ function siteUploadUrl(media, use) {
78
+ return options.resolveUploadUrl?.(media, use) ?? void 0;
79
+ }
80
+ function fileUrl(media) {
81
+ if (!media) {
82
+ return null;
83
+ }
84
+ if (options.isDev) {
85
+ return media.url ?? null;
86
+ }
87
+ const own = siteUploadUrl(media, "file");
88
+ if (own !== void 0) {
89
+ return own;
90
+ }
91
+ if (hasUploadsBucket && media.filename) {
92
+ return getUploadsPublicUrl(media.filename);
93
+ }
94
+ return media.url ?? null;
95
+ }
96
+ function imageUrl(media) {
97
+ if (!media) {
98
+ return getRandomPlaceholderUrl();
99
+ }
100
+ if (options.isDev) {
101
+ return media.url ?? getRandomPlaceholderUrl();
102
+ }
103
+ const own = siteUploadUrl(media, "image");
104
+ if (own !== void 0) {
105
+ return own ?? getRandomPlaceholderUrl();
106
+ }
107
+ if (hasUploadsBucket && media.filename) {
108
+ return getUploadsPublicUrl(media.filename);
109
+ }
110
+ return media.url || getRandomPlaceholderUrl();
111
+ }
112
+ async function fetchCmsFile(url) {
113
+ return await fetch(new URL(url, options.apiUrl), {
114
+ headers: buildHeaders()
115
+ });
116
+ }
117
+ async function fetchCmsGlobal(global, fetchOptions = {}) {
118
+ const query = fetchOptions.query || {};
119
+ const depth = fetchOptions.depth ?? DEPTH;
120
+ applyLocale(query, fetchOptions.locale, fetchOptions.fallback ?? "null");
121
+ const res = await fetch(
122
+ apiUrl(`/api/globals/${global}`, { depth, ...query }),
123
+ { headers: buildHeaders() }
124
+ );
125
+ return await res.json();
126
+ }
127
+ async function fetchCmsGlobalCollection(collection, fetchOptions = {}) {
128
+ const query = fetchOptions.query || {};
129
+ const depth = fetchOptions.depth ?? DEPTH;
130
+ applyLocale(query, fetchOptions.locale, fetchOptions.fallback ?? "null");
131
+ const res = await fetch(apiUrl(`/api/${collection}`, { depth, ...query }), {
132
+ headers: buildHeaders()
133
+ });
134
+ if (!res.ok) {
135
+ const url = apiUrl(`/api/${collection}`, { depth, ...query });
136
+ const text = await res.text();
137
+ let error;
138
+ try {
139
+ const json = JSON.parse(text);
140
+ error = `${res.status} (${url})j: ${json.message || JSON.stringify(json)}`;
141
+ } catch {
142
+ error = `${res.status} (${url})t: ${text}`;
143
+ }
144
+ throw new Error(error);
145
+ }
146
+ const docs = (await res.json()).docs;
147
+ if (!docs.length) {
148
+ throw new Error(
149
+ `Failed to load global collection "${collection}", no docs found.`
150
+ );
151
+ }
152
+ return docs[0];
153
+ }
154
+ async function fetchCmsCollection(collection, fetchOptions = {}) {
155
+ const query = fetchOptions.query || {};
156
+ const depth = fetchOptions.depth ?? DEPTH;
157
+ const limit = fetchOptions.limit || LIMIT;
158
+ const sort = fetchOptions.sort || "-createdAt";
159
+ const fields = query.fields || {};
160
+ if (fetchOptions.status) {
161
+ query.where = query?.where || {};
162
+ query.where._status = { equals: fetchOptions.status };
163
+ }
164
+ applyLocale(query, fetchOptions.locale, fetchOptions.fallback ?? "null");
165
+ const fetchPage = async (page = 1) => {
166
+ const res = await fetch(
167
+ apiUrl(`/api/${collection}`, {
168
+ depth,
169
+ limit,
170
+ sort,
171
+ fields,
172
+ ...query,
173
+ page
174
+ }),
175
+ { headers: buildHeaders() }
176
+ );
177
+ if (!res.ok) {
178
+ const errorText = await res.text();
179
+ throw new Error(
180
+ `CMS API request failed for ${collection} (page ${page})
181
+ Status: ${res.status} ${res.statusText}
182
+ Details: ${errorText.substring(0, 500)}${errorText.length > 500 ? "..." : ""}`
183
+ );
184
+ }
185
+ return await res.json();
186
+ };
187
+ const firstPage = await fetchPage();
188
+ let otherPagesData = [];
189
+ if (firstPage.totalPages > 1) {
190
+ const otherPages = Array.from(
191
+ { length: firstPage.totalPages - 1 },
192
+ (_, i) => i + 2
193
+ );
194
+ otherPagesData = await mapWithConcurrency(
195
+ otherPages,
196
+ pageConcurrency,
197
+ async (p) => (await fetchPage(p)).docs
198
+ );
199
+ }
200
+ return firstPage.docs.concat(otherPagesData.flat());
201
+ }
202
+ async function fetchCmsProject() {
203
+ const slug = options.projectSlug;
204
+ const res = await fetch(
205
+ apiUrl(
206
+ "/api/projects",
207
+ { limit: 1, where: { slug: { equals: slug } } },
208
+ false
209
+ ),
210
+ { headers: buildHeaders() }
211
+ );
212
+ if (!res.ok) {
213
+ const errorText = await res.text();
214
+ throw new Error(
215
+ `CMS API request failed for ProjectBySlug
216
+ Status: ${res.status} ${res.statusText}
217
+ Details: ${errorText.substring(0, 500)}${errorText.length > 500 ? "..." : ""}`
218
+ );
219
+ }
220
+ const docs = (await res.json()).docs;
221
+ if (!docs) {
222
+ throw new Error(
223
+ `CMS API request failed for ProjectBySlug
224
+ Status: ${res.status} ${res.statusText}
225
+ Details: failed to find project by slug "${slug}"`
226
+ );
227
+ }
228
+ return docs[0];
229
+ }
230
+ return {
231
+ apiUrl,
232
+ buildHeaders,
233
+ getUploadsPublicUrl,
234
+ fileUrl,
235
+ imageUrl,
236
+ fetchCmsFile,
237
+ fetchCmsGlobal,
238
+ fetchCmsGlobalCollection,
239
+ fetchCmsCollection,
240
+ fetchCmsProject
241
+ };
242
+ }
243
+
244
+ // src/blog-listing.ts
245
+ var EMPTY = { name: null, description: null };
246
+ async function fetchBlogListing(ctx, locale) {
247
+ const options = await ctx.cms(locale).options();
248
+ const configured = options.blog?.page;
249
+ const id = typeof configured === "string" ? configured : configured?.id ?? null;
250
+ if (!id) {
251
+ return EMPTY;
252
+ }
253
+ const [doc] = await ctx.api.fetchCmsCollection("pages", {
254
+ locale,
255
+ fallback: "default",
256
+ status: "published",
257
+ limit: 1,
258
+ query: { where: { id: { equals: id } } }
259
+ });
260
+ if (!doc) {
261
+ return EMPTY;
262
+ }
263
+ return {
264
+ name: doc.listingName || null,
265
+ description: doc.listingDescription ?? null
266
+ };
267
+ }
268
+ function createBlogListing(ctx) {
269
+ const cache = /* @__PURE__ */ new Map();
270
+ return function getBlogListing(locale) {
271
+ let pending = cache.get(locale);
272
+ if (!pending) {
273
+ pending = fetchBlogListing(ctx, locale);
274
+ cache.set(locale, pending);
275
+ }
276
+ return pending;
277
+ };
278
+ }
279
+
280
+ // src/loader.ts
281
+ function createGlobalLoader(fetchFn, options) {
282
+ let cache;
283
+ return async () => {
284
+ if (!cache) {
285
+ cache = fetchFn().then(async (data) => {
286
+ if (options?.transform) {
287
+ return await options.transform(data);
288
+ }
289
+ return data;
290
+ });
291
+ }
292
+ return await cache;
293
+ };
294
+ }
295
+ function createCollectionLoader(fetchFn, options) {
296
+ let cache;
297
+ const load = async () => {
298
+ if (!cache) {
299
+ cache = fetchFn().then(async (data) => {
300
+ if (options?.transform) {
301
+ return await options.transform(data);
302
+ }
303
+ return data;
304
+ });
305
+ }
306
+ return await cache;
307
+ };
308
+ return Object.assign(load, {
309
+ page: async (page, limit, options2 = { excluded: [] }) => {
310
+ const data = (await load()).filter((doc) => {
311
+ if (!options2.excluded.length) {
312
+ return true;
313
+ }
314
+ return !options2.excluded.find((ex) => ex.id === doc.id);
315
+ });
316
+ const first = (page - 1) * limit;
317
+ const last = first + limit;
318
+ return data.slice(first, last);
319
+ },
320
+ find: async (predicate) => {
321
+ return (await load()).find(predicate);
322
+ },
323
+ filter: async (predicate) => {
324
+ return (await load()).filter(predicate);
325
+ }
326
+ });
327
+ }
328
+
329
+ // src/collections/appearance.ts
330
+ var DEFAULT_APPEARANCE_FALLBACKS = {
331
+ // The theme ramp's 500.
332
+ brandColor: "var(--color-theme-500)",
333
+ backgroundColor: {
334
+ lightTheme: "var(--site-background-light)",
335
+ darkTheme: "var(--site-background-dark)"
336
+ }
337
+ };
338
+ function removeInvalid(obj) {
339
+ if (obj === null || obj === void 0) {
340
+ return {};
341
+ }
342
+ const cleaned = {};
343
+ for (const [key, value] of Object.entries(obj)) {
344
+ if (value !== null && value !== void 0 && value !== "") {
345
+ cleaned[key] = value;
346
+ }
347
+ }
348
+ return cleaned;
349
+ }
350
+ function appearance_default(ctx) {
351
+ return createGlobalLoader(
352
+ async () => {
353
+ if (!ctx.options.appearance) {
354
+ throw new Error(
355
+ "cms.appearance() needs the `appearance` option of createClient: the Appearance document fetched at config time."
356
+ );
357
+ }
358
+ return ctx.options.appearance;
359
+ },
360
+ {
361
+ transform: (appearance) => {
362
+ const fallbacks = ctx.options.appearanceFallbacks ?? DEFAULT_APPEARANCE_FALLBACKS;
363
+ return {
364
+ ...appearance,
365
+ branding: {
366
+ ...appearance.branding,
367
+ brandColor: appearance.branding?.brandColor || fallbacks.brandColor,
368
+ logo: {
369
+ type: appearance.branding?.logo?.type,
370
+ dark: appearance.branding?.logo?.dark,
371
+ darkSvg: appearance.branding?.logo?.darkSvg,
372
+ light: appearance.branding?.logo?.light,
373
+ lightSvg: appearance.branding?.logo?.lightSvg
374
+ },
375
+ icon: appearance.branding?.icon
376
+ },
377
+ backgroundColor: {
378
+ ...fallbacks.backgroundColor,
379
+ ...removeInvalid(appearance.backgroundColor)
380
+ }
381
+ };
382
+ }
383
+ }
384
+ );
385
+ }
386
+
387
+ // src/collections/areas.ts
388
+ function areas_default(ctx, locale) {
389
+ const areas = createCollectionLoader(
390
+ () => ctx.api.fetchCmsCollection("areas", {
391
+ sort: "_order",
392
+ locale
393
+ }),
394
+ {
395
+ transform: (areas2) => areas2.map((area) => ({
396
+ ...area,
397
+ page: typeof area.page === "object" ? area.page : null
398
+ }))
399
+ }
400
+ );
401
+ return {
402
+ all: areas
403
+ };
404
+ }
405
+
406
+ // src/collections/authors.ts
407
+ function authors_default(ctx, locale) {
408
+ const authors = createCollectionLoader(
409
+ async () => ctx.api.fetchCmsCollection("authors", { locale }),
410
+ {
411
+ transform: (authors2) => {
412
+ const validAuthors = authors2.filter((author) => author.slug);
413
+ return validAuthors;
414
+ }
415
+ }
416
+ );
417
+ return {
418
+ all: authors,
419
+ findBySlug: async (slug) => {
420
+ return authors.find((author) => author.slug === slug);
421
+ }
422
+ };
423
+ }
424
+
425
+ // src/collections/banners.ts
426
+ function banners_default(ctx, locale) {
427
+ const banners = createCollectionLoader(
428
+ async () => ctx.api.fetchCmsCollection("banners", { locale })
429
+ );
430
+ return {
431
+ all: banners
432
+ };
433
+ }
434
+
435
+ // src/decode-html.ts
436
+ function decodeHtml(str) {
437
+ return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
438
+ }
439
+
440
+ // src/collections/categories.ts
441
+ function getCategoryPath(category) {
442
+ if (!category.parent) {
443
+ return category.slug;
444
+ }
445
+ return `${getCategoryPath(category.parent)}/${category.slug}`;
446
+ }
447
+ function getCategoryBreadcrumbs(category, locale, prefixPath) {
448
+ const ancestors = category.parent ? getCategoryBreadcrumbs(category.parent, locale, prefixPath) : [];
449
+ const path = getCategoryPath(category);
450
+ return [
451
+ ...ancestors,
452
+ {
453
+ label: category.title,
454
+ url: prefixPath(`/blog/category/${path}`, locale)
455
+ }
456
+ ];
457
+ }
458
+ function categories_default(ctx, locale) {
459
+ const categories = createCollectionLoader(
460
+ async () => ctx.api.fetchCmsCollection("categories", { locale }),
461
+ {
462
+ transform: (categories2) => {
463
+ const catMap = new Map(
464
+ categories2.filter((category) => category.slug).map((category) => [
465
+ category.id,
466
+ {
467
+ ...category,
468
+ title: decodeHtml(category.title),
469
+ children: [],
470
+ path: category.slug,
471
+ breadcrumbs: []
472
+ }
473
+ ])
474
+ );
475
+ const wireRelationships = (category) => {
476
+ if (typeof category.parent === "string") {
477
+ const parent = catMap.get(category.parent);
478
+ if (parent) {
479
+ category.parent = parent;
480
+ parent.children.push(category);
481
+ }
482
+ }
483
+ };
484
+ catMap.forEach((category) => wireRelationships(category));
485
+ catMap.forEach((category) => {
486
+ category.path = getCategoryPath(category);
487
+ category.breadcrumbs = getCategoryBreadcrumbs(
488
+ category,
489
+ locale,
490
+ ctx.locales.prefixPath
491
+ );
492
+ });
493
+ return Array.from(catMap.values());
494
+ }
495
+ }
496
+ );
497
+ return {
498
+ all: categories,
499
+ wire: async (category) => {
500
+ if (!category) {
501
+ return null;
502
+ }
503
+ const id = typeof category === "string" ? category : category.id;
504
+ return (await categories()).find((cat) => cat.id === id);
505
+ }
506
+ };
507
+ }
508
+
509
+ // src/collections/forms.ts
510
+ function forms_default(ctx, locale) {
511
+ const forms = createCollectionLoader(
512
+ () => ctx.api.fetchCmsCollection("forms", { locale, fallback: "default" })
513
+ );
514
+ const findById = async (id) => await forms.find((form) => form.id === id) ?? null;
515
+ return {
516
+ all: forms,
517
+ findById,
518
+ // Swaps a form reference coming from a block for its localized document
519
+ resolve: async (form) => {
520
+ if (typeof form === "string") {
521
+ return await findById(form);
522
+ }
523
+ return await findById(form.id) ?? form;
524
+ }
525
+ };
526
+ }
527
+
528
+ // src/collections/header-footer.ts
529
+ function header_footer_default(ctx, locale) {
530
+ return createGlobalLoader(
531
+ async () => ctx.api.fetchCmsGlobalCollection("header-footer", { locale })
532
+ );
533
+ }
534
+
535
+ // src/collections/image-links.ts
536
+ function image_links_default(ctx, locale) {
537
+ const imageLinks = createCollectionLoader(
538
+ async () => ctx.api.fetchCmsCollection("image-links", { locale })
539
+ );
540
+ return {
541
+ all: imageLinks
542
+ };
543
+ }
544
+
545
+ // src/collections/media.ts
546
+ function media_default(ctx, locale) {
547
+ const media = createCollectionLoader(
548
+ async () => ctx.api.fetchCmsCollection("media", { locale, depth: 0 })
549
+ );
550
+ return {
551
+ all: media
552
+ };
553
+ }
554
+
555
+ // src/collections/options.ts
556
+ function options_default(ctx, locale) {
557
+ return createGlobalLoader(
558
+ async () => ctx.api.fetchCmsGlobalCollection("options", { locale })
559
+ );
560
+ }
561
+
562
+ // src/collections/pages.ts
563
+ function pages_default(ctx, locale) {
564
+ const pages = createCollectionLoader(
565
+ async () => ctx.api.fetchCmsCollection("pages", {
566
+ status: "published",
567
+ locale
568
+ }),
569
+ {
570
+ transform: (rawPages) => {
571
+ const pageMap = new Map(
572
+ rawPages.filter((page) => page.slug).map((page) => {
573
+ const path = (page.breadcrumbs?.at(-1)?.url ?? page.slug).replace(
574
+ /^\//,
575
+ ""
576
+ );
577
+ return [page.id, { ...page, children: [], path }];
578
+ })
579
+ );
580
+ pageMap.forEach((page) => {
581
+ const rawParent = page.parent;
582
+ const parentId = typeof rawParent === "string" ? rawParent : rawParent?.id;
583
+ if (parentId) {
584
+ const parent = pageMap.get(parentId);
585
+ if (parent) {
586
+ page.parent = parent;
587
+ parent.children.push(page);
588
+ } else {
589
+ page.parent = void 0;
590
+ }
591
+ }
592
+ });
593
+ pageMap.forEach((page) => {
594
+ page.children.sort((a, b) => {
595
+ if (a.order == null && b.order == null) {
596
+ return 0;
597
+ }
598
+ if (a.order == null) {
599
+ return 1;
600
+ }
601
+ if (b.order == null) {
602
+ return -1;
603
+ }
604
+ return a.order - b.order;
605
+ });
606
+ });
607
+ return Array.from(pageMap.values());
608
+ }
609
+ }
610
+ );
611
+ return {
612
+ all: pages,
613
+ findByPath: async (path) => (await pages()).find((p) => p.path === path),
614
+ findById: async (id) => (await pages()).find((p) => p.id === id) ?? null
615
+ };
616
+ }
617
+
618
+ // src/collections/posts.ts
619
+ import { TfIdf } from "natural/lib/natural/tfidf/index.js";
620
+ async function wirePost(ctx, post, locale, tags = []) {
621
+ if (!post) {
622
+ return void 0;
623
+ }
624
+ let coverImage = post.coverImage;
625
+ if (typeof coverImage === "string") {
626
+ coverImage = void 0;
627
+ }
628
+ const postTags = post.tags?.map(
629
+ (tag) => typeof tag === "string" ? tags.find((t) => t.id === tag) : tag
630
+ ).filter((tag) => tag !== void 0);
631
+ const cms = ctx.cms(locale);
632
+ const secondaryCategories = (await Promise.all(
633
+ (post.secondaryCategories ?? []).map(
634
+ (c) => cms.categories.wire(c)
635
+ )
636
+ )).filter((c) => c != null);
637
+ const category = await cms.categories.wire(post.category);
638
+ const categoryTitles = [category, ...secondaryCategories].filter((c) => c != null).map((c) => c.title);
639
+ return {
640
+ ...post,
641
+ category,
642
+ secondaryCategories,
643
+ coverImage,
644
+ tags: postTags,
645
+ searchTokens: `${post.title} ${categoryTitles.join(" ")}`
646
+ };
647
+ }
648
+ function collectCustomTeamMemberIds(post) {
649
+ const ids = [];
650
+ for (const block of post.contentBlocks ?? []) {
651
+ if (block.blockType !== "team-member") {
652
+ continue;
653
+ }
654
+ if (block.filter !== "custom") {
655
+ continue;
656
+ }
657
+ for (const m of block.members ?? []) {
658
+ const id = typeof m === "string" ? m : m?.id;
659
+ if (id) {
660
+ ids.push(id);
661
+ }
662
+ }
663
+ }
664
+ return ids;
665
+ }
666
+ function posts_default(ctx, locale) {
667
+ const tfIdf = new TfIdf();
668
+ const keyedPosts = {};
669
+ let teamMemberIndex = null;
670
+ const posts = createCollectionLoader(
671
+ async () => ctx.api.fetchCmsCollection("posts", {
672
+ status: "published",
673
+ sort: "-publishDate",
674
+ locale
675
+ }) || [],
676
+ {
677
+ transform: async (posts2) => {
678
+ const cms = ctx.cms(locale);
679
+ const tags = await cms.tags.all();
680
+ const wiredPosts = await Promise.all(
681
+ posts2?.filter((post) => post.slug).map(async (post) => {
682
+ return wirePost(ctx, post, locale, tags);
683
+ }) || []
684
+ );
685
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
686
+ wiredPosts.forEach((post) => {
687
+ if (post.publishDate && (new Date(post.publishDate).getFullYear() > currentYear - 4 || tfIdf.documents.length < 200)) {
688
+ tfIdf.addDocument(post.searchTokens, post.id);
689
+ keyedPosts[post.id] = post;
690
+ }
691
+ });
692
+ return wiredPosts;
693
+ }
694
+ }
695
+ );
696
+ return {
697
+ all: posts,
698
+ exists: async () => !!(await posts()).length,
699
+ page: async (...args) => {
700
+ return posts.page(...args);
701
+ },
702
+ latest: async () => {
703
+ return posts.page(1, 6);
704
+ },
705
+ featured: async () => {
706
+ return (await posts()).filter((post) => post.featured).slice(0, 10);
707
+ },
708
+ // TF-IDF similarity over title + category titles. When similarity
709
+ // yields fewer than `limit` posts, pad with the latest posts from the
710
+ // same category (newest first, secondary categories count too).
711
+ related: async (post, limit = 6) => {
712
+ const scores = [];
713
+ tfIdf.tfidfs(post.searchTokens, (_, score, postId) => {
714
+ scores.push({
715
+ postId,
716
+ score
717
+ });
718
+ });
719
+ const similar = scores.filter((entry) => entry.postId !== post.id && entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => keyedPosts[entry.postId]);
720
+ if (similar.length >= limit) {
721
+ return similar;
722
+ }
723
+ const categoryId = post.category?.id;
724
+ const seen = new Set(similar.map((p) => p.id));
725
+ const fromCategory = await posts.filter(
726
+ (p) => p.id !== post.id && !seen.has(p.id) && (!categoryId || p.category?.id === categoryId || (p.secondaryCategories ?? []).some(
727
+ (c) => c.id === categoryId
728
+ ))
729
+ );
730
+ return [...similar, ...fromCategory.slice(0, limit - similar.length)];
731
+ },
732
+ findBySlug: async (slug) => {
733
+ return posts.find((post) => post.slug === slug);
734
+ },
735
+ findByAuthor: async (author) => {
736
+ return posts.filter((post) => post.author?.id === author.id);
737
+ },
738
+ findByTeamMember: async (memberId) => {
739
+ if (!teamMemberIndex) {
740
+ teamMemberIndex = (async () => {
741
+ const all = await posts();
742
+ const index = /* @__PURE__ */ new Map();
743
+ for (const post of all) {
744
+ for (const id of collectCustomTeamMemberIds(post)) {
745
+ const list = index.get(id);
746
+ if (list) {
747
+ list.push(post);
748
+ } else {
749
+ index.set(id, [post]);
750
+ }
751
+ }
752
+ }
753
+ for (const list of index.values()) {
754
+ list.sort(
755
+ (a, b) => new Date(b.publishDate ?? 0).getTime() - new Date(a.publishDate ?? 0).getTime()
756
+ );
757
+ }
758
+ return index;
759
+ })();
760
+ }
761
+ return (await teamMemberIndex).get(memberId) ?? [];
762
+ },
763
+ findByCategory: async (category) => {
764
+ return posts.filter(
765
+ (post) => post.category === category.id || post.category?.id === category.id || (post.secondaryCategories ?? []).some(
766
+ (c) => c.id === category.id
767
+ )
768
+ );
769
+ },
770
+ findByTag: async (tag) => {
771
+ return posts.filter((post) => {
772
+ return post.tags?.some((postTag) => postTag.id === tag.id);
773
+ });
774
+ },
775
+ nextInCategory: async (post) => {
776
+ if (!post.category) {
777
+ return void 0;
778
+ }
779
+ const allCategoryIds = /* @__PURE__ */ new Set([
780
+ post.category.id,
781
+ ...(post.secondaryCategories ?? []).map((c) => c.id)
782
+ ]);
783
+ const categoryPosts = await posts.filter(
784
+ (p) => allCategoryIds.has(p.category?.id ?? "") || (p.secondaryCategories ?? []).some(
785
+ (c) => allCategoryIds.has(c.id)
786
+ )
787
+ );
788
+ const idx = categoryPosts.findIndex((p) => p.id === post.id);
789
+ return idx >= 0 && idx + 1 < categoryPosts.length ? categoryPosts[idx + 1] : void 0;
790
+ }
791
+ };
792
+ }
793
+
794
+ // src/collections/project.ts
795
+ function project_default(ctx) {
796
+ return createGlobalLoader(async () => {
797
+ if (!ctx.options.project) {
798
+ throw new Error(
799
+ "cms.project() needs the `project` option of createClient: the Project document fetched at config time."
800
+ );
801
+ }
802
+ return ctx.options.project;
803
+ });
804
+ }
805
+
806
+ // src/collections/questions.ts
807
+ function questions_default(ctx, locale) {
808
+ const questions = createCollectionLoader(
809
+ async () => ctx.api.fetchCmsCollection("questions", {
810
+ locale,
811
+ sort: "_questions_questions_order"
812
+ }),
813
+ {
814
+ transform: (questions2) => {
815
+ const validQuestions = questions2.filter((question) => question.tags);
816
+ return validQuestions;
817
+ }
818
+ }
819
+ );
820
+ return {
821
+ all: questions,
822
+ filterByTag: async (searchTag) => {
823
+ return questions.filter(
824
+ (question) => question.tags.some((tag) => {
825
+ return tag.id === searchTag.id;
826
+ })
827
+ );
828
+ }
829
+ };
830
+ }
831
+
832
+ // src/collections/seo.ts
833
+ function seo_default(ctx, locale) {
834
+ return createGlobalLoader(
835
+ async () => ctx.api.fetchCmsGlobalCollection("seo", { locale })
836
+ );
837
+ }
838
+
839
+ // src/collections/tags.ts
840
+ function tags_default(ctx, locale) {
841
+ const tags = createCollectionLoader(
842
+ async () => ctx.api.fetchCmsCollection("tags", { locale }),
843
+ {
844
+ transform: (tags2) => {
845
+ const validTags = tags2.filter((tag) => tag.slug);
846
+ return validTags;
847
+ }
848
+ }
849
+ );
850
+ return {
851
+ all: tags,
852
+ findBySlug: async (slug) => {
853
+ return tags.find((tag) => tag.slug === slug);
854
+ }
855
+ };
856
+ }
857
+
858
+ // src/collections/team-members.ts
859
+ function team_members_default(ctx, locale) {
860
+ const members = createCollectionLoader(
861
+ async () => ctx.api.fetchCmsCollection("team-members", {
862
+ sort: "_order",
863
+ locale
864
+ }),
865
+ {
866
+ transform: async (members2) => {
867
+ const allAreas = await ctx.cms(locale).areas.all();
868
+ const areaById = new Map(allAreas.map((a) => [a.id, a]));
869
+ return members2.filter((m) => m.slug).map((m) => {
870
+ const docs = [];
871
+ for (const row of m.memberAreas ?? []) {
872
+ const ref = row.area;
873
+ const id = typeof ref === "string" ? ref : ref.id;
874
+ const hit = areaById.get(id);
875
+ if (hit) {
876
+ docs.push(hit);
877
+ } else if (typeof ref === "string") {
878
+ docs.push(ref);
879
+ } else {
880
+ docs.push({
881
+ ...ref,
882
+ page: typeof ref.page === "object" ? ref.page : null
883
+ });
884
+ }
885
+ }
886
+ return {
887
+ ...m,
888
+ areas: {
889
+ docs,
890
+ hasNextPage: false,
891
+ totalDocs: docs.length
892
+ }
893
+ };
894
+ });
895
+ }
896
+ }
897
+ );
898
+ const memberTypes = createCollectionLoader(
899
+ () => ctx.api.fetchCmsCollection("member-types", {
900
+ sort: "_order",
901
+ locale
902
+ })
903
+ );
904
+ return {
905
+ all: members,
906
+ types: memberTypes,
907
+ findBySlug: async (slug) => {
908
+ return members.find((m) => m.slug === slug);
909
+ }
910
+ };
911
+ }
912
+
913
+ // src/cms.ts
914
+ function buildCms(ctx, locale) {
915
+ const categoriesService = categories_default(ctx, locale);
916
+ const postsService = posts_default(ctx, locale);
917
+ return {
918
+ locale,
919
+ areas: areas_default(ctx, locale),
920
+ authors: authors_default(ctx, locale),
921
+ banners: banners_default(ctx, locale),
922
+ appearance: appearance_default(ctx),
923
+ imageLinks: image_links_default(ctx, locale),
924
+ categories: {
925
+ ...categoriesService,
926
+ populated: async () => {
927
+ const cats = await categoriesService.all();
928
+ const allPosts = await postsService.all();
929
+ const ids = /* @__PURE__ */ new Set([
930
+ ...allPosts.map((p) => p.category?.id).filter(Boolean),
931
+ ...allPosts.flatMap(
932
+ (p) => (p.secondaryCategories ?? []).map((c) => c.id)
933
+ )
934
+ ]);
935
+ return cats.filter((c) => ids.has(c.id));
936
+ },
937
+ // Categories that hold posts, each with how many. `populated()`
938
+ // answers only whether that number is non-zero and has five callers
939
+ // depending on its shape, so this is a second reading of the same
940
+ // fold rather than a change to it. Both collections are already
941
+ // cached, so it costs no extra fetch.
942
+ withCounts: async () => {
943
+ const cats = await categoriesService.all();
944
+ const allPosts = await postsService.all();
945
+ const counts = /* @__PURE__ */ new Map();
946
+ for (const post of allPosts) {
947
+ const ids = new Set(
948
+ [
949
+ post.category?.id,
950
+ ...(post.secondaryCategories ?? []).map((c) => c.id)
951
+ ].filter((id) => !!id)
952
+ );
953
+ for (const id of ids) {
954
+ counts.set(id, (counts.get(id) ?? 0) + 1);
955
+ }
956
+ }
957
+ return cats.map((c) => ({ ...c, postCount: counts.get(c.id) ?? 0 })).filter((c) => c.postCount > 0);
958
+ }
959
+ },
960
+ forms: forms_default(ctx, locale),
961
+ headerFooter: header_footer_default(ctx, locale),
962
+ media: media_default(ctx, locale),
963
+ options: options_default(ctx, locale),
964
+ pages: pages_default(ctx, locale),
965
+ posts: postsService,
966
+ project: project_default(ctx),
967
+ questions: questions_default(ctx, locale),
968
+ seo: seo_default(ctx, locale),
969
+ tags: tags_default(ctx, locale),
970
+ teamMembers: team_members_default(ctx, locale)
971
+ };
972
+ }
973
+
974
+ // src/config-time.ts
975
+ function createRedirect(routes, from, to) {
976
+ if (!to) {
977
+ return;
978
+ }
979
+ if (to.type === "custom") {
980
+ if (!to.url) {
981
+ return;
982
+ }
983
+ return { [from]: to.url };
984
+ } else if (!to.reference) {
985
+ return;
986
+ } else if (typeof to.reference.value === "string") {
987
+ return;
988
+ }
989
+ switch (to.reference.relationTo) {
990
+ // NOTE: astro doesn't support external redirects so if this is that then it won't work
991
+ case "pages":
992
+ return to.reference.value.slug ? { [from]: routes.page(to.reference.value.slug) } : void 0;
993
+ case "categories":
994
+ return to.reference.value.slug ? { [from]: routes.blog("category", to.reference.value.slug) } : void 0;
995
+ default:
996
+ return;
997
+ }
998
+ }
999
+ function createConfigTime(api, defaultRoutes) {
1000
+ return {
1001
+ /** The three documents the Astro config and `createClient` need. */
1002
+ async fetchProjectSettings() {
1003
+ const [project, appearance, options] = await Promise.all([
1004
+ api.fetchCmsProject(),
1005
+ api.fetchCmsGlobalCollection("appearance", { depth: 2 }),
1006
+ api.fetchCmsGlobalCollection("options", { depth: 2 })
1007
+ ]);
1008
+ return { project, appearance, options };
1009
+ },
1010
+ // Redirects are emitted once at build time for the whole site. Use the
1011
+ // default locale — prefixed paths are handled by page routing, not the
1012
+ // redirect table.
1013
+ async redirects() {
1014
+ const docs = await api.fetchCmsCollection("redirects", {
1015
+ depth: 2,
1016
+ limit: 1e3
1017
+ });
1018
+ return Object.assign(
1019
+ {},
1020
+ ...docs?.map(({ from, to }) => createRedirect(defaultRoutes, from, to)).filter(Boolean) ?? []
1021
+ );
1022
+ },
1023
+ /** The favicon source file, or `false` when there is none to read. */
1024
+ async fetchIconBuffer(icon) {
1025
+ if (icon.url) {
1026
+ const response = await fetch(
1027
+ icon.url.startsWith("/") ? api.apiUrl(icon.url, {}, false) : icon.url,
1028
+ { headers: api.buildHeaders() }
1029
+ );
1030
+ if (response.ok) {
1031
+ return Buffer.from(await response.arrayBuffer());
1032
+ }
1033
+ }
1034
+ return false;
1035
+ }
1036
+ };
1037
+ }
1038
+
1039
+ // src/form-proxy/rate-limit.ts
1040
+ function createRateLimiter({
1041
+ limit,
1042
+ windowMs,
1043
+ now = Date.now
1044
+ }) {
1045
+ const windows = /* @__PURE__ */ new Map();
1046
+ function sweep(current) {
1047
+ for (const [key, entry] of windows) {
1048
+ if (current - entry.start > windowMs) {
1049
+ windows.delete(key);
1050
+ }
1051
+ }
1052
+ }
1053
+ return {
1054
+ allow(key) {
1055
+ const current = now();
1056
+ const entry = windows.get(key);
1057
+ if (!entry || current - entry.start > windowMs) {
1058
+ if (windows.size > 1e4) {
1059
+ sweep(current);
1060
+ }
1061
+ windows.set(key, { start: current, count: 1 });
1062
+ return true;
1063
+ }
1064
+ entry.count += 1;
1065
+ return entry.count <= limit;
1066
+ }
1067
+ };
1068
+ }
1069
+
1070
+ // src/form-proxy/http.ts
1071
+ function jsonResponse(body, status) {
1072
+ return new Response(JSON.stringify(body), {
1073
+ status,
1074
+ headers: { "Content-Type": "application/json" }
1075
+ });
1076
+ }
1077
+ function declaredLength(request) {
1078
+ const raw = request.headers.get("Content-Length");
1079
+ if (raw === null) {
1080
+ return null;
1081
+ }
1082
+ const n = Number(raw);
1083
+ return Number.isFinite(n) ? n : null;
1084
+ }
1085
+ function exceedsLimit(request, maxBytes) {
1086
+ const declared = declaredLength(request);
1087
+ return declared !== null && declared > maxBytes;
1088
+ }
1089
+ async function readBodyWithin(request, maxBytes, tooLarge) {
1090
+ if (exceedsLimit(request, maxBytes)) {
1091
+ return { ok: false, response: jsonResponse({ error: tooLarge }, 413) };
1092
+ }
1093
+ const bytes = await request.arrayBuffer();
1094
+ if (bytes.byteLength > maxBytes) {
1095
+ return { ok: false, response: jsonResponse({ error: tooLarge }, 413) };
1096
+ }
1097
+ return { ok: true, value: bytes };
1098
+ }
1099
+ async function readJsonBody(request, maxBytes) {
1100
+ const read = await readBodyWithin(request, maxBytes, "Payload too large");
1101
+ if (!read.ok) {
1102
+ return read;
1103
+ }
1104
+ try {
1105
+ return {
1106
+ ok: true,
1107
+ value: JSON.parse(new TextDecoder().decode(read.value))
1108
+ };
1109
+ } catch {
1110
+ return {
1111
+ ok: false,
1112
+ response: jsonResponse({ error: "Invalid JSON" }, 400)
1113
+ };
1114
+ }
1115
+ }
1116
+ async function readMultipartBody(request, maxBytes) {
1117
+ const contentType = request.headers.get("Content-Type") ?? "";
1118
+ if (!contentType.includes("multipart/form-data")) {
1119
+ return {
1120
+ ok: false,
1121
+ response: jsonResponse({ error: "Expected multipart form data" }, 400)
1122
+ };
1123
+ }
1124
+ const read = await readBodyWithin(request, maxBytes, "File too large");
1125
+ if (!read.ok) {
1126
+ return read;
1127
+ }
1128
+ try {
1129
+ const value = await new Response(read.value, {
1130
+ headers: { "Content-Type": contentType }
1131
+ }).formData();
1132
+ return { ok: true, value };
1133
+ } catch {
1134
+ return {
1135
+ ok: false,
1136
+ response: jsonResponse({ error: "Invalid multipart body" }, 400)
1137
+ };
1138
+ }
1139
+ }
1140
+
1141
+ // src/form-proxy/cms-write.ts
1142
+ async function writeThroughCms(cms, formId, write, shape) {
1143
+ let result;
1144
+ try {
1145
+ if (!await cms.formBelongsToProject(formId)) {
1146
+ return jsonResponse({ error: "Invalid form id" }, 400);
1147
+ }
1148
+ result = await write();
1149
+ } catch {
1150
+ return jsonResponse({ error: shape.failed }, 502);
1151
+ }
1152
+ if (result.status >= 200 && result.status < 300) {
1153
+ return jsonResponse(shape.success(result.id), 201);
1154
+ }
1155
+ const message = shape.passThrough[result.status];
1156
+ if (message !== void 0) {
1157
+ const fields = result.fields?.length ? { fields: result.fields } : {};
1158
+ return jsonResponse({ error: message, ...fields }, result.status);
1159
+ }
1160
+ return jsonResponse({ error: shape.failed }, 502);
1161
+ }
1162
+
1163
+ // src/form-proxy/submissions.ts
1164
+ var MAX_SUBMISSION_BYTES = 64 * 1024;
1165
+ var CMS_LOCALES = ["es", "en"];
1166
+ function isRecord(value) {
1167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1168
+ }
1169
+ function honeypotFilled(value) {
1170
+ return value !== void 0 && value !== null && value !== "";
1171
+ }
1172
+ async function handleFormSubmission(request, deps) {
1173
+ if (deps.rateLimiter && !deps.rateLimiter.allow(deps.clientAddress)) {
1174
+ return jsonResponse({ error: "Too many requests" }, 429);
1175
+ }
1176
+ const read = await readJsonBody(request, MAX_SUBMISSION_BYTES);
1177
+ if (!read.ok) {
1178
+ return read.response;
1179
+ }
1180
+ const body = read.value;
1181
+ if (!isRecord(body)) {
1182
+ return jsonResponse({ error: "Invalid data" }, 400);
1183
+ }
1184
+ if (typeof body.form !== "string" || !body.form) {
1185
+ return jsonResponse({ error: "Invalid form id" }, 400);
1186
+ }
1187
+ if (body.project !== deps.projectId) {
1188
+ return jsonResponse({ error: "Invalid project id" }, 400);
1189
+ }
1190
+ if (!isRecord(body.data)) {
1191
+ return jsonResponse({ error: "Invalid data" }, 400);
1192
+ }
1193
+ if (honeypotFilled(body.honeypot)) {
1194
+ return jsonResponse({ error: "Invalid submission" }, 400);
1195
+ }
1196
+ const form = body.form;
1197
+ const data = body.data;
1198
+ const locales = deps.locales ?? CMS_LOCALES;
1199
+ const locale = typeof body.locale === "string" && locales.includes(body.locale) ? body.locale : void 0;
1200
+ return writeThroughCms(
1201
+ deps.cms,
1202
+ form,
1203
+ () => deps.cms.createSubmission(
1204
+ { form, project: deps.projectId, data, honeypot: "" },
1205
+ locale
1206
+ ),
1207
+ {
1208
+ success: (id) => ({ ok: true, id }),
1209
+ passThrough: { 400: "Invalid submission" },
1210
+ failed: "Submission failed"
1211
+ }
1212
+ );
1213
+ }
1214
+
1215
+ // src/form-proxy/uploads.ts
1216
+ function parsePayload(raw) {
1217
+ if (typeof raw !== "string") {
1218
+ return null;
1219
+ }
1220
+ try {
1221
+ const parsed = JSON.parse(raw);
1222
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.form !== "string" || !parsed.form) {
1223
+ return null;
1224
+ }
1225
+ return { form: parsed.form, project: parsed.project };
1226
+ } catch {
1227
+ return null;
1228
+ }
1229
+ }
1230
+ async function handleFormUpload(request, deps) {
1231
+ if (deps.rateLimiter && !deps.rateLimiter.allow(deps.clientAddress)) {
1232
+ return jsonResponse({ error: "Too many requests" }, 429);
1233
+ }
1234
+ const read = await readMultipartBody(request, MAX_REQUEST_BYTES);
1235
+ if (!read.ok) {
1236
+ return read.response;
1237
+ }
1238
+ const formData = read.value;
1239
+ const file = formData.get("file");
1240
+ if (!(file instanceof File)) {
1241
+ return jsonResponse({ error: "Missing file" }, 400);
1242
+ }
1243
+ if (file.size > MAX_UPLOAD_BYTES) {
1244
+ return jsonResponse({ error: "File too large" }, 413);
1245
+ }
1246
+ const payload = parsePayload(formData.get("_payload"));
1247
+ if (!payload) {
1248
+ return jsonResponse({ error: "Invalid form id" }, 400);
1249
+ }
1250
+ if (payload.project !== deps.projectId) {
1251
+ return jsonResponse({ error: "Invalid project id" }, 400);
1252
+ }
1253
+ return writeThroughCms(
1254
+ deps.cms,
1255
+ payload.form,
1256
+ () => deps.cms.createUpload(file, {
1257
+ form: payload.form,
1258
+ project: deps.projectId
1259
+ }),
1260
+ {
1261
+ success: (id) => ({ doc: { id } }),
1262
+ passThrough: { 400: "Invalid file", 413: "File too large" },
1263
+ failed: "Upload failed"
1264
+ }
1265
+ );
1266
+ }
1267
+
1268
+ // src/form-proxy/cms-errors.ts
1269
+ var FIELD_PATH = /^data\.([A-Za-z0-9_-]+)$/;
1270
+ var MAX_FIELDS = 50;
1271
+ function isRecord2(value) {
1272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1273
+ }
1274
+ function pathsOf(entry) {
1275
+ if (!isRecord2(entry)) {
1276
+ return [];
1277
+ }
1278
+ const paths = [];
1279
+ if (typeof entry.path === "string") {
1280
+ paths.push(entry.path);
1281
+ }
1282
+ const data = entry.data;
1283
+ if (isRecord2(data) && Array.isArray(data.errors)) {
1284
+ for (const nested of data.errors) {
1285
+ if (isRecord2(nested) && typeof nested.path === "string") {
1286
+ paths.push(nested.path);
1287
+ }
1288
+ }
1289
+ }
1290
+ return paths;
1291
+ }
1292
+ function refusedFieldNames(body) {
1293
+ if (!isRecord2(body) || !Array.isArray(body.errors)) {
1294
+ return [];
1295
+ }
1296
+ const names = /* @__PURE__ */ new Set();
1297
+ for (const entry of body.errors) {
1298
+ for (const path of pathsOf(entry)) {
1299
+ const match = FIELD_PATH.exec(path);
1300
+ if (match) {
1301
+ names.add(match[1]);
1302
+ }
1303
+ }
1304
+ }
1305
+ return [...names].slice(0, MAX_FIELDS);
1306
+ }
1307
+
1308
+ // src/form-proxy/cms-client.ts
1309
+ async function stripToStatusAndId(res) {
1310
+ let body;
1311
+ try {
1312
+ body = await res.json();
1313
+ } catch {
1314
+ return { status: res.status };
1315
+ }
1316
+ if (!res.ok) {
1317
+ return { status: res.status, fields: refusedFieldNames(body) };
1318
+ }
1319
+ const doc = body;
1320
+ const id = doc?.doc?.id ?? doc?.id;
1321
+ return { status: res.status, id: typeof id === "string" ? id : void 0 };
1322
+ }
1323
+ function createFormProxyCmsClient(api, apiUrl) {
1324
+ function writeUrl(path, locale) {
1325
+ const url = new URL(path, apiUrl);
1326
+ url.searchParams.set("depth", "0");
1327
+ if (locale) {
1328
+ url.searchParams.set("locale", locale);
1329
+ }
1330
+ return url;
1331
+ }
1332
+ async function formBelongsToProject(formId) {
1333
+ if (!/^[a-zA-Z0-9_-]+$/.test(formId)) {
1334
+ return false;
1335
+ }
1336
+ const docs = await api.fetchCmsCollection("forms", {
1337
+ depth: 0,
1338
+ limit: 1,
1339
+ query: { where: { id: { equals: formId } } }
1340
+ });
1341
+ return docs.some((doc) => doc.id === formId);
1342
+ }
1343
+ return {
1344
+ formBelongsToProject,
1345
+ async createSubmission(payload, locale) {
1346
+ const res = await fetch(writeUrl("/api/form-submissions", locale), {
1347
+ method: "POST",
1348
+ headers: { ...api.buildHeaders(), "Content-Type": "application/json" },
1349
+ body: JSON.stringify(payload)
1350
+ });
1351
+ return stripToStatusAndId(res);
1352
+ },
1353
+ // `form` in the payload is dropped by the CMS — the field is server-written
1354
+ // there, and an upload carries no form until the submission claiming it
1355
+ // arrives. It is sent anyway so an older CMS still records it, and nothing
1356
+ // on this side reads it back.
1357
+ async createUpload(file, payload) {
1358
+ const body = new FormData();
1359
+ body.append("file", file, file.name);
1360
+ body.append("_payload", JSON.stringify(payload));
1361
+ const res = await fetch(writeUrl("/api/form-uploads"), {
1362
+ method: "POST",
1363
+ headers: api.buildHeaders(),
1364
+ body
1365
+ });
1366
+ return stripToStatusAndId(res);
1367
+ }
1368
+ };
1369
+ }
1370
+
1371
+ // src/form-proxy/index.ts
1372
+ function clientAddress(context) {
1373
+ try {
1374
+ return context.clientAddress;
1375
+ } catch {
1376
+ return "unknown";
1377
+ }
1378
+ }
1379
+ function createFormProxy(cms, project, locales) {
1380
+ const submissionLimiter = createRateLimiter({
1381
+ limit: 10,
1382
+ windowMs: 6e4
1383
+ });
1384
+ const uploadLimiter = createRateLimiter({ limit: 20, windowMs: 6e4 });
1385
+ async function buildProxyDeps(context, rateLimiter) {
1386
+ return {
1387
+ projectId: (await project()).id,
1388
+ cms,
1389
+ clientAddress: clientAddress(context),
1390
+ rateLimiter,
1391
+ locales
1392
+ };
1393
+ }
1394
+ return {
1395
+ cms,
1396
+ submissionLimiter,
1397
+ uploadLimiter,
1398
+ buildProxyDeps,
1399
+ handleFormSubmission,
1400
+ handleFormUpload
1401
+ };
1402
+ }
1403
+
1404
+ // src/home-page.ts
1405
+ async function fetchHomePage(ctx, locale) {
1406
+ const options = await ctx.cms(locale).options();
1407
+ const configured = options.homepage?.page;
1408
+ const id = typeof configured === "string" ? configured : configured?.id ?? null;
1409
+ if (!id) {
1410
+ return null;
1411
+ }
1412
+ const [doc] = await ctx.api.fetchCmsCollection("pages", {
1413
+ locale,
1414
+ fallback: "default",
1415
+ status: "published",
1416
+ limit: 1,
1417
+ query: { where: { id: { equals: id } } }
1418
+ });
1419
+ if (!doc) {
1420
+ return null;
1421
+ }
1422
+ const path = (doc.breadcrumbs?.at(-1)?.url ?? doc.slug ?? "").replace(
1423
+ /^\//,
1424
+ ""
1425
+ );
1426
+ return { ...doc, children: [], parent: void 0, path };
1427
+ }
1428
+ function createHomePage(ctx) {
1429
+ const cache = /* @__PURE__ */ new Map();
1430
+ function getHomePage(locale) {
1431
+ let pending = cache.get(locale);
1432
+ if (!pending) {
1433
+ pending = fetchHomePage(ctx, locale);
1434
+ cache.set(locale, pending);
1435
+ }
1436
+ return pending;
1437
+ }
1438
+ async function getHomePageRef(locale) {
1439
+ const page = await getHomePage(locale);
1440
+ return page ? { id: page.id, path: page.path } : null;
1441
+ }
1442
+ return { getHomePage, getHomePageRef };
1443
+ }
1444
+
1445
+ // src/client.ts
1446
+ function createClient(options) {
1447
+ const locales = defineLocales(options);
1448
+ const api = createApi(options);
1449
+ const createRoutes = defineRoutes(locales);
1450
+ const cmsToHref = defineCmsToHref(createRoutes);
1451
+ const instances = /* @__PURE__ */ new Map();
1452
+ const ctx = {
1453
+ api,
1454
+ locales,
1455
+ options,
1456
+ cms: (locale) => cms(locale)
1457
+ };
1458
+ function cms(locale = locales.defaultLocale) {
1459
+ let instance = instances.get(locale);
1460
+ if (!instance) {
1461
+ const base = buildCms(ctx, locale);
1462
+ const extended = {};
1463
+ for (const [key, extension] of Object.entries(
1464
+ options.extensions ?? {}
1465
+ )) {
1466
+ extended[key] = extension({
1467
+ api,
1468
+ locale,
1469
+ cms: ctx.cms,
1470
+ createCollectionLoader,
1471
+ createGlobalLoader
1472
+ });
1473
+ }
1474
+ instance = { ...base, ...extended };
1475
+ instances.set(locale, instance);
1476
+ }
1477
+ return instance;
1478
+ }
1479
+ const { getHomePage, getHomePageRef } = createHomePage(ctx);
1480
+ const formProxy = createFormProxy(
1481
+ createFormProxyCmsClient(api, options.apiUrl),
1482
+ () => cms().project(),
1483
+ locales.locales
1484
+ );
1485
+ return {
1486
+ options,
1487
+ locales,
1488
+ api,
1489
+ cms,
1490
+ routes: createRoutes,
1491
+ cmsToHref,
1492
+ getHomePage,
1493
+ getHomePageRef,
1494
+ getBlogListing: createBlogListing(ctx),
1495
+ /** Wires a single post document, e.g. a draft for live preview. */
1496
+ wirePost: (post, locale) => wirePost(ctx, post, locale),
1497
+ formProxy,
1498
+ /** The fetches `astro.config.mjs` makes before the build starts. */
1499
+ config: createConfigTime(api, createRoutes(locales.defaultLocale))
1500
+ };
1501
+ }
1502
+
1503
+ // src/resolve-menu-items.ts
1504
+ function customLink(label, url) {
1505
+ return { type: "custom", label, url };
1506
+ }
1507
+ function sortByLabel(items) {
1508
+ return [...items].sort((a, b) => {
1509
+ const labelA = a.link?.label ?? "";
1510
+ const labelB = b.link?.label ?? "";
1511
+ return labelA.localeCompare(labelB);
1512
+ });
1513
+ }
1514
+ function pageToSubmenuItem(routes, page) {
1515
+ return {
1516
+ type: "link",
1517
+ link: customLink(page.title, routes.page(page))
1518
+ };
1519
+ }
1520
+ function categoryToSubmenuItem(routes, category) {
1521
+ return {
1522
+ type: "link",
1523
+ link: customLink(category.title, routes.blog("category", category.path))
1524
+ };
1525
+ }
1526
+ function tagToSubmenuItem(routes, tag) {
1527
+ return {
1528
+ type: "link",
1529
+ link: customLink(tag.title, routes.blog("tag", tag.slug))
1530
+ };
1531
+ }
1532
+ function teamMemberToSubmenuItem(routes, member) {
1533
+ return {
1534
+ type: "link",
1535
+ link: customLink(
1536
+ `${member.firstName} ${member.lastName}`.trim(),
1537
+ routes.team("member", member.slug)
1538
+ )
1539
+ };
1540
+ }
1541
+ function resolveNested(menuItem, data, routes, labels) {
1542
+ const { nestedType } = menuItem;
1543
+ if (nestedType === "nested-pages") {
1544
+ const parentPage = menuItem.parentPage;
1545
+ if (!parentPage || typeof parentPage === "string") {
1546
+ return { submenuItems: menuItem.submenuItems };
1547
+ }
1548
+ const parentId = parentPage.id;
1549
+ const wiredParent = data.pages.find((p) => p.id === parentId);
1550
+ if (!wiredParent) {
1551
+ return { submenuItems: menuItem.submenuItems };
1552
+ }
1553
+ const mainLink = customLink(wiredParent.title, routes.page(wiredParent));
1554
+ return {
1555
+ submenuItems: wiredParent.children.map(
1556
+ (child) => pageToSubmenuItem(routes, child)
1557
+ ),
1558
+ mainLink,
1559
+ hasMainLink: true
1560
+ };
1561
+ }
1562
+ if (nestedType === "collection") {
1563
+ const slug = menuItem.collectionSlug;
1564
+ if (slug === "categories") {
1565
+ return {
1566
+ submenuItems: sortByLabel(
1567
+ data.categories.map((c) => categoryToSubmenuItem(routes, c))
1568
+ ),
1569
+ hasMainLink: false
1570
+ };
1571
+ }
1572
+ if (slug === "tags") {
1573
+ return {
1574
+ submenuItems: sortByLabel(
1575
+ data.tags.map((t) => tagToSubmenuItem(routes, t))
1576
+ ),
1577
+ hasMainLink: false
1578
+ };
1579
+ }
1580
+ if (slug === "team-members") {
1581
+ return {
1582
+ submenuItems: sortByLabel(
1583
+ data.teamMembers.map((m) => teamMemberToSubmenuItem(routes, m))
1584
+ ),
1585
+ mainLink: customLink(labels.team, routes.team()),
1586
+ hasMainLink: true
1587
+ };
1588
+ }
1589
+ return { submenuItems: menuItem.submenuItems };
1590
+ }
1591
+ return { submenuItems: menuItem.submenuItems };
1592
+ }
1593
+ function resolveMenuItems(mainMenu, data, routes, labels) {
1594
+ return mainMenu.map((entry) => {
1595
+ const menuItem = entry.menuItem;
1596
+ if (!menuItem || menuItem.type !== "nested") {
1597
+ return entry;
1598
+ }
1599
+ const { submenuItems, mainLink, hasMainLink } = resolveNested(
1600
+ menuItem,
1601
+ data,
1602
+ routes,
1603
+ labels
1604
+ );
1605
+ const resolvedMainLink = mainLink ?? menuItem.mainLink;
1606
+ const resolvedHasMainLink = hasMainLink !== void 0 ? hasMainLink : menuItem.hasMainLink;
1607
+ return {
1608
+ ...entry,
1609
+ menuItem: {
1610
+ ...menuItem,
1611
+ submenuItems,
1612
+ mainLink: resolvedMainLink,
1613
+ hasMainLink: resolvedHasMainLink
1614
+ }
1615
+ };
1616
+ });
1617
+ }
1618
+ export {
1619
+ DEFAULT_APPEARANCE_FALLBACKS,
1620
+ MAX_SUBMISSION_BYTES,
1621
+ createClient,
1622
+ createCollectionLoader,
1623
+ createFormProxy,
1624
+ createFormProxyCmsClient,
1625
+ createGlobalLoader,
1626
+ createRateLimiter,
1627
+ defineCmsToHref,
1628
+ defineLocales,
1629
+ defineRoutes,
1630
+ handleFormSubmission,
1631
+ handleFormUpload,
1632
+ mapWithConcurrency,
1633
+ refusedFieldNames,
1634
+ resolveMenuItems
1635
+ };
1636
+ //# sourceMappingURL=index.js.map