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.
- package/dist/components/PlatformLabel.js +62 -0
- package/dist/core/components/lexical/GradientNode.js +126 -0
- package/dist/core/components/lexical/InlineLexicalEditor.js +37 -56
- package/dist/core/components/lexical/InlineStylesPlugin.js +183 -0
- package/dist/core/components/lexical/SelectionBroadcaster.js +49 -46
- package/dist/core/components/lexical/SidebarLexicalEditor.js +197 -151
- package/dist/core/components/lexical/editorConfig.js +25 -7
- package/dist/core/components/lexical/selectTextLink.js +21 -0
- package/dist/core/createApp.d.ts +30 -17
- package/dist/core/createApp.js +14 -3
- package/dist/core/createBlock.d.ts +2 -2
- package/dist/core/createBlock.js +18 -15
- package/dist/core/createLayout.d.ts +48 -4
- package/dist/core/createLayout.js +51 -23
- package/dist/core/derivedRoutes.js +36 -0
- package/dist/core/editing/createEditableBlock.d.ts +9 -8
- package/dist/core/editing/createEditableBlock.js +167 -168
- package/dist/core/lib/lexicalReact.d.ts +1 -26
- package/dist/core/lib/lexicalReact.js +46 -60
- package/dist/core/lib/lexicalState.js +164 -81
- package/dist/core/lib/modifierFormats.js +4 -15
- package/dist/core/lib/textStyles.d.ts +27 -0
- package/dist/core/lib/textStyles.js +47 -0
- package/dist/features/navigation/navigation.d.ts +2 -0
- package/dist/features/navigation/navigation.js +51 -46
- package/dist/features/page/DerivedPageContent.js +61 -0
- package/dist/features/page/PublishedPageContent.js +52 -83
- package/dist/features/page/SharedLayoutContent.js +66 -0
- package/dist/features/preview/CamoxPreview.d.ts +16 -1
- package/dist/features/preview/CamoxPreview.js +242 -265
- package/dist/features/preview/DerivedPreview.js +137 -0
- package/dist/features/preview/EditablePageExperience.js +25 -8
- package/dist/features/preview/components/AddBlockSidebar.js +43 -33
- package/dist/features/preview/components/AssetLightbox.js +1 -1
- package/dist/features/preview/components/BlockThumbnail.js +192 -0
- package/dist/features/preview/components/CreatePageModal.js +123 -115
- package/dist/features/preview/components/DerivedLayoutDialog.js +100 -0
- package/dist/features/preview/components/DerivedLayoutSidebar.d.ts +5 -0
- package/dist/features/preview/components/DerivedLayoutSidebar.js +163 -0
- package/dist/features/preview/components/DerivedPageInfoSidebar.js +57 -0
- package/dist/features/preview/components/FieldToolbar.js +42 -20
- package/dist/features/preview/components/Frame.js +133 -140
- package/dist/features/preview/components/LeftSidebar.js +42 -38
- package/dist/features/preview/components/MobilePreviewDrawer.js +4 -4
- package/dist/features/preview/components/Overlays.js +57 -42
- package/dist/features/preview/components/PageInfoSidebar.js +63 -55
- package/dist/features/preview/components/PageNavigatorSidebar.js +34 -483
- package/dist/features/preview/components/PagePicker.js +205 -153
- package/dist/features/preview/components/PageTree.js +298 -206
- package/dist/features/preview/components/PreviewActivation.js +78 -0
- package/dist/features/preview/components/PreviewPanel.js +67 -35
- package/dist/features/preview/components/PreviewToolbar.js +218 -232
- package/dist/features/preview/components/PublicationRow.js +431 -0
- package/dist/features/preview/components/PublishDialog.js +180 -97
- package/dist/features/preview/components/RightSidebar.js +19 -34
- package/dist/features/preview/components/previewStyles.js +8 -0
- package/dist/features/preview/components/useUpdateBlockPosition.js +7 -13
- package/dist/features/preview/previewConstants.js +8 -0
- package/dist/features/preview/previewStore.js +0 -10
- package/dist/features/preview/publication.js +71 -0
- package/dist/features/provider/AuthenticatedCamoxProvider.js +2 -1
- package/dist/features/provider/CoreCamoxProvider.js +11 -2
- package/dist/features/provider/LocalhostPreviewProvider.js +6 -7
- package/dist/features/routes/pageRuntime.d.ts +10 -0
- package/dist/features/routes/pageRuntime.js +7 -9
- package/dist/features/runtime/PreviewDocumentContext.js +7 -0
- package/dist/features/runtime/RuntimeChrome.js +137 -0
- package/dist/features/runtime/SharedChromeContext.js +7 -0
- package/dist/features/runtime/navigationRequest.js +21 -0
- package/dist/features/runtime/navigationTarget.js +23 -0
- package/dist/features/runtime/pageApp.js +196 -121
- package/dist/features/runtime/pageNavigation.js +79 -130
- package/dist/features/runtime/runtime.d.ts +11 -0
- package/dist/features/runtime/runtime.js +183 -47
- package/dist/features/runtime/studioApp.d.ts +2 -11
- package/dist/features/runtime/studioApp.js +14 -177
- package/dist/features/studio/CamoxStudio.js +27 -13
- package/dist/features/studio/components/Navbar.js +80 -74
- package/dist/features/studio/studioTheme.js +25 -0
- package/dist/features/studio/useTheme.js +94 -109
- package/dist/features/vite/appGeneration.js +12 -3
- package/dist/features/vite/definitionsSync.js +4 -2
- package/dist/features/vite/layoutGeneration.js +38 -0
- package/dist/features/vite/runtimeDev.js +3 -1
- package/dist/lib/auth.js +15 -13
- package/dist/lib/normalized-data.js +4 -4
- package/dist/lib/platform.js +13 -0
- package/dist/lib/utils.js +90 -38
- package/dist/studio-overlays.css +5 -40
- package/dist/studio.css +1 -1
- package/package.json +8 -4
- package/skills/camox/SKILL.md +2 -0
- package/skills/camox/references/block-definitions.md +31 -12
- package/skills/camox/references/layout-definitions.md +2 -0
- package/dist/core/lib/modifiers.js +0 -24
- package/dist/features/preview/previewQueryFns.js +0 -23
- 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
|
|
10
|
-
if (!
|
|
11
|
-
|
|
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) =>
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
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
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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.
|
|
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:
|
|
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 () =>
|
|
95
|
+
return () => {
|
|
96
|
+
document.removeEventListener("click", onClick);
|
|
97
|
+
window.removeEventListener("popstate", onPopState);
|
|
98
|
+
requests.cancel();
|
|
99
|
+
};
|
|
151
100
|
}, [
|
|
152
|
-
|
|
101
|
+
navigate,
|
|
153
102
|
initialInput.runtimeBasePath,
|
|
154
|
-
|
|
103
|
+
requests
|
|
155
104
|
]);
|
|
105
|
+
const url_0 = new URL(input.href);
|
|
156
106
|
return /* @__PURE__ */ jsx(NavigationProvider, {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 {
|
|
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(
|
|
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({
|
|
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))
|
|
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
|
|
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(
|
|
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: {
|
|
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({
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
}
|
|
443
|
+
};
|
|
444
|
+
await preparePreviewDocument(input, options, await options.getDocument?.() ?? {});
|
|
445
|
+
return Response.json(input, { headers });
|
|
325
446
|
} catch (error) {
|
|
326
|
-
if (isNotFoundError(error))
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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 };
|