camox 0.36.0 → 0.36.1

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.
@@ -51,9 +51,9 @@ function pageStructureQueryFn(path, projectSlug, source) {
51
51
  }
52
52
  function usePreviewedPage() {
53
53
  const $ = c(26);
54
- if ($[0] !== "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d") {
54
+ if ($[0] !== "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a") {
55
55
  for (let $i = 0; $i < 26; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
56
- $[0] = "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d";
56
+ $[0] = "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a";
57
57
  }
58
58
  const { pathname } = useLocation();
59
59
  const queryClient = useQueryClient();
@@ -150,9 +150,9 @@ function _temp(state) {
150
150
  }
151
151
  const BlockRenderer = (t0) => {
152
152
  const $ = c(23);
153
- if ($[0] !== "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d") {
153
+ if ($[0] !== "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a") {
154
154
  for (let $i = 0; $i < 23; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
155
- $[0] = "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d";
155
+ $[0] = "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a";
156
156
  }
157
157
  const { blockId, mode, showAddBlockTop, showAddBlockBottom } = t0;
158
158
  const previewSource = useSelector(previewStore, _temp3);
@@ -295,9 +295,9 @@ const PageContent = () => {
295
295
  };
296
296
  function useHydrateDraftCache() {
297
297
  const $ = c(7);
298
- if ($[0] !== "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d") {
298
+ if ($[0] !== "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a") {
299
299
  for (let $i = 0; $i < 7; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
300
- $[0] = "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d";
300
+ $[0] = "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a";
301
301
  }
302
302
  const isAuthenticated = useIsAuthenticated();
303
303
  const queryClient = useQueryClient();
@@ -347,16 +347,17 @@ function useHydrateDraftCache() {
347
347
  }
348
348
  function _temp4() {}
349
349
  const CamoxPreview = (t0) => {
350
- const $ = c(39);
351
- if ($[0] !== "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d") {
352
- for (let $i = 0; $i < 39; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
353
- $[0] = "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d";
350
+ const $ = c(40);
351
+ if ($[0] !== "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a") {
352
+ for (let $i = 0; $i < 40; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
353
+ $[0] = "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a";
354
354
  }
355
355
  const { children } = t0;
356
356
  const isAuthenticated = useIsAuthenticated();
357
357
  const isEditMode = useSelector(previewStore, _temp5);
358
358
  const isToolbarHidden = useSelector(previewStore, _temp6);
359
- const previewSource = useSelector(previewStore, _temp7);
359
+ const isAddBlockSidebarOpen = useSelector(previewStore, _temp7);
360
+ const previewSource = useSelector(previewStore, _temp8);
360
361
  const pageData = usePreviewedPage();
361
362
  useHydrateDraftCache();
362
363
  const hasLiveCheckpoint = pageData.page.livePublishedCheckpointId != null;
@@ -376,7 +377,7 @@ const CamoxPreview = (t0) => {
376
377
  ],
377
378
  groupLabel: "Preview",
378
379
  checkIfAvailable: () => isAuthenticated && isEditMode,
379
- execute: _temp8,
380
+ execute: _temp9,
380
381
  shortcut: {
381
382
  key: "Enter",
382
383
  withMeta: true
@@ -392,7 +393,7 @@ const CamoxPreview = (t0) => {
392
393
  ],
393
394
  groupLabel: "Preview",
394
395
  checkIfAvailable: () => isAuthenticated && !isEditMode,
395
- execute: _temp9,
396
+ execute: _temp0,
396
397
  shortcut: {
397
398
  key: "Enter",
398
399
  withMeta: true
@@ -408,7 +409,7 @@ const CamoxPreview = (t0) => {
408
409
  ],
409
410
  groupLabel: "Preview",
410
411
  checkIfAvailable: () => isAuthenticated && previewSource === "draft" && hasLiveCheckpoint,
411
- execute: _temp0,
412
+ execute: _temp1,
412
413
  shortcut: {
413
414
  key: "d",
414
415
  withAlt: true
@@ -424,7 +425,7 @@ const CamoxPreview = (t0) => {
424
425
  ],
425
426
  groupLabel: "Preview",
426
427
  checkIfAvailable: () => isAuthenticated && previewSource === "live",
427
- execute: _temp1,
428
+ execute: _temp10,
428
429
  shortcut: {
429
430
  key: "d",
430
431
  withAlt: true
@@ -435,8 +436,8 @@ const CamoxPreview = (t0) => {
435
436
  label: "Clear selection",
436
437
  aliases: ["Deselect", "Unselect"],
437
438
  groupLabel: "Preview",
438
- checkIfAvailable: _temp10,
439
- execute: _temp11,
439
+ checkIfAvailable: _temp11,
440
+ execute: _temp12,
440
441
  shortcut: { key: "Escape" }
441
442
  }
442
443
  ];
@@ -447,7 +448,7 @@ const CamoxPreview = (t0) => {
447
448
  return () => {
448
449
  actionsStore.send({
449
450
  type: "unregisterManyActions",
450
- ids: actions.map(_temp12)
451
+ ids: actions.map(_temp13)
451
452
  });
452
453
  };
453
454
  };
@@ -485,57 +486,65 @@ const CamoxPreview = (t0) => {
485
486
  $[10] = t4;
486
487
  } else t4 = $[10];
487
488
  let t5;
488
- if ($[11] !== isToolbarHidden) {
489
- t5 = !isToolbarHidden && /* @__PURE__ */ jsx(Navbar, {});
490
- $[11] = isToolbarHidden;
491
- $[12] = t5;
492
- } else t5 = $[12];
489
+ if ($[11] !== isAddBlockSidebarOpen || $[12] !== isToolbarHidden) {
490
+ t5 = !isToolbarHidden && /* @__PURE__ */ jsxs("div", {
491
+ className: "relative",
492
+ children: [/* @__PURE__ */ jsx(Navbar, {}), isAddBlockSidebarOpen && /* @__PURE__ */ jsx("div", {
493
+ className: "absolute inset-0 z-20",
494
+ style: { background: "rgba(0, 0, 0, 0.66)" },
495
+ onClick: _temp14
496
+ })]
497
+ });
498
+ $[11] = isAddBlockSidebarOpen;
499
+ $[12] = isToolbarHidden;
500
+ $[13] = t5;
501
+ } else t5 = $[13];
493
502
  let t6;
494
- if ($[13] !== isEditMode || $[14] !== pageData.page) {
503
+ if ($[14] !== isEditMode || $[15] !== pageData.page) {
495
504
  t6 = isEditMode && /* @__PURE__ */ jsx(LeftSidebar, { page: pageData.page });
496
- $[13] = isEditMode;
497
- $[14] = pageData.page;
498
- $[15] = t6;
499
- } else t6 = $[15];
505
+ $[14] = isEditMode;
506
+ $[15] = pageData.page;
507
+ $[16] = t6;
508
+ } else t6 = $[16];
500
509
  let t7;
501
- if ($[16] !== hasLiveCheckpoint || $[17] !== pageData.page.status) {
510
+ if ($[17] !== hasLiveCheckpoint || $[18] !== pageData.page.status) {
502
511
  t7 = {
503
512
  pageStatus: pageData.page.status,
504
513
  hasLiveVersion: hasLiveCheckpoint
505
514
  };
506
- $[16] = hasLiveCheckpoint;
507
- $[17] = pageData.page.status;
508
- $[18] = t7;
509
- } else t7 = $[18];
515
+ $[17] = hasLiveCheckpoint;
516
+ $[18] = pageData.page.status;
517
+ $[19] = t7;
518
+ } else t7 = $[19];
510
519
  let t8;
511
- if ($[19] !== isEditMode) {
520
+ if ($[20] !== isEditMode) {
512
521
  t8 = isEditMode && /* @__PURE__ */ jsx("div", { style: {
513
522
  height: "80px",
514
523
  background: "transparent"
515
524
  } });
516
- $[19] = isEditMode;
517
- $[20] = t8;
518
- } else t8 = $[20];
525
+ $[20] = isEditMode;
526
+ $[21] = t8;
527
+ } else t8 = $[21];
519
528
  let t9;
520
- if ($[21] !== children || $[22] !== t7 || $[23] !== t8) {
529
+ if ($[22] !== children || $[23] !== t7 || $[24] !== t8) {
521
530
  t9 = /* @__PURE__ */ jsxs(PreviewPanel, {
522
531
  toolbarProps: t7,
523
532
  children: [children, t8]
524
533
  });
525
- $[21] = children;
526
- $[22] = t7;
527
- $[23] = t8;
528
- $[24] = t9;
529
- } else t9 = $[24];
534
+ $[22] = children;
535
+ $[23] = t7;
536
+ $[24] = t8;
537
+ $[25] = t9;
538
+ } else t9 = $[25];
530
539
  let t10;
531
- if ($[25] !== isEditMode || $[26] !== pageData.page.id) {
540
+ if ($[26] !== isEditMode || $[27] !== pageData.page.id) {
532
541
  t10 = isEditMode && /* @__PURE__ */ jsx(RightSidebar, { pageId: pageData.page.id });
533
- $[25] = isEditMode;
534
- $[26] = pageData.page.id;
535
- $[27] = t10;
536
- } else t10 = $[27];
542
+ $[26] = isEditMode;
543
+ $[27] = pageData.page.id;
544
+ $[28] = t10;
545
+ } else t10 = $[28];
537
546
  let t11;
538
- if ($[28] !== t10 || $[29] !== t6 || $[30] !== t9) {
547
+ if ($[29] !== t10 || $[30] !== t6 || $[31] !== t9) {
539
548
  t11 = /* @__PURE__ */ jsxs("div", {
540
549
  className: "flex h-full flex-row items-stretch",
541
550
  children: [
@@ -544,19 +553,19 @@ const CamoxPreview = (t0) => {
544
553
  t10
545
554
  ]
546
555
  });
547
- $[28] = t10;
548
- $[29] = t6;
549
- $[30] = t9;
550
- $[31] = t11;
551
- } else t11 = $[31];
556
+ $[29] = t10;
557
+ $[30] = t6;
558
+ $[31] = t9;
559
+ $[32] = t11;
560
+ } else t11 = $[32];
552
561
  let t12;
553
- if ($[32] !== isEditMode) {
562
+ if ($[33] !== isEditMode) {
554
563
  t12 = isEditMode && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CreatePageModal, {}), /* @__PURE__ */ jsx(DraftSwitchDialog, {})] });
555
- $[32] = isEditMode;
556
- $[33] = t12;
557
- } else t12 = $[33];
564
+ $[33] = isEditMode;
565
+ $[34] = t12;
566
+ } else t12 = $[34];
558
567
  let t13;
559
- if ($[34] !== t11 || $[35] !== t12 || $[36] !== t4 || $[37] !== t5) {
568
+ if ($[35] !== t11 || $[36] !== t12 || $[37] !== t4 || $[38] !== t5) {
560
569
  t13 = /* @__PURE__ */ jsxs("div", {
561
570
  className: t4,
562
571
  children: [
@@ -565,19 +574,19 @@ const CamoxPreview = (t0) => {
565
574
  t12
566
575
  ]
567
576
  });
568
- $[34] = t11;
569
- $[35] = t12;
570
- $[36] = t4;
571
- $[37] = t5;
572
- $[38] = t13;
573
- } else t13 = $[38];
577
+ $[35] = t11;
578
+ $[36] = t12;
579
+ $[37] = t4;
580
+ $[38] = t5;
581
+ $[39] = t13;
582
+ } else t13 = $[39];
574
583
  return t13;
575
584
  };
576
585
  function usePreviewPagesActions() {
577
586
  const $ = c(13);
578
- if ($[0] !== "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d") {
587
+ if ($[0] !== "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a") {
579
588
  for (let $i = 0; $i < 13; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
580
- $[0] = "a8aadbae83f448aedc99fd8344f3c11935e775ce61e901362c961f0c7099892d";
589
+ $[0] = "08efc24964e6f71a0b75d26676e04d8253640e7adc88fa9c6e2293e86440c27a";
581
590
  }
582
591
  const navigate = useNavigate();
583
592
  const { pathname } = useLocation();
@@ -618,8 +627,8 @@ function usePreviewPagesActions() {
618
627
  label: "Create page",
619
628
  aliases: ["New page", "Add page"],
620
629
  groupLabel: "Preview",
621
- checkIfAvailable: _temp13,
622
- execute: _temp14
630
+ checkIfAvailable: _temp15,
631
+ execute: _temp16
623
632
  },
624
633
  {
625
634
  id: "edit-current-page",
@@ -650,14 +659,14 @@ function usePreviewPagesActions() {
650
659
  groupLabel: "Preview",
651
660
  checkIfAvailable: () => !!pages,
652
661
  hasChildren: true,
653
- execute: _temp15
662
+ execute: _temp17
654
663
  },
655
664
  ...pages ? pages.map((page) => ({
656
665
  id: `go-to-page-${page.id}`,
657
666
  parentActionId: "go-to-page",
658
667
  label: `Go to "${page.nickname}"`,
659
668
  groupLabel: "Preview",
660
- checkIfAvailable: _temp16,
669
+ checkIfAvailable: _temp18,
661
670
  execute: () => navigate({ to: page.fullPath })
662
671
  })) : []
663
672
  ];
@@ -668,7 +677,7 @@ function usePreviewPagesActions() {
668
677
  return () => {
669
678
  actionsStore.send({
670
679
  type: "unregisterManyActions",
671
- ids: actions.map(_temp17)
680
+ ids: actions.map(_temp19)
672
681
  });
673
682
  };
674
683
  };
@@ -688,17 +697,17 @@ function usePreviewPagesActions() {
688
697
  }
689
698
  React.useEffect(t4, t5);
690
699
  }
691
- function _temp17(a) {
700
+ function _temp19(a) {
692
701
  return a.id;
693
702
  }
694
- function _temp16() {
703
+ function _temp18() {
695
704
  return true;
696
705
  }
697
- function _temp15() {}
698
- function _temp14() {
706
+ function _temp17() {}
707
+ function _temp16() {
699
708
  return previewStore.send({ type: "openCreatePageModal" });
700
709
  }
701
- function _temp13() {
710
+ function _temp15() {
702
711
  return true;
703
712
  }
704
713
  function _temp3(state) {
@@ -711,35 +720,41 @@ function _temp6(state_0) {
711
720
  return state_0.context.isToolbarHidden;
712
721
  }
713
722
  function _temp7(state_1) {
714
- return state_1.context.previewSource;
723
+ return state_1.context.isAddBlockSidebarOpen;
715
724
  }
716
- function _temp8() {
717
- return previewStore.send({ type: "exitEditMode" });
725
+ function _temp8(state_2) {
726
+ return state_2.context.previewSource;
718
727
  }
719
728
  function _temp9() {
720
- return previewStore.send({ type: "enterEditMode" });
729
+ return previewStore.send({ type: "exitEditMode" });
721
730
  }
722
731
  function _temp0() {
732
+ return previewStore.send({ type: "enterEditMode" });
733
+ }
734
+ function _temp1() {
723
735
  return previewStore.send({
724
736
  type: "setPreviewSource",
725
737
  source: "live"
726
738
  });
727
739
  }
728
- function _temp1() {
740
+ function _temp10() {
729
741
  return previewStore.send({
730
742
  type: "setPreviewSource",
731
743
  source: "draft"
732
744
  });
733
745
  }
734
- function _temp10() {
746
+ function _temp11() {
735
747
  return true;
736
748
  }
737
- function _temp11() {
749
+ function _temp12() {
738
750
  previewStore.send({ type: "clearSelection" });
739
751
  }
740
- function _temp12(a) {
752
+ function _temp13(a) {
741
753
  return a.id;
742
754
  }
755
+ function _temp14() {
756
+ return previewStore.send({ type: "closeAddBlockSidebar" });
757
+ }
743
758
 
744
759
  //#endregion
745
760
  export { CamoxPreview, PageContent, usePreviewPagesActions, usePreviewedPage };
@@ -16,17 +16,18 @@ import { Toggle } from "@camox/ui/toggle";
16
16
  //#region src/features/preview/components/PreviewToolbar.tsx
17
17
  const PreviewToolbar = (t0) => {
18
18
  const $ = c(74);
19
- if ($[0] !== "f9ff999569a69264033e31a270ebbb7097b2072aec97a7129d1758e95b5b6407") {
19
+ if ($[0] !== "88648f3760cc744b4acae6ce6f9f000de00b69d7e7f65268463e98389ba90d90") {
20
20
  for (let $i = 0; $i < 74; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
21
- $[0] = "f9ff999569a69264033e31a270ebbb7097b2072aec97a7129d1758e95b5b6407";
21
+ $[0] = "88648f3760cc744b4acae6ce6f9f000de00b69d7e7f65268463e98389ba90d90";
22
22
  }
23
23
  const { onEditModeChange, pageStatus, hasLiveVersion } = t0;
24
24
  const isEditMode = useSelector(previewStore, _temp);
25
25
  const isToolbarHidden = useSelector(previewStore, _temp2);
26
- const actions = useSelector(actionsStore, _temp3);
27
- const viewportMode = useSelector(previewStore, _temp4);
26
+ const peekedBlock = useSelector(previewStore, _temp3);
27
+ const actions = useSelector(actionsStore, _temp4);
28
+ const viewportMode = useSelector(previewStore, _temp5);
28
29
  const editModeActionId = isEditMode ? "exit-edit-mode" : "enter-edit-mode";
29
- if (isToolbarHidden) return null;
30
+ if (isToolbarHidden || peekedBlock) return null;
30
31
  let t1;
31
32
  if ($[1] !== onEditModeChange) {
32
33
  t1 = (checked) => {
@@ -251,7 +252,7 @@ const PreviewToolbar = (t0) => {
251
252
  type: "button",
252
253
  variant: "outline",
253
254
  disabled: pageStatus === "published" || !hasLiveVersion,
254
- onClick: _temp5,
255
+ onClick: _temp6,
255
256
  children: "View live site"
256
257
  });
257
258
  $[54] = hasLiveVersion;
@@ -265,7 +266,7 @@ const PreviewToolbar = (t0) => {
265
266
  variant: "ghost",
266
267
  size: "icon",
267
268
  disabled: isEditMode,
268
- onClick: _temp6,
269
+ onClick: _temp7,
269
270
  "aria-label": "Hide toolbar",
270
271
  className: "text-muted-foreground"
271
272
  });
@@ -336,15 +337,18 @@ function _temp2(state_0) {
336
337
  return state_0.context.isToolbarHidden;
337
338
  }
338
339
  function _temp3(state_1) {
339
- return state_1.context.actions;
340
+ return state_1.context.peekedBlock;
340
341
  }
341
342
  function _temp4(state_2) {
342
- return state_2.context.viewportMode;
343
+ return state_2.context.actions;
343
344
  }
344
- function _temp5() {
345
- return previewStore.send({ type: "viewLivePage" });
345
+ function _temp5(state_3) {
346
+ return state_3.context.viewportMode;
346
347
  }
347
348
  function _temp6() {
349
+ return previewStore.send({ type: "viewLivePage" });
350
+ }
351
+ function _temp7() {
348
352
  return previewStore.send({ type: "hideToolbar" });
349
353
  }
350
354
 
@@ -1,6 +1,244 @@
1
- import "../../lib/telemetry.js";
1
+ import { trackEvent } from "../../lib/telemetry.js";
2
2
  import { queryKeys } from "@camox/api-contract/query-keys";
3
3
  import { createORPCClient } from "@orpc/client";
4
4
  import { RPCLink } from "@orpc/client/fetch";
5
5
 
6
- export { };
6
+ //#region src/features/routes/pageRuntime.ts
7
+ function parseQuality(part) {
8
+ const match = part.match(/;\s*q=([0-9.]+)/);
9
+ return match ? parseFloat(match[1]) : 1;
10
+ }
11
+ function prefersMarkdown(accept) {
12
+ let markdownQ = -1;
13
+ let htmlQ = -1;
14
+ for (const part of accept.split(",")) {
15
+ const trimmed = part.trim();
16
+ if (trimmed.startsWith("text/markdown")) markdownQ = parseQuality(trimmed);
17
+ else if (trimmed.startsWith("text/html")) htmlQ = parseQuality(trimmed);
18
+ }
19
+ return markdownQ > 0 && markdownQ >= htmlQ;
20
+ }
21
+ function createServerApiClient(apiUrl, environmentName, options) {
22
+ const headers = {};
23
+ if (environmentName) headers["x-environment-name"] = environmentName;
24
+ return createORPCClient(new RPCLink({
25
+ url: `${apiUrl}/rpc`,
26
+ headers,
27
+ fetch: (request, init) => {
28
+ if (options?.authCookieHeader && request instanceof Request) request.headers.set("Better-Auth-Cookie", options.authCookieHeader);
29
+ return fetch(request, init);
30
+ }
31
+ }));
32
+ }
33
+ function getErrorDetails(error) {
34
+ if (!error || typeof error !== "object") return { message: String(error) };
35
+ const errorRecord = error;
36
+ return {
37
+ code: typeof errorRecord.code === "string" ? errorRecord.code : void 0,
38
+ status: typeof errorRecord.status === "number" ? errorRecord.status : void 0,
39
+ message: error instanceof Error ? error.message : typeof errorRecord.message === "string" ? errorRecord.message : ""
40
+ };
41
+ }
42
+ function isAuthSessionError(error) {
43
+ const { code, status, message } = getErrorDetails(error);
44
+ if (code === "UNAUTHORIZED" || code === "FORBIDDEN") return true;
45
+ if (status === 401 || status === 403) return true;
46
+ const lowerMessage = message.toLowerCase();
47
+ return lowerMessage.includes("unauthorized") || lowerMessage.includes("forbidden") || lowerMessage.includes("invalid session") || lowerMessage.includes("session expired") || lowerMessage.includes("expired session");
48
+ }
49
+ function isNotFoundError(error) {
50
+ const { code, status } = getErrorDetails(error);
51
+ return code === "NOT_FOUND" || status === 404;
52
+ }
53
+ function visitForFileIds(value, ids) {
54
+ if (value == null || typeof value !== "object") return;
55
+ if ("_fileId" in value && typeof value._fileId === "number") {
56
+ ids.add(value._fileId);
57
+ return;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ for (const item of value) visitForFileIds(item, ids);
61
+ return;
62
+ }
63
+ for (const child of Object.values(value)) visitForFileIds(child, ids);
64
+ }
65
+ function collectFileIdsFromContent(content, ids) {
66
+ for (const value of Object.values(content)) visitForFileIds(value, ids);
67
+ }
68
+ function seedBlockCaches(queryClient, pageData, source = "draft") {
69
+ const filesById = new Map(pageData.files.map((f) => [f.id, f]));
70
+ for (const block of pageData.blocks) {
71
+ const blockItems = pageData.repeatableItems.filter((i) => i.blockId === block.id);
72
+ const fileIds = /* @__PURE__ */ new Set();
73
+ collectFileIdsFromContent(block.content, fileIds);
74
+ for (const item of blockItems) collectFileIdsFromContent(item.content, fileIds);
75
+ const blockFiles = [...fileIds].map((id) => filesById.get(id)).filter((f) => f != null);
76
+ queryClient.setQueryData(queryKeys.blocks.get(block.id, source), {
77
+ block,
78
+ repeatableItems: blockItems,
79
+ files: blockFiles
80
+ });
81
+ for (const file of blockFiles) queryClient.setQueryData(queryKeys.files.get(file.id), file);
82
+ }
83
+ }
84
+ async function loadCamoxPage({ apiUrl, authCookieHeader, environmentName, pathname, projectSlug, queryClient, source }) {
85
+ const serverApi = createServerApiClient(apiUrl, environmentName, { authCookieHeader });
86
+ return queryClient.ensureQueryData({
87
+ queryKey: queryKeys.pages.getByPath(pathname, source),
88
+ queryFn: async () => {
89
+ const [data, pagesList] = await Promise.all([serverApi.pages.getByPath({
90
+ path: pathname,
91
+ projectSlug,
92
+ source
93
+ }), serverApi.pages.listBySlug({ projectSlug }).catch(() => null)]);
94
+ seedBlockCaches(queryClient, data, source);
95
+ queryClient.setQueryData(queryKeys.pages.getByPath(pathname, source), {
96
+ page: data.page,
97
+ layout: data.layout,
98
+ projectName: data.projectName,
99
+ project: data.project
100
+ });
101
+ if (source === "live") {
102
+ seedBlockCaches(queryClient, data, "draft");
103
+ queryClient.setQueryData(queryKeys.pages.getByPath(pathname, "draft"), {
104
+ page: data.page,
105
+ layout: data.layout,
106
+ projectName: data.projectName,
107
+ project: data.project
108
+ });
109
+ }
110
+ if (pagesList) queryClient.setQueryData(queryKeys.pages.list, pagesList);
111
+ return {
112
+ page: data.page,
113
+ layout: data.layout,
114
+ projectName: data.projectName,
115
+ project: data.project
116
+ };
117
+ },
118
+ staleTime: Infinity
119
+ });
120
+ }
121
+ async function loadLivePage(options) {
122
+ return loadCamoxPage({
123
+ ...options,
124
+ source: "live"
125
+ });
126
+ }
127
+ function buildCamoxPageLoaderData(apiUrl, page, origin) {
128
+ return {
129
+ page,
130
+ origin,
131
+ faviconUrl: `${apiUrl}/favicons/${page.project.id}?v=${page.project.updatedAt}`
132
+ };
133
+ }
134
+ async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, environmentName, origin, pathname, projectSlug, queryClient }) {
135
+ const loadOptions = {
136
+ apiUrl,
137
+ environmentName,
138
+ pathname,
139
+ projectSlug,
140
+ queryClient
141
+ };
142
+ if (!authCookieHeader) return {
143
+ data: buildCamoxPageLoaderData(apiUrl, await loadLivePage(loadOptions), origin),
144
+ shouldClearAuthCookie: false,
145
+ source: "live"
146
+ };
147
+ try {
148
+ return {
149
+ data: buildCamoxPageLoaderData(apiUrl, await loadCamoxPage({
150
+ ...loadOptions,
151
+ authCookieHeader,
152
+ source: "draft"
153
+ }), origin),
154
+ shouldClearAuthCookie: false,
155
+ source: "draft"
156
+ };
157
+ } catch (error) {
158
+ if (!isAuthSessionError(error)) throw error;
159
+ console.warn("[camox] Ignoring stale camox_auth_cookie and retrying published page load.");
160
+ return {
161
+ data: buildCamoxPageLoaderData(apiUrl, await loadLivePage(loadOptions), origin),
162
+ shouldClearAuthCookie: true,
163
+ source: "live"
164
+ };
165
+ }
166
+ }
167
+ async function createMarkdownResponse({ apiUrl, environmentName, pathname, projectSlug, request }) {
168
+ if (!prefersMarkdown(request.headers.get("Accept") ?? "")) return null;
169
+ const api = createServerApiClient(apiUrl, environmentName);
170
+ const requestPathname = pathname ?? new URL(request.url).pathname;
171
+ try {
172
+ const page = await api.pages.getByPath({
173
+ path: requestPathname,
174
+ projectSlug,
175
+ source: "live"
176
+ });
177
+ const { markdown } = await api.blocks.getPageMarkdown({ pageId: page.page.id });
178
+ if (!markdown) return null;
179
+ trackEvent("markdown_served", {
180
+ pathname: requestPathname,
181
+ projectId: page.page.projectId,
182
+ projectName: page.projectName
183
+ });
184
+ return new Response(markdown, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ function buildCamoxPageHead(camoxApp, loaderData) {
190
+ if (!loaderData) return {};
191
+ const { page, origin, faviconUrl } = loaderData;
192
+ const pageMetaTitle = page.page.metaTitle ?? page.page.pathSegment;
193
+ const meta = [];
194
+ let title = pageMetaTitle;
195
+ if (page.layout) {
196
+ const layout = camoxApp.getLayoutById(page.layout.layoutId);
197
+ if (layout) {
198
+ title = layout._internal.buildMetaTitle({
199
+ pageMetaTitle,
200
+ projectName: page.projectName,
201
+ pageFullPath: page.page.fullPath
202
+ });
203
+ meta.push({ title });
204
+ }
205
+ }
206
+ if (page.page.metaDescription) meta.push({
207
+ name: "description",
208
+ content: page.page.metaDescription
209
+ });
210
+ const ogImageParams = new URLSearchParams({
211
+ ...page.layout && { layoutId: page.layout.layoutId },
212
+ title: pageMetaTitle,
213
+ ...page.page.metaDescription && { description: page.page.metaDescription },
214
+ ...page.projectName && { projectName: page.projectName }
215
+ });
216
+ const ogImageUrl = page.page.customOgImageUrl ?? `${origin}/og?${ogImageParams.toString()}`;
217
+ meta.push({
218
+ property: "og:title",
219
+ content: title
220
+ }, {
221
+ property: "og:image",
222
+ content: ogImageUrl
223
+ }, {
224
+ property: "og:image:width",
225
+ content: "1200"
226
+ }, {
227
+ property: "og:image:height",
228
+ content: "630"
229
+ });
230
+ if (page.page.metaDescription) meta.push({
231
+ property: "og:description",
232
+ content: page.page.metaDescription
233
+ });
234
+ return {
235
+ meta,
236
+ links: [{
237
+ rel: "icon",
238
+ href: faviconUrl
239
+ }]
240
+ };
241
+ }
242
+
243
+ //#endregion
244
+ export { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isNotFoundError, loadCamoxPageForRequest };
@@ -1,6 +1,14 @@
1
+ import { CamoxApp } from "../../core/createApp.js";
2
+ import { CamoxDocument } from "../../core/defineDocument.js";
3
+ import { StudioRenderInput } from "./studioApp.js";
1
4
  import { UseHeadInput } from "unhead/types";
2
5
 
3
6
  //#region src/features/runtime/runtime.d.ts
7
+ type RuntimeRouteKind = "data" | "health" | "og" | "page" | "registry" | "sitemap" | "studio" | "studio-content" | "studio-nested";
8
+ interface RuntimeRouteMatch {
9
+ kind: RuntimeRouteKind;
10
+ pathname: string;
11
+ }
4
12
  interface LayoutIdentity {
5
13
  blockIds: number[];
6
14
  id: number;
@@ -21,5 +29,26 @@ interface PageRenderInput {
21
29
  runtimeBasePath: string;
22
30
  source: "live" | "draft";
23
31
  }
32
+ interface RuntimeOptions {
33
+ apiUrl?: string;
34
+ authenticationUrl?: string;
35
+ environmentName?: string;
36
+ pageClientEntryUrl?: string;
37
+ studioClientEntryUrl?: string;
38
+ getCamoxApp?: () => Promise<CamoxApp>;
39
+ getDocument?: () => Promise<CamoxDocument>;
40
+ projectSlug?: string;
41
+ renderPage?: (input: PageRenderInput) => Promise<string>;
42
+ renderStudio?: (input: StudioRenderInput) => Promise<string>;
43
+ runtimeBasePath?: string;
44
+ stylesheetUrl?: string;
45
+ }
46
+ declare function getRuntimePathname(url: string, runtimeBasePath?: string): string | null;
47
+ declare function matchRuntimeRoute(pathname: string): RuntimeRouteMatch;
48
+ declare function createPageHeadInput(head: {
49
+ meta?: Array<Record<string, string>>;
50
+ links?: Array<Record<string, string>>;
51
+ }): UseHeadInput;
52
+ declare function handleCamoxRequest(request: Request, options?: RuntimeOptions): Promise<Response | null>;
24
53
  //#endregion
25
- export { PageRenderInput };
54
+ export { LayoutIdentity, PageRenderInput, RuntimeOptions, RuntimeRouteKind, RuntimeRouteMatch, createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
@@ -1,8 +1,142 @@
1
- import "../routes/pageRuntime.js";
2
- import { QueryClient } from "@tanstack/react-query";
3
- import "@unhead/react/server";
1
+ import { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isNotFoundError, loadCamoxPageForRequest } from "../routes/pageRuntime.js";
2
+ import { QueryClient, dehydrate } from "@tanstack/react-query";
3
+ import { createHead, renderSSRHead } from "@unhead/react/server";
4
4
 
5
5
  //#region src/features/runtime/runtime.ts
6
+ const DEFAULT_RUNTIME_BASE_PATH = "";
7
+ const RUNTIME_HEALTH_PATH = "/_camox/health";
8
+ const RUNTIME_REGISTRY_PATH = "/_camox/registry";
9
+ const SERVER_AUTH_COOKIE_NAME = "camox_auth_cookie";
10
+ function buildClearServerAuthCookieHeader() {
11
+ return `${SERVER_AUTH_COOKIE_NAME}=; Path=/; SameSite=Lax; Max-Age=0`;
12
+ }
13
+ function getServerAuthCookieHeader(headers) {
14
+ const authCookie = (headers.get("Cookie") ?? headers.get("cookie") ?? "").split(";").map((part) => part.trim()).find((part) => part.startsWith(`${SERVER_AUTH_COOKIE_NAME}=`));
15
+ if (!authCookie) return "";
16
+ const value = authCookie.slice(18);
17
+ try {
18
+ return decodeURIComponent(value);
19
+ } catch {
20
+ return "";
21
+ }
22
+ }
23
+ function normalizeRuntimeBasePath(basePath) {
24
+ if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
25
+ if (!basePath || basePath === "/") return "";
26
+ return basePath.startsWith("/") ? basePath.replace(/\/+$/, "") : `/${basePath.replace(/\/+$/, "")}`;
27
+ }
28
+ function getRuntimePathname(url, runtimeBasePath = DEFAULT_RUNTIME_BASE_PATH) {
29
+ const { pathname } = new URL(url);
30
+ const basePath = normalizeRuntimeBasePath(runtimeBasePath);
31
+ if (!basePath) return pathname || "/";
32
+ if (pathname === basePath) return "/";
33
+ if (!pathname.startsWith(`${basePath}/`)) return null;
34
+ return pathname.slice(basePath.length) || "/";
35
+ }
36
+ function escapeXml(value) {
37
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
38
+ }
39
+ function normalizePagePath(path) {
40
+ if (!path) return "/";
41
+ return path.startsWith("/") ? path : `/${path}`;
42
+ }
43
+ function getLayoutIdentity(data, source) {
44
+ const layout = data.page.layout;
45
+ if (!layout) return null;
46
+ const version = source === "live" ? String(layout.livePublishedCheckpointId ?? "none") : String(layout.contentUpdatedAt ?? layout.updatedAt);
47
+ return {
48
+ blockIds: [...layout.beforeBlockIds, ...layout.afterBlockIds],
49
+ id: layout.id,
50
+ layoutId: layout.layoutId,
51
+ version
52
+ };
53
+ }
54
+ function matchRuntimeRoute(pathname) {
55
+ if (pathname === RUNTIME_HEALTH_PATH) return {
56
+ kind: "health",
57
+ pathname
58
+ };
59
+ if (pathname === RUNTIME_REGISTRY_PATH) return {
60
+ kind: "registry",
61
+ pathname
62
+ };
63
+ if (pathname === "/_camox/data") return {
64
+ kind: "data",
65
+ pathname
66
+ };
67
+ if (pathname === "/camox") return {
68
+ kind: "studio",
69
+ pathname
70
+ };
71
+ if (pathname === "/camox/content") return {
72
+ kind: "studio-content",
73
+ pathname
74
+ };
75
+ if (pathname.startsWith("/camox/")) return {
76
+ kind: "studio-nested",
77
+ pathname
78
+ };
79
+ if (pathname === "/og") return {
80
+ kind: "og",
81
+ pathname
82
+ };
83
+ if (pathname === "/sitemap.xml") return {
84
+ kind: "sitemap",
85
+ pathname
86
+ };
87
+ return {
88
+ kind: "page",
89
+ pathname
90
+ };
91
+ }
92
+ function withRuntimeBasePath(pathname, runtimeBasePath) {
93
+ const basePath = normalizeRuntimeBasePath(runtimeBasePath);
94
+ if (!basePath) return pathname;
95
+ if (pathname === "/") return `${basePath}/`;
96
+ return `${basePath}${pathname}`;
97
+ }
98
+ async function createSitemapResponse(request, options) {
99
+ if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox sitemap is not configured." }, { status: 500 });
100
+ const api = createServerApiClient(options.apiUrl, options.environmentName);
101
+ const origin = new URL(request.url).origin;
102
+ const entries = (await api.pages.listBySlug({ projectSlug: options.projectSlug })).map((page) => ` <url>
103
+ <loc>${escapeXml(`${origin}${withRuntimeBasePath(page.fullPath, options.runtimeBasePath)}`)}</loc>
104
+ <lastmod>${escapeXml(new Date(page.updatedAt).toISOString())}</lastmod>
105
+ </url>`).join("\n");
106
+ return new Response(`<?xml version="1.0" encoding="UTF-8"?>
107
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
108
+ ${entries}
109
+ </urlset>`, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
110
+ }
111
+ async function createOgResponse(request, options) {
112
+ const camoxApp = await options.getCamoxApp?.();
113
+ if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
114
+ const url = new URL(request.url);
115
+ const layoutId = url.searchParams.get("layoutId") || "";
116
+ const title = url.searchParams.get("title") || "";
117
+ const description = url.searchParams.get("description") || "";
118
+ const projectName = url.searchParams.get("projectName") || "";
119
+ const layout = camoxApp.getLayoutById(layoutId);
120
+ if (!layout?._internal.buildOgImage) return new Response("Not found", { status: 404 });
121
+ return layout._internal.buildOgImage({
122
+ title,
123
+ description,
124
+ projectName
125
+ });
126
+ }
127
+ function createDefaultHeadInput(stylesheetUrl) {
128
+ return {
129
+ htmlAttrs: { lang: "en" },
130
+ meta: [{ charset: "utf-8" }, {
131
+ name: "viewport",
132
+ content: "width=device-width, initial-scale=1"
133
+ }],
134
+ link: stylesheetUrl ? [{
135
+ rel: "stylesheet",
136
+ href: stylesheetUrl
137
+ }] : []
138
+ };
139
+ }
6
140
  function createPageHeadInput(head) {
7
141
  const meta = head.meta ?? [];
8
142
  const links = head.links ?? [];
@@ -39,6 +173,211 @@ function createPageHeadInput(head) {
39
173
  link: pageLinks
40
174
  };
41
175
  }
176
+ function renderHead(document, pageHead, stylesheetUrl) {
177
+ const head = createHead();
178
+ head.push(createDefaultHeadInput(stylesheetUrl));
179
+ head.push(document);
180
+ head.push(pageHead);
181
+ return renderSSRHead(head);
182
+ }
183
+ function serializeJsonForHtml(value) {
184
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
185
+ }
186
+ function renderClientEntryScript(src) {
187
+ if (!src) return "";
188
+ return `<script type="module" src="${escapeXml(src)}"><\/script>`;
189
+ }
190
+ async function createPageHtmlResponse({ options, pathname, request }) {
191
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderPage) return createPageDataResponse({
192
+ options,
193
+ pathname,
194
+ request
195
+ });
196
+ const queryClient = new QueryClient();
197
+ try {
198
+ const result = await loadCamoxPageForRequest({
199
+ apiUrl: options.apiUrl,
200
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
201
+ environmentName: options.environmentName,
202
+ origin: new URL(request.url).origin,
203
+ pathname,
204
+ projectSlug: options.projectSlug,
205
+ queryClient
206
+ });
207
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
208
+ if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
209
+ const camoxApp = await options.getCamoxApp?.();
210
+ const document = await options.getDocument?.() ?? {};
211
+ const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : {};
212
+ const dehydratedState = dehydrate(queryClient);
213
+ const pageRenderInput = {
214
+ apiUrl: options.apiUrl,
215
+ authenticationUrl: options.authenticationUrl,
216
+ dehydratedState,
217
+ environmentName: options.environmentName,
218
+ head,
219
+ href: new URL(request.url).href,
220
+ layoutIdentity: getLayoutIdentity(result.data, result.source),
221
+ loaderData: result.data,
222
+ pathname,
223
+ projectSlug: options.projectSlug,
224
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
225
+ source: result.source
226
+ };
227
+ const appHtml = await options.renderPage(pageRenderInput);
228
+ const renderedHead = renderHead(document, createPageHeadInput(head), options.stylesheetUrl);
229
+ return new Response(`<!doctype html>
230
+ <html${renderedHead.htmlAttrs}>
231
+ <head>
232
+ ${renderedHead.headTags}
233
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(pageRenderInput)}<\/script>
234
+ </head>
235
+ <body${renderedHead.bodyAttrs}>
236
+ ${renderedHead.bodyTagsOpen}
237
+ <div id="root">${appHtml}</div>
238
+ ${renderedHead.bodyTags}
239
+ ${renderClientEntryScript(options.pageClientEntryUrl)}
240
+ </body>
241
+ </html>`, { headers });
242
+ } catch (error) {
243
+ if (isNotFoundError(error)) return new Response("No Camox page on this path", { status: 404 });
244
+ throw error;
245
+ }
246
+ }
247
+ async function createStudioHtmlResponse({ match, options, request }) {
248
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderStudio) return Response.json({
249
+ message: "Camox Studio renderer is not configured.",
250
+ ...match
251
+ }, { status: 500 });
252
+ const queryClient = new QueryClient();
253
+ const document = await options.getDocument?.() ?? {};
254
+ const studioRenderInput = {
255
+ apiUrl: options.apiUrl,
256
+ authenticationUrl: options.authenticationUrl,
257
+ dehydratedState: dehydrate(queryClient),
258
+ environmentName: options.environmentName,
259
+ href: new URL(request.url).href,
260
+ pathname: match.pathname,
261
+ projectSlug: options.projectSlug,
262
+ routeKind: match.kind,
263
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
264
+ };
265
+ const appHtml = await options.renderStudio(studioRenderInput);
266
+ const renderedHead = renderHead(document, {}, options.stylesheetUrl);
267
+ return new Response(`<!doctype html>
268
+ <html${renderedHead.htmlAttrs}>
269
+ <head>
270
+ ${renderedHead.headTags}
271
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(studioRenderInput)}<\/script>
272
+ </head>
273
+ <body${renderedHead.bodyAttrs}>
274
+ ${renderedHead.bodyTagsOpen}
275
+ <div id="root">${appHtml}</div>
276
+ ${renderedHead.bodyTags}
277
+ ${renderClientEntryScript(options.studioClientEntryUrl)}
278
+ </body>
279
+ </html>`, { headers: { "Content-Type": "text/html; charset=utf-8" } });
280
+ }
281
+ async function createPageDataResponse({ options, pathname, request }) {
282
+ if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
283
+ const queryClient = new QueryClient();
284
+ try {
285
+ const result = await loadCamoxPageForRequest({
286
+ apiUrl: options.apiUrl,
287
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
288
+ environmentName: options.environmentName,
289
+ origin: new URL(request.url).origin,
290
+ pathname,
291
+ projectSlug: options.projectSlug,
292
+ queryClient
293
+ });
294
+ const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });
295
+ if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
296
+ const camoxApp = await options.getCamoxApp?.();
297
+ const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : null;
298
+ const dehydratedState = dehydrate(queryClient);
299
+ const { page, layout, projectName, project } = result.data.page;
300
+ return new Response(`${JSON.stringify({
301
+ kind: "page",
302
+ pathname,
303
+ page: {
304
+ id: page.id,
305
+ fullPath: page.fullPath,
306
+ metaTitle: page.metaTitle,
307
+ metaDescription: page.metaDescription
308
+ },
309
+ layout: layout ? {
310
+ id: layout.id,
311
+ layoutId: layout.layoutId
312
+ } : null,
313
+ layoutIdentity: getLayoutIdentity(result.data, result.source),
314
+ project: {
315
+ id: project.id,
316
+ name: projectName
317
+ },
318
+ loaderData: {
319
+ faviconUrl: result.data.faviconUrl,
320
+ origin: result.data.origin
321
+ },
322
+ head,
323
+ dehydratedState
324
+ }, null, 2)}\n`, { headers });
325
+ } catch (error) {
326
+ if (isNotFoundError(error)) return Response.json({
327
+ kind: "page",
328
+ message: "No Camox page on this path",
329
+ pathname
330
+ }, { status: 404 });
331
+ throw error;
332
+ }
333
+ }
334
+ async function handleCamoxRequest(request, options = {}) {
335
+ const pathname = getRuntimePathname(request.url, options.runtimeBasePath);
336
+ if (!pathname) return null;
337
+ const match = matchRuntimeRoute(pathname);
338
+ if (match.kind === "health") return new Response("ok\n", { headers: { "Content-Type": "text/plain; charset=utf-8" } });
339
+ if (match.kind === "registry") {
340
+ const camoxApp = await options.getCamoxApp?.();
341
+ if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
342
+ return Response.json({
343
+ blocks: camoxApp.getBlocks().map((block) => block._internal.id),
344
+ layouts: camoxApp.getLayouts().map((layout) => layout._internal.id)
345
+ });
346
+ }
347
+ if (match.kind === "og") return createOgResponse(request, options);
348
+ if (match.kind === "sitemap") return createSitemapResponse(request, options);
349
+ if (match.kind === "data") return createPageDataResponse({
350
+ options,
351
+ pathname: normalizePagePath(new URL(request.url).searchParams.get("path")),
352
+ request
353
+ });
354
+ if (match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested") return createStudioHtmlResponse({
355
+ match,
356
+ options,
357
+ request
358
+ });
359
+ if (match.kind === "page") {
360
+ if (options.apiUrl && options.projectSlug) {
361
+ const markdownResponse = await createMarkdownResponse({
362
+ apiUrl: options.apiUrl,
363
+ environmentName: options.environmentName,
364
+ pathname: match.pathname,
365
+ projectSlug: options.projectSlug,
366
+ request
367
+ });
368
+ if (markdownResponse) return markdownResponse;
369
+ }
370
+ return createPageHtmlResponse({
371
+ options,
372
+ pathname: match.pathname,
373
+ request
374
+ });
375
+ }
376
+ return Response.json({
377
+ message: "Camox runtime route matched.",
378
+ ...match
379
+ }, { status: 501 });
380
+ }
42
381
 
43
382
  //#endregion
44
- export { createPageHeadInput };
383
+ export { createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };
@@ -1,3 +1,26 @@
1
1
  import { POSTHOG_PUBLIC_KEY } from "@camox/api-contract";
2
2
 
3
- export { };
3
+ //#region src/lib/telemetry.ts
4
+ let client = null;
5
+ async function getClient() {
6
+ if (__CAMOX_TELEMETRY_DISABLED__) return null;
7
+ if (!client) {
8
+ const { PostHog } = await import("posthog-node");
9
+ client = new PostHog(POSTHOG_PUBLIC_KEY, { host: "https://us.i.posthog.com" });
10
+ }
11
+ return client;
12
+ }
13
+ /** Server-side tracking for page views (used in pageRoute.tsx) */
14
+ async function trackEvent(event, properties) {
15
+ const posthog = await getClient();
16
+ if (!posthog) return;
17
+ const distinctId = properties.projectId ?? "unknown";
18
+ posthog.capture({
19
+ distinctId,
20
+ event,
21
+ properties
22
+ });
23
+ }
24
+
25
+ //#endregion
26
+ export { trackEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camox",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "camox": "./bin/camox.mjs"
@@ -157,9 +157,9 @@
157
157
  "streamdown": "^2.5.0",
158
158
  "unhead": "^3.1.7",
159
159
  "zod": "^4.4.3",
160
- "@camox/api-contract": "0.36.0",
161
- "@camox/cli": "0.36.0",
162
- "@camox/ui": "0.36.0"
160
+ "@camox/cli": "0.36.1",
161
+ "@camox/api-contract": "0.36.1",
162
+ "@camox/ui": "0.36.1"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@babel/core": "^7.29.0",