camox 0.40.0 → 0.41.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 (97) hide show
  1. package/dist/components/PlatformLabel.js +62 -0
  2. package/dist/core/components/lexical/GradientNode.js +126 -0
  3. package/dist/core/components/lexical/InlineLexicalEditor.js +37 -56
  4. package/dist/core/components/lexical/InlineStylesPlugin.js +183 -0
  5. package/dist/core/components/lexical/SelectionBroadcaster.js +49 -46
  6. package/dist/core/components/lexical/SidebarLexicalEditor.js +197 -151
  7. package/dist/core/components/lexical/editorConfig.js +25 -7
  8. package/dist/core/components/lexical/selectTextLink.js +21 -0
  9. package/dist/core/createApp.d.ts +30 -17
  10. package/dist/core/createApp.js +14 -3
  11. package/dist/core/createBlock.d.ts +2 -2
  12. package/dist/core/createBlock.js +18 -15
  13. package/dist/core/createLayout.d.ts +48 -4
  14. package/dist/core/createLayout.js +51 -23
  15. package/dist/core/derivedRoutes.js +36 -0
  16. package/dist/core/editing/createEditableBlock.d.ts +9 -8
  17. package/dist/core/editing/createEditableBlock.js +167 -168
  18. package/dist/core/lib/lexicalReact.d.ts +1 -26
  19. package/dist/core/lib/lexicalReact.js +46 -60
  20. package/dist/core/lib/lexicalState.js +164 -81
  21. package/dist/core/lib/modifierFormats.js +4 -15
  22. package/dist/core/lib/textStyles.d.ts +27 -0
  23. package/dist/core/lib/textStyles.js +47 -0
  24. package/dist/features/navigation/navigation.d.ts +2 -0
  25. package/dist/features/navigation/navigation.js +51 -46
  26. package/dist/features/page/DerivedPageContent.js +61 -0
  27. package/dist/features/page/PublishedPageContent.js +52 -83
  28. package/dist/features/page/SharedLayoutContent.js +66 -0
  29. package/dist/features/preview/CamoxPreview.d.ts +16 -1
  30. package/dist/features/preview/CamoxPreview.js +242 -265
  31. package/dist/features/preview/DerivedPreview.js +137 -0
  32. package/dist/features/preview/EditablePageExperience.js +25 -8
  33. package/dist/features/preview/components/AddBlockSidebar.js +43 -33
  34. package/dist/features/preview/components/AssetLightbox.js +1 -1
  35. package/dist/features/preview/components/BlockThumbnail.js +192 -0
  36. package/dist/features/preview/components/CreatePageModal.js +123 -115
  37. package/dist/features/preview/components/DerivedLayoutDialog.js +100 -0
  38. package/dist/features/preview/components/DerivedLayoutSidebar.d.ts +5 -0
  39. package/dist/features/preview/components/DerivedLayoutSidebar.js +163 -0
  40. package/dist/features/preview/components/DerivedPageInfoSidebar.js +57 -0
  41. package/dist/features/preview/components/FieldToolbar.js +42 -20
  42. package/dist/features/preview/components/Frame.js +133 -140
  43. package/dist/features/preview/components/LeftSidebar.js +42 -38
  44. package/dist/features/preview/components/MobilePreviewDrawer.js +4 -4
  45. package/dist/features/preview/components/Overlays.js +57 -42
  46. package/dist/features/preview/components/PageInfoSidebar.js +63 -55
  47. package/dist/features/preview/components/PageNavigatorSidebar.js +34 -483
  48. package/dist/features/preview/components/PagePicker.js +205 -153
  49. package/dist/features/preview/components/PageTree.js +298 -206
  50. package/dist/features/preview/components/PreviewActivation.js +78 -0
  51. package/dist/features/preview/components/PreviewPanel.js +67 -35
  52. package/dist/features/preview/components/PreviewToolbar.js +218 -232
  53. package/dist/features/preview/components/PublicationRow.js +431 -0
  54. package/dist/features/preview/components/PublishDialog.js +180 -97
  55. package/dist/features/preview/components/RightSidebar.js +19 -34
  56. package/dist/features/preview/components/previewStyles.js +8 -0
  57. package/dist/features/preview/components/useUpdateBlockPosition.js +7 -13
  58. package/dist/features/preview/previewConstants.js +8 -0
  59. package/dist/features/preview/previewStore.js +0 -10
  60. package/dist/features/preview/publication.js +71 -0
  61. package/dist/features/provider/AuthenticatedCamoxProvider.js +2 -1
  62. package/dist/features/provider/CoreCamoxProvider.js +11 -2
  63. package/dist/features/provider/LocalhostPreviewProvider.js +6 -7
  64. package/dist/features/routes/pageRuntime.d.ts +10 -0
  65. package/dist/features/routes/pageRuntime.js +7 -9
  66. package/dist/features/runtime/PreviewDocumentContext.js +7 -0
  67. package/dist/features/runtime/RuntimeChrome.js +137 -0
  68. package/dist/features/runtime/SharedChromeContext.js +7 -0
  69. package/dist/features/runtime/navigationRequest.js +21 -0
  70. package/dist/features/runtime/navigationTarget.js +23 -0
  71. package/dist/features/runtime/pageApp.js +196 -121
  72. package/dist/features/runtime/pageNavigation.js +79 -130
  73. package/dist/features/runtime/runtime.d.ts +11 -0
  74. package/dist/features/runtime/runtime.js +183 -47
  75. package/dist/features/runtime/studioApp.d.ts +2 -11
  76. package/dist/features/runtime/studioApp.js +14 -177
  77. package/dist/features/studio/CamoxStudio.js +27 -13
  78. package/dist/features/studio/components/Navbar.js +80 -74
  79. package/dist/features/studio/studioTheme.js +25 -0
  80. package/dist/features/studio/useTheme.js +94 -109
  81. package/dist/features/vite/appGeneration.js +12 -3
  82. package/dist/features/vite/definitionsSync.js +4 -2
  83. package/dist/features/vite/layoutGeneration.js +38 -0
  84. package/dist/features/vite/runtimeDev.js +3 -1
  85. package/dist/lib/auth.js +15 -13
  86. package/dist/lib/normalized-data.js +4 -4
  87. package/dist/lib/platform.js +13 -0
  88. package/dist/lib/utils.js +90 -38
  89. package/dist/studio-overlays.css +5 -40
  90. package/dist/studio.css +1 -1
  91. package/package.json +8 -4
  92. package/skills/camox/SKILL.md +2 -0
  93. package/skills/camox/references/block-definitions.md +31 -12
  94. package/skills/camox/references/layout-definitions.md +2 -0
  95. package/dist/core/lib/modifiers.js +0 -24
  96. package/dist/features/preview/previewQueryFns.js +0 -23
  97. package/dist/features/runtime/studioNavigation.js +0 -120
@@ -1,168 +1,117 @@
1
1
  import { NavigationProvider } from "../navigation/navigation.js";
2
2
  import { createPageHeadInput } from "./runtime.js";
3
+ import { createNavigationRequestTracker } from "./navigationRequest.js";
4
+ import { getNavigationTarget, runtimePath } from "./navigationTarget.js";
3
5
  import { hydrate } from "@tanstack/react-query";
4
6
  import * as React from "react";
5
7
  import { jsx } from "react/jsx-runtime";
6
8
  import { createHead, renderDOMHead } from "@unhead/react/client";
7
9
 
8
10
  //#region src/features/runtime/pageNavigation.tsx
9
- function normalizeRuntimeBasePath(basePath) {
10
- if (!basePath || basePath === "/") return "";
11
- return basePath.startsWith("/") ? basePath.replace(/\/+$/, "") : `/${basePath.replace(/\/+$/, "")}`;
12
- }
13
- function normalizePagePath(pathname, runtimeBasePath) {
14
- const basePath = normalizeRuntimeBasePath(runtimeBasePath);
15
- if (!basePath) return pathname || "/";
16
- if (pathname === basePath) return "/";
17
- if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length) || "/";
18
- return pathname || "/";
19
- }
20
- function toRuntimePathname(pagePathname, runtimeBasePath) {
21
- const basePath = normalizeRuntimeBasePath(runtimeBasePath);
22
- const normalizedPathname = pagePathname.startsWith("/") ? pagePathname : `/${pagePathname}`;
23
- if (!basePath) return normalizedPathname;
24
- if (normalizedPathname === "/") return `${basePath}/`;
25
- if (normalizedPathname.startsWith(`${basePath}/`)) return normalizedPathname;
26
- return `${basePath}${normalizedPathname}`;
27
- }
28
- function getRuntimeLocation(runtimeBasePath) {
29
- return {
30
- hash: window.location.hash,
31
- href: window.location.href,
32
- pathname: normalizePagePath(window.location.pathname, runtimeBasePath),
33
- search: window.location.search
34
- };
35
- }
36
- function isReservedPagePath(pathname) {
37
- return pathname.startsWith("/_camox/") || pathname === "/_camox" || pathname === "/camox" || pathname.startsWith("/camox/") || pathname === "/og" || pathname === "/sitemap.xml";
38
- }
39
- function getClientNavigationTarget(to, runtimeBasePath) {
40
- const url = new URL(to, window.location.href);
41
- if (url.origin !== window.location.origin) return null;
42
- const pagePathname = normalizePagePath(url.pathname, runtimeBasePath);
43
- if (isReservedPagePath(pagePathname)) return null;
44
- url.pathname = toRuntimePathname(pagePathname, runtimeBasePath);
45
- return url;
46
- }
47
- async function fetchPageData(pagePathname, runtimeBasePath) {
48
- const dataUrl = new URL(toRuntimePathname("/_camox/data", runtimeBasePath), window.location.origin);
49
- dataUrl.searchParams.set("path", pagePathname);
50
- const response = await fetch(dataUrl, {
51
- headers: { Accept: "application/json" },
52
- credentials: "same-origin"
53
- });
54
- if (!response.ok) throw new Error(`Page data request failed: ${response.status}`);
55
- return await response.json();
56
- }
57
- function isPageHead(head) {
58
- return !!head && typeof head === "object";
59
- }
60
- function updateHead(head, manager) {
61
- if (!isPageHead(head)) return;
62
- if (!manager.removedServerPageHead) {
63
- document.querySelectorAll("[data-camox-page-head]").forEach((element) => element.remove());
64
- manager.removedServerPageHead = true;
65
- }
66
- manager.entry?.dispose();
67
- manager.entry = manager.head.push(createPageHeadInput(head));
68
- renderDOMHead(manager.head);
69
- }
70
- function hasSameLayoutIdentity(current, next) {
71
- return !!current && !!next && current.id === next.id && current.layoutId === next.layoutId && current.version === next.version;
72
- }
73
- function isLayoutBlockQuery(query, layoutBlockIds) {
74
- if (!query || typeof query !== "object") return false;
75
- const queryKey = query.queryKey;
76
- return Array.isArray(queryKey) && queryKey[0] === "camox" && queryKey[1] === "blocks" && queryKey[2] === "get" && typeof queryKey[3] === "number" && layoutBlockIds.has(queryKey[3]);
77
- }
78
- function preserveStableLayoutBlockCaches(dehydratedState, currentLayout, nextLayout) {
79
- if (!hasSameLayoutIdentity(currentLayout, nextLayout)) return dehydratedState;
80
- if (!dehydratedState || typeof dehydratedState !== "object") return dehydratedState;
81
- if (!Array.isArray(dehydratedState.queries)) return dehydratedState;
82
- const layoutBlockIds = new Set(currentLayout?.blockIds ?? []);
11
+ function preserveStableLayoutBlockCaches(dehydratedState, current, next) {
12
+ if (!current || !next || current.id !== next.id || current.layoutId !== next.layoutId || current.version !== next.version) return dehydratedState;
13
+ if (!dehydratedState || typeof dehydratedState !== "object" || !("queries" in dehydratedState) || !Array.isArray(dehydratedState.queries)) return dehydratedState;
14
+ const ids = new Set(current.blockIds);
83
15
  return {
84
16
  ...dehydratedState,
85
- queries: dehydratedState.queries.filter((query) => !isLayoutBlockQuery(query, layoutBlockIds))
17
+ queries: dehydratedState.queries.filter((query) => {
18
+ const key = query.queryKey;
19
+ return !(key?.[0] === "camox" && key[1] === "blocks" && key[2] === "get" && typeof key[3] === "number" && ids.has(key[3]));
20
+ })
86
21
  };
87
22
  }
88
23
  function PageNavigationProvider({ children, initialInput, queryClient }) {
89
- const headManagerRef = React.useRef(null);
90
- if (!headManagerRef.current) headManagerRef.current = {
91
- head: createHead(),
92
- removedServerPageHead: false
93
- };
94
- const headManager = headManagerRef.current;
95
- const currentLayoutIdentityRef = React.useRef(initialInput.layoutIdentity);
96
- const navigate = React.useCallback(async ({ replace, to }) => {
97
- const target = getClientNavigationTarget(to, initialInput.runtimeBasePath);
24
+ const [input, setInput] = React.useState(initialInput);
25
+ const current = React.useRef(initialInput);
26
+ const [requests] = React.useState(createNavigationRequestTracker);
27
+ const headManager = React.useRef(null);
28
+ const navigate = React.useCallback(async ({ to, replace, pop = false }) => {
29
+ const request = requests.begin();
30
+ const target = getNavigationTarget(to, window.location.href, initialInput.runtimeBasePath);
98
31
  if (!target) {
99
- window.location.assign(to);
32
+ if (replace) window.location.replace(to);
33
+ else window.location.assign(to);
100
34
  return;
101
35
  }
102
- const pagePathname = normalizePagePath(target.pathname, initialInput.runtimeBasePath);
103
36
  try {
104
- const data = await fetchPageData(pagePathname, initialInput.runtimeBasePath);
105
- if (data.dehydratedState) hydrate(queryClient, preserveStableLayoutBlockCaches(data.dehydratedState, currentLayoutIdentityRef.current, data.layoutIdentity));
106
- currentLayoutIdentityRef.current = data.layoutIdentity ?? null;
107
- updateHead(data.head, headManager);
37
+ let next = current.current;
38
+ if (target.pathname !== next.pathname || pop) {
39
+ const url = new URL(runtimePath("/_camox/data", initialInput.runtimeBasePath), window.location.origin);
40
+ url.searchParams.set("path", target.pathname);
41
+ const response = await fetch(url, {
42
+ credentials: "same-origin",
43
+ headers: { Accept: "application/json" },
44
+ signal: request.signal
45
+ });
46
+ if (!response.ok) throw new Error(`Navigation failed: ${response.status}`);
47
+ next = await response.json();
48
+ if (next.pathname !== target.pathname || !next.dehydratedState) throw new Error("Invalid navigation response");
49
+ }
50
+ if (!request.isCurrent()) return;
51
+ hydrate(queryClient, preserveStableLayoutBlockCaches(next.dehydratedState, current.current.layoutIdentity, next.layoutIdentity));
52
+ next = {
53
+ ...next,
54
+ href: target.url.href
55
+ };
56
+ current.current = next;
57
+ if (!pop) if (replace) window.history.replaceState(null, "", target.url.href);
58
+ else window.history.pushState(null, "", target.url.href);
59
+ if (!headManager.current) {
60
+ document.querySelectorAll("[data-camox-page-head]").forEach((element) => element.remove());
61
+ headManager.current = { head: createHead() };
62
+ }
63
+ const manager = headManager.current;
64
+ manager.entry?.dispose();
65
+ manager.entry = manager.head.push(createPageHeadInput(next.head ?? {}));
66
+ renderDOMHead(manager.head);
67
+ React.startTransition(() => setInput(next));
68
+ if (!pop && !replace && !target.url.hash) window.scrollTo({ top: 0 });
108
69
  } catch {
109
- window.location.assign(target.href);
110
- return;
70
+ if (!request.isCurrent()) return;
71
+ if (pop) window.location.reload();
72
+ else if (replace) window.location.replace(target.url.href);
73
+ else window.location.assign(target.url.href);
111
74
  }
112
- if (replace) window.history.replaceState(null, "", target.href);
113
- else {
114
- window.history.pushState(null, "", target.href);
115
- window.scrollTo({ top: 0 });
116
- }
117
- window.dispatchEvent(new Event("camox:navigation"));
118
75
  }, [
119
- headManager,
120
76
  initialInput.runtimeBasePath,
121
- queryClient
77
+ queryClient,
78
+ requests
122
79
  ]);
123
80
  React.useEffect(() => {
124
81
  const onClick = (event) => {
125
- if (event.defaultPrevented) return;
126
- if (event.button !== 0) return;
127
- if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
82
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
128
83
  const anchor = event.target?.closest("a");
129
- if (!anchor) return;
130
- if (anchor.hasAttribute("download")) return;
131
- if (anchor.target && anchor.target !== "_self") return;
132
- const target_0 = getClientNavigationTarget(anchor.href, initialInput.runtimeBasePath);
133
- if (!target_0) return;
84
+ if (!anchor || anchor.hasAttribute("download") || anchor.target && anchor.target !== "_self") return;
85
+ if (!getNavigationTarget(anchor.href, window.location.href, initialInput.runtimeBasePath)) return;
134
86
  event.preventDefault();
135
- navigate({ to: target_0.href });
87
+ navigate({ to: anchor.href });
136
88
  };
89
+ const onPopState = () => void navigate({
90
+ to: window.location.href,
91
+ pop: true
92
+ });
137
93
  document.addEventListener("click", onClick);
138
- return () => document.removeEventListener("click", onClick);
139
- }, [navigate]);
140
- React.useEffect(() => {
141
- const onPopState = () => {
142
- fetchPageData(normalizePagePath(window.location.pathname, initialInput.runtimeBasePath), initialInput.runtimeBasePath).then((data_0) => {
143
- if (data_0.dehydratedState) hydrate(queryClient, preserveStableLayoutBlockCaches(data_0.dehydratedState, currentLayoutIdentityRef.current, data_0.layoutIdentity));
144
- currentLayoutIdentityRef.current = data_0.layoutIdentity ?? null;
145
- updateHead(data_0.head, headManager);
146
- window.dispatchEvent(new Event("camox:navigation"));
147
- }).catch(() => window.location.reload());
148
- };
149
94
  window.addEventListener("popstate", onPopState);
150
- return () => window.removeEventListener("popstate", onPopState);
95
+ return () => {
96
+ document.removeEventListener("click", onClick);
97
+ window.removeEventListener("popstate", onPopState);
98
+ requests.cancel();
99
+ };
151
100
  }, [
152
- headManager,
101
+ navigate,
153
102
  initialInput.runtimeBasePath,
154
- queryClient
103
+ requests
155
104
  ]);
105
+ const url_0 = new URL(input.href);
156
106
  return /* @__PURE__ */ jsx(NavigationProvider, {
157
- getLocation: React.useCallback(() => getRuntimeLocation(initialInput.runtimeBasePath), [initialInput.runtimeBasePath]),
158
- initialLocation: {
159
- hash: new URL(initialInput.href).hash,
160
- href: initialInput.href,
161
- pathname: initialInput.pathname,
162
- search: new URL(initialInput.href).search
107
+ location: {
108
+ href: input.href,
109
+ pathname: input.pathname,
110
+ hash: url_0.hash,
111
+ search: url_0.search
163
112
  },
164
113
  navigate,
165
- children
114
+ children: children(input)
166
115
  });
167
116
  }
168
117
 
@@ -1,5 +1,6 @@
1
1
  import { CamoxApp } from "../../core/createApp.js";
2
2
  import { CamoxDocument } from "../../core/defineDocument.js";
3
+ import { createServerApiClient } from "../routes/pageRuntime.js";
3
4
  import { StudioRenderInput } from "./studioApp.js";
4
5
  import { UseHeadInput } from "unhead/types";
5
6
 
@@ -16,6 +17,15 @@ interface LayoutIdentity {
16
17
  version: string;
17
18
  }
18
19
  interface PageRenderInput {
20
+ previewDocument?: string;
21
+ routeKind?: "studio" | "studio-content" | "studio-nested";
22
+ project?: Awaited<ReturnType<ReturnType<typeof createServerApiClient>["projects"]["getBySlug"]>>;
23
+ presentation?: "public" | "studio";
24
+ derived?: {
25
+ layoutId: string;
26
+ data: unknown;
27
+ layout: Awaited<ReturnType<ReturnType<typeof createServerApiClient>["layouts"]["get"]>>["layout"];
28
+ };
19
29
  apiUrl: string;
20
30
  authenticationUrl: string;
21
31
  dehydratedState: unknown;
@@ -42,6 +52,7 @@ interface RuntimeOptions {
42
52
  renderStudio?: (input: StudioRenderInput) => Promise<string>;
43
53
  runtimeBasePath?: string;
44
54
  stylesheetUrl?: string;
55
+ studioStylesheetUrl?: string;
45
56
  }
46
57
  declare function getRuntimePathname(url: string, runtimeBasePath?: string): string | null;
47
58
  declare function matchRuntimeRoute(pathname: string): RuntimeRouteMatch;
@@ -1,4 +1,7 @@
1
- import { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isNotFoundError, loadCamoxPageForRequest } from "../routes/pageRuntime.js";
1
+ import { PLATFORM_SCRIPT } from "../../lib/platform.js";
2
+ import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
3
+ import { matchDerivedLayout } from "../../core/derivedRoutes.js";
4
+ import { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isAuthSessionError, isNotFoundError, loadCamoxPageForRequest, seedBlockCaches } from "../routes/pageRuntime.js";
2
5
  import { QueryClient, dehydrate } from "@tanstack/react-query";
3
6
  import { createHead, renderSSRHead } from "@unhead/react/server";
4
7
 
@@ -40,8 +43,7 @@ function normalizePagePath(path) {
40
43
  if (!path) return "/";
41
44
  return path.startsWith("/") ? path : `/${path}`;
42
45
  }
43
- function getLayoutIdentity(data, source) {
44
- const layout = data.page.layout;
46
+ function getLayoutIdentity(layout, source) {
45
47
  if (!layout) return null;
46
48
  const version = source === "live" ? String(layout.livePublishedCheckpointId ?? "none") : String(layout.contentUpdatedAt ?? layout.updatedAt);
47
49
  return {
@@ -173,10 +175,15 @@ function createPageHeadInput(head) {
173
175
  link: pageLinks
174
176
  };
175
177
  }
176
- function renderHead(document, pageHead, stylesheetUrl) {
178
+ function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl) {
177
179
  const head = createHead();
178
180
  head.push(createDefaultHeadInput(stylesheetUrl));
179
181
  head.push(document);
182
+ if (studioStylesheetUrl) head.push({ link: [{
183
+ rel: "stylesheet",
184
+ href: studioStylesheetUrl,
185
+ "data-camox-studio": "true"
186
+ }] });
180
187
  head.push(pageHead);
181
188
  return renderSSRHead(head);
182
189
  }
@@ -187,6 +194,20 @@ function renderClientEntryScript(src) {
187
194
  if (!src) return "";
188
195
  return `<script type="module" src="${escapeXml(src)}"><\/script>`;
189
196
  }
197
+ async function preparePreviewDocument(input, options, document, content) {
198
+ const siteHead = renderHead(document, createPageHeadInput(input.head ?? {}), options.stylesheetUrl);
199
+ const href = new URL(withRuntimeBasePath(input.pathname, options.runtimeBasePath), input.href).href;
200
+ const siteHtml = content ?? (options.renderPage ? await options.renderPage({
201
+ ...input,
202
+ href,
203
+ presentation: "public",
204
+ previewDocument: void 0
205
+ }) : "");
206
+ input.previewDocument = `<!doctype html><html${siteHead.htmlAttrs}><head><base href="${escapeXml(href)}">${siteHead.headTags}</head><body${siteHead.bodyAttrs}>${siteHead.bodyTagsOpen}<div id="root" data-camox-preview-root>${siteHtml}</div>${siteHead.bodyTags}</body></html>`;
207
+ }
208
+ function renderStudioThemeScript(enabled) {
209
+ return enabled ? `<script data-camox-studio>${STUDIO_THEME_SCRIPT}${PLATFORM_SCRIPT}<\/script>` : "";
210
+ }
190
211
  async function createPageHtmlResponse({ options, pathname, request }) {
191
212
  if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderPage) return createPageDataResponse({
192
213
  options,
@@ -204,31 +225,39 @@ async function createPageHtmlResponse({ options, pathname, request }) {
204
225
  projectSlug: options.projectSlug,
205
226
  queryClient
206
227
  });
207
- const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
228
+ const headers = new Headers({
229
+ "Content-Type": "text/html; charset=utf-8",
230
+ Vary: "Cookie"
231
+ });
232
+ if (getServerAuthCookieHeader(request.headers)) headers.set("Cache-Control", "private, no-store");
208
233
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
209
234
  const camoxApp = await options.getCamoxApp?.();
210
235
  const document = await options.getDocument?.() ?? {};
211
236
  const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : {};
212
237
  const dehydratedState = dehydrate(queryClient);
213
238
  const pageRenderInput = {
239
+ project: result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
240
+ presentation: result.source === "draft" ? "studio" : "public",
214
241
  apiUrl: options.apiUrl,
215
242
  authenticationUrl: options.authenticationUrl,
216
243
  dehydratedState,
217
244
  environmentName: options.environmentName,
218
245
  head,
219
246
  href: new URL(request.url).href,
220
- layoutIdentity: getLayoutIdentity(result.data, result.source),
247
+ layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
221
248
  loaderData: result.data,
222
249
  pathname,
223
250
  projectSlug: options.projectSlug,
224
251
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
225
252
  source: result.source
226
253
  };
254
+ await preparePreviewDocument(pageRenderInput, options, document, result.source === "live" ? "" : void 0);
227
255
  const appHtml = await options.renderPage(pageRenderInput);
228
- const renderedHead = renderHead(document, createPageHeadInput(head), options.stylesheetUrl);
256
+ const renderedHead = renderHead(result.source === "draft" ? {} : document, createPageHeadInput(head), result.source === "draft" ? void 0 : options.stylesheetUrl, result.source === "draft" ? options.studioStylesheetUrl : void 0);
229
257
  return new Response(`<!doctype html>
230
258
  <html${renderedHead.htmlAttrs}>
231
259
  <head>
260
+ ${renderStudioThemeScript(result.source === "draft")}
232
261
  ${renderedHead.headTags}
233
262
  <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(pageRenderInput)}<\/script>
234
263
  </head>
@@ -240,18 +269,102 @@ ${renderClientEntryScript(options.pageClientEntryUrl)}
240
269
  </body>
241
270
  </html>`, { headers });
242
271
  } catch (error) {
243
- if (isNotFoundError(error)) return new Response("No Camox page on this path", { status: 404 });
272
+ if (isNotFoundError(error)) {
273
+ const derived = await createDerivedResponse(options, pathname, request);
274
+ if (derived) return derived;
275
+ return new Response("No Camox page on this path", { status: 404 });
276
+ }
277
+ throw error;
278
+ }
279
+ }
280
+ async function createDerivedResponse(options, pathname, request, dataOnly = false) {
281
+ const app = await options.getCamoxApp?.();
282
+ const match = app && matchDerivedLayout(app.getLayouts(), pathname);
283
+ if (!match || !dataOnly && !options.renderPage) return null;
284
+ try {
285
+ const data = await match.layout._internal.loader({ params: match.params });
286
+ const serializedData = JSON.parse(JSON.stringify(data));
287
+ const queryClient = new QueryClient();
288
+ const authCookieHeader = getServerAuthCookieHeader(request.headers);
289
+ let source = authCookieHeader ? "draft" : "live";
290
+ let clearAuthCookie = false;
291
+ const loadLayout = (source) => createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: source === "draft" ? authCookieHeader : void 0 }).layouts.get({
292
+ projectSlug: options.projectSlug,
293
+ layoutId: match.layout._internal.id,
294
+ source
295
+ });
296
+ const shared = await loadLayout(source).catch(async (error) => {
297
+ if (source !== "draft" || !isAuthSessionError(error)) throw error;
298
+ source = "live";
299
+ clearAuthCookie = true;
300
+ return loadLayout("live");
301
+ });
302
+ seedBlockCaches(queryClient, shared, source);
303
+ const input = {
304
+ project: source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
305
+ presentation: source === "draft" ? "studio" : "public",
306
+ apiUrl: options.apiUrl,
307
+ authenticationUrl: options.authenticationUrl,
308
+ projectSlug: options.projectSlug,
309
+ environmentName: options.environmentName,
310
+ dehydratedState: dehydrate(queryClient),
311
+ head: { meta: [{ title: match.layout._internal.title }] },
312
+ href: request.url,
313
+ layoutIdentity: getLayoutIdentity(shared.layout, source),
314
+ loaderData: null,
315
+ pathname,
316
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
317
+ source,
318
+ derived: {
319
+ layoutId: match.layout._internal.id,
320
+ data: serializedData,
321
+ layout: shared.layout
322
+ }
323
+ };
324
+ const document = await options.getDocument?.() ?? {};
325
+ await preparePreviewDocument(input, options, document, source === "live" && !dataOnly ? "" : void 0);
326
+ if (dataOnly) return Response.json(input, { headers: {
327
+ "Cache-Control": "private, no-store",
328
+ Vary: "Cookie",
329
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
330
+ } });
331
+ const html = await options.renderPage(input);
332
+ const head = renderHead(source === "draft" ? {} : document, { title: match.layout._internal.title }, source === "draft" ? void 0 : options.stylesheetUrl, source === "draft" ? options.studioStylesheetUrl : void 0);
333
+ return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(source === "draft")}${head.headTags}<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(input)}<\/script></head><body${head.bodyAttrs}>${head.bodyTagsOpen}<div id="root">${html}</div>${head.bodyTags}${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
334
+ "Content-Type": "text/html; charset=utf-8",
335
+ Vary: "Cookie",
336
+ ...authCookieHeader ? { "Cache-Control": "private, no-store" } : {},
337
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
338
+ } });
339
+ } catch (error) {
340
+ if (isNotFoundError(error)) return new Response("Page not found", { status: 404 });
244
341
  throw error;
245
342
  }
246
343
  }
247
- async function createStudioHtmlResponse({ match, options, request }) {
248
- if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderStudio) return Response.json({
344
+ async function createStudioHtmlResponse({ match, options, request, dataOnly = false }) {
345
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !dataOnly && !options.renderStudio) return Response.json({
249
346
  message: "Camox Studio renderer is not configured.",
250
347
  ...match
251
348
  }, { status: 500 });
252
349
  const queryClient = new QueryClient();
253
- const document = await options.getDocument?.() ?? {};
350
+ const authCookieHeader = getServerAuthCookieHeader(request.headers);
351
+ let project;
352
+ let presentation = "public";
353
+ let clearAuthCookie = false;
354
+ if (authCookieHeader) try {
355
+ project = await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug });
356
+ presentation = "studio";
357
+ } catch (error) {
358
+ if (!isAuthSessionError(error)) throw error;
359
+ clearAuthCookie = true;
360
+ }
254
361
  const studioRenderInput = {
362
+ project,
363
+ source: "live",
364
+ head: { meta: [{ title: "Camox Studio" }] },
365
+ layoutIdentity: null,
366
+ loaderData: null,
367
+ presentation,
255
368
  apiUrl: options.apiUrl,
256
369
  authenticationUrl: options.authenticationUrl,
257
370
  dehydratedState: dehydrate(queryClient),
@@ -262,11 +375,17 @@ async function createStudioHtmlResponse({ match, options, request }) {
262
375
  routeKind: match.kind,
263
376
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
264
377
  };
378
+ if (dataOnly) return Response.json(studioRenderInput, { headers: {
379
+ "Cache-Control": "private, no-store",
380
+ Vary: "Cookie",
381
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
382
+ } });
265
383
  const appHtml = await options.renderStudio(studioRenderInput);
266
- const renderedHead = renderHead(document, {}, options.stylesheetUrl);
384
+ const renderedHead = renderHead({}, { title: "Camox Studio" }, void 0, presentation === "studio" ? options.studioStylesheetUrl : void 0);
267
385
  return new Response(`<!doctype html>
268
386
  <html${renderedHead.htmlAttrs}>
269
387
  <head>
388
+ ${renderStudioThemeScript(presentation === "studio")}
270
389
  ${renderedHead.headTags}
271
390
  <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(studioRenderInput)}<\/script>
272
391
  </head>
@@ -276,7 +395,12 @@ ${renderedHead.bodyTagsOpen}
276
395
  ${renderedHead.bodyTags}
277
396
  ${renderClientEntryScript(options.studioClientEntryUrl)}
278
397
  </body>
279
- </html>`, { headers: { "Content-Type": "text/html; charset=utf-8" } });
398
+ </html>`, { headers: {
399
+ "Content-Type": "text/html; charset=utf-8",
400
+ "Cache-Control": "private, no-store",
401
+ Vary: "Cookie",
402
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
403
+ } });
280
404
  }
281
405
  async function createPageDataResponse({ options, pathname, request }) {
282
406
  if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
@@ -291,43 +415,44 @@ async function createPageDataResponse({ options, pathname, request }) {
291
415
  projectSlug: options.projectSlug,
292
416
  queryClient
293
417
  });
294
- const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });
418
+ const headers = new Headers({
419
+ "Content-Type": "application/json; charset=utf-8",
420
+ "Cache-Control": "private, no-store",
421
+ Vary: "Cookie"
422
+ });
295
423
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
296
424
  const camoxApp = await options.getCamoxApp?.();
297
425
  const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : null;
298
426
  const dehydratedState = dehydrate(queryClient);
299
- const { page, layout, projectName, project } = result.data.page;
300
- return new Response(`${JSON.stringify({
301
- kind: "page",
427
+ const project = result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0;
428
+ const input = {
429
+ apiUrl: options.apiUrl,
430
+ authenticationUrl: options.authenticationUrl ?? "",
431
+ environmentName: options.environmentName,
432
+ projectSlug: options.projectSlug,
433
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
434
+ href: request.url,
435
+ presentation: result.source === "draft" ? "studio" : "public",
436
+ source: result.source,
302
437
  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
- },
438
+ layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
439
+ project,
440
+ loaderData: result.data,
322
441
  head,
323
442
  dehydratedState
324
- }, null, 2)}\n`, { headers });
443
+ };
444
+ await preparePreviewDocument(input, options, await options.getDocument?.() ?? {});
445
+ return Response.json(input, { headers });
325
446
  } 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 });
447
+ if (isNotFoundError(error)) {
448
+ const derived = await createDerivedResponse(options, pathname, request, true);
449
+ if (derived) return derived;
450
+ return Response.json({
451
+ kind: "page",
452
+ message: "No Camox page on this path",
453
+ pathname
454
+ }, { status: 404 });
455
+ }
331
456
  throw error;
332
457
  }
333
458
  }
@@ -346,11 +471,22 @@ async function handleCamoxRequest(request, options = {}) {
346
471
  }
347
472
  if (match.kind === "og") return createOgResponse(request, options);
348
473
  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
- });
474
+ if (match.kind === "data") {
475
+ const targetPathname = normalizePagePath(new URL(request.url).searchParams.get("path"));
476
+ const targetMatch = matchRuntimeRoute(targetPathname);
477
+ if (targetMatch.kind.startsWith("studio")) return createStudioHtmlResponse({
478
+ match: targetMatch,
479
+ options,
480
+ request,
481
+ dataOnly: true
482
+ });
483
+ if (targetMatch.kind !== "page") return new Response("Not found", { status: 404 });
484
+ return createPageDataResponse({
485
+ options,
486
+ pathname: targetPathname,
487
+ request
488
+ });
489
+ }
354
490
  if (match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested") return createStudioHtmlResponse({
355
491
  match,
356
492
  options,
@@ -1,15 +1,6 @@
1
+ import { PageRenderInput } from "./runtime.js";
1
2
  import { QueryClient } from "@tanstack/react-query";
2
3
  //#region src/features/runtime/studioApp.d.ts
3
- interface StudioRenderInput {
4
- apiUrl: string;
5
- authenticationUrl: string;
6
- dehydratedState: unknown;
7
- environmentName?: string;
8
- href: string;
9
- pathname: string;
10
- projectSlug: string;
11
- routeKind: "studio" | "studio-content" | "studio-nested";
12
- runtimeBasePath: string;
13
- }
4
+ type StudioRenderInput = PageRenderInput;
14
5
  //#endregion
15
6
  export { StudioRenderInput };