camox 0.43.0 → 0.45.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 (412) hide show
  1. package/bin/camox.mjs +9 -1
  2. package/dist/components/AuthGate.js +0 -6
  3. package/dist/components/DeviceSelector.js +96 -0
  4. package/dist/components/PlatformLabel.js +20 -24
  5. package/dist/core/components/lexical/HighlightNode.js +1 -1
  6. package/dist/core/components/lexical/InlineContentEditable.js +15 -19
  7. package/dist/core/components/lexical/InlineLexicalEditor.js +54 -74
  8. package/dist/core/components/lexical/InlineStylesPlugin.js +57 -41
  9. package/dist/core/components/lexical/MaxLengthPlugin.js +59 -0
  10. package/dist/core/components/lexical/SelectionBroadcaster.js +55 -56
  11. package/dist/core/components/lexical/SidebarLexicalEditor.js +327 -234
  12. package/dist/core/components/lexical/SummaryToggleGuard.js +42 -0
  13. package/dist/core/components/lexical/TextLinkPopover.js +169 -180
  14. package/dist/core/createApp.d.ts +254 -299
  15. package/dist/core/createApp.js +123 -29
  16. package/dist/core/createBlock.d.ts +5 -5
  17. package/dist/core/createBlock.js +399 -152
  18. package/dist/core/createCollection.d.ts +55 -0
  19. package/dist/core/createCollection.js +93 -0
  20. package/dist/core/createLayout.d.ts +197 -61
  21. package/dist/core/createLayout.js +114 -56
  22. package/dist/core/defineDocument.d.ts +3 -5
  23. package/dist/core/derivedRoutes.js +49 -1
  24. package/dist/core/dom.d.ts +13 -4
  25. package/dist/core/dom.js +44 -1
  26. package/dist/core/editing/BlockEditingRuntime.d.ts +16 -0
  27. package/dist/core/editing/BlockEditingRuntime.js +6 -15
  28. package/dist/core/editing/CompleteBlockEditingRuntime.js +28 -23
  29. package/dist/core/editing/createEditableBlock.d.ts +820 -565
  30. package/dist/core/editing/createEditableBlock.js +2153 -971
  31. package/dist/core/editing/referenceWrites.js +51 -0
  32. package/dist/core/editing/useLatestRecord.js +90 -0
  33. package/dist/core/hooks/useArtifactHost.js +139 -0
  34. package/dist/core/hooks/useFieldSelection.js +43 -18
  35. package/dist/core/hooks/useIsEditable.js +12 -10
  36. package/dist/core/hooks/useOverlayMessage.js +15 -19
  37. package/dist/core/lib/artifact.d.ts +21 -0
  38. package/dist/core/lib/artifact.js +154 -0
  39. package/dist/core/lib/artifactColors.js +123 -0
  40. package/dist/core/lib/artifactData.d.ts +1 -0
  41. package/dist/core/lib/artifactData.js +131 -0
  42. package/dist/core/lib/artifactHost.js +205 -0
  43. package/dist/core/lib/artifactRuntime.js +36 -0
  44. package/dist/core/lib/artifactTheme.js +57 -0
  45. package/dist/core/lib/contentType.d.ts +304 -103
  46. package/dist/core/lib/contentType.js +207 -168
  47. package/dist/core/lib/fieldTypes.js +4 -0
  48. package/dist/core/lib/iconTypes.d.ts +13 -0
  49. package/dist/core/lib/icons.d.ts +6 -0
  50. package/dist/core/lib/icons.js +126 -0
  51. package/dist/core/lib/imagePlaceholder.js +177 -0
  52. package/dist/core/lib/imageTransform.js +1 -1
  53. package/dist/core/lib/lexicalReact.d.ts +3 -1
  54. package/dist/core/lib/lexicalState.js +7 -7
  55. package/dist/core/lib/modifierFormats.js +1 -1
  56. package/dist/core/lib/previewPaused.js +9 -0
  57. package/dist/core/lib/recordScope.js +316 -0
  58. package/dist/core/lib/reference.d.ts +81 -0
  59. package/dist/core/lib/reference.js +29 -0
  60. package/dist/core/lib/textLinks.js +7 -5
  61. package/dist/core/lib/textStyles.d.ts +6 -8
  62. package/dist/core/lib/textStyles.js +5 -5
  63. package/dist/core/notFound.d.ts +7 -0
  64. package/dist/core/notFound.js +11 -0
  65. package/dist/core/pageDestinations.js +6 -3
  66. package/dist/core/recordItem.js +34 -0
  67. package/dist/core/requestContext.d.ts +26 -0
  68. package/dist/core/requestContext.js +20 -0
  69. package/dist/features/canvas/CamoxCanvas.js +1734 -0
  70. package/dist/features/canvas/CanvasBlockFrame.js +353 -0
  71. package/dist/features/canvas/CanvasBlockHeader.js +55 -0
  72. package/dist/features/canvas/CanvasLoadingScreen.js +35 -0
  73. package/dist/features/canvas/CanvasOverlays.js +254 -0
  74. package/dist/features/canvas/CanvasPageEdge.js +87 -0
  75. package/dist/features/canvas/CanvasPageFrame.js +335 -0
  76. package/dist/features/canvas/CanvasPageHeader.js +125 -0
  77. package/dist/features/canvas/CanvasPreviewPageSlot.js +103 -0
  78. package/dist/features/canvas/CanvasShelfMarker.js +255 -0
  79. package/dist/features/canvas/DerivedPagePathInput.js +238 -0
  80. package/dist/features/canvas/canvasCamera.js +229 -0
  81. package/dist/features/canvas/canvasFlight.js +104 -0
  82. package/dist/features/canvas/canvasFrameCapture.js +364 -0
  83. package/dist/features/canvas/canvasFrameDocument.js +317 -0
  84. package/dist/features/canvas/canvasFrameMode.js +53 -0
  85. package/dist/features/canvas/canvasInsertionSeams.js +44 -0
  86. package/dist/features/canvas/canvasItems.js +120 -0
  87. package/dist/features/canvas/canvasOverlayGeometry.js +240 -0
  88. package/dist/features/canvas/canvasPages.js +79 -0
  89. package/dist/features/canvas/canvasScreenSpace.js +26 -0
  90. package/dist/features/canvas/canvasSelection.js +18 -0
  91. package/dist/features/canvas/canvasStore.js +149 -0
  92. package/dist/features/canvas/canvasThumbnails.js +304 -0
  93. package/dist/features/canvas/canvasZoom.js +10 -0
  94. package/dist/features/canvas/sidebar/CanvasInspector.js +202 -0
  95. package/dist/features/canvas/sidebar/CanvasNavigator.js +226 -0
  96. package/dist/features/canvas/sidebar/CanvasSidebar.js +60 -0
  97. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +48 -0
  98. package/dist/features/canvas/sidebar/CanvasSidebarSkeleton.js +39 -0
  99. package/dist/features/canvas/sidebar/CatalogBlockInspector.js +189 -0
  100. package/dist/features/canvas/sidebar/CuratedPageInspector.js +69 -0
  101. package/dist/features/canvas/sidebar/DerivedPageInspector.js +37 -0
  102. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +78 -0
  103. package/dist/features/canvas/sidebar/LayoutPageOverview.js +71 -0
  104. package/dist/features/canvas/sidebar/PageInspector.js +157 -0
  105. package/dist/features/canvas/sidebar/PageInspectorSkeleton.js +90 -0
  106. package/dist/features/canvas/sidebar/PageOverview.js +27 -0
  107. package/dist/features/canvas/sidebar/SingletonPageInspector.js +31 -0
  108. package/dist/features/canvas/useCanvasCamera.js +803 -0
  109. package/dist/features/canvas/useCanvasFrame.js +149 -0
  110. package/dist/features/canvas/useCanvasFrameInput.js +97 -0
  111. package/dist/features/canvas/useCanvasNearness.js +57 -0
  112. package/dist/features/canvas/useDiscoveredPreviewPaths.js +62 -0
  113. package/dist/features/content/CamoxContent.d.ts +2 -5
  114. package/dist/features/content/CamoxContent.js +174 -271
  115. package/dist/features/content/CollectionItemSheetContext.js +106 -0
  116. package/dist/features/content/ContentAssets.js +315 -0
  117. package/dist/features/content/ContentCollection.js +663 -0
  118. package/dist/features/content/collection-form.js +58 -0
  119. package/dist/features/content/components/AssetCard.js +101 -69
  120. package/dist/features/content/components/AssetCardSkeleton.js +4 -8
  121. package/dist/features/content/components/ContentSidebar.js +129 -18
  122. package/dist/features/content/components/ContentSidebarGroup.js +37 -0
  123. package/dist/features/content/components/DeleteCollectionItemButton.js +202 -0
  124. package/dist/features/content/components/MissingFromLocale.js +282 -0
  125. package/dist/features/content/components/UploadDropZone.js +27 -31
  126. package/dist/features/content/components/UploadProgressDrawer.js +90 -98
  127. package/dist/features/navigation/navigation.d.ts +66 -20
  128. package/dist/features/navigation/navigation.js +257 -90
  129. package/dist/features/og/defaultFonts.js +8 -8
  130. package/dist/features/og/geist-variable-ttf-base64.js +1 -1
  131. package/dist/features/og/imageResponse.d.ts +8 -0
  132. package/dist/features/og/imageResponse.js +26 -0
  133. package/dist/features/page/CuratedPageContent.js +167 -0
  134. package/dist/features/page/DerivedPageContent.js +26 -30
  135. package/dist/features/page/SharedLayoutContent.js +25 -27
  136. package/dist/features/preview/CamoxPreview.d.ts +23 -61
  137. package/dist/features/preview/CamoxPreview.js +811 -507
  138. package/dist/features/preview/EditablePageContent.js +1 -1
  139. package/dist/features/preview/EditablePageExperience.js +16 -22
  140. package/dist/features/preview/components/AddBlockDialog.js +270 -0
  141. package/dist/features/preview/components/AssetFieldEditor.js +173 -154
  142. package/dist/features/preview/components/AssetLightbox.js +194 -136
  143. package/dist/features/preview/components/AssetPickerModal.js +153 -170
  144. package/dist/features/preview/components/BlockActionsPopover.js +150 -149
  145. package/dist/features/preview/components/BlockErrorBoundary.js +8 -12
  146. package/dist/features/preview/components/BlockThumbnail.js +176 -100
  147. package/dist/features/preview/components/CanvasPreview.js +243 -0
  148. package/dist/features/preview/components/CreatePageModal.js +128 -137
  149. package/dist/features/preview/components/DebouncedFieldEditor.js +29 -33
  150. package/dist/features/preview/components/DerivedLayoutDialog.js +46 -29
  151. package/dist/features/preview/components/DrillRow.js +150 -0
  152. package/dist/features/preview/components/FieldOverlayStyles.js +33 -10
  153. package/dist/features/preview/components/FieldToolbar.js +166 -116
  154. package/dist/features/preview/components/Frame.js +108 -20
  155. package/dist/features/preview/components/IconFieldEditor.js +227 -0
  156. package/dist/features/preview/components/ItemFieldsEditor.js +238 -205
  157. package/dist/features/preview/components/LinkFieldEditor.js +135 -139
  158. package/dist/features/preview/components/Metadata.js +22 -30
  159. package/dist/features/preview/components/MobilePreviewDrawer.js +89 -108
  160. package/dist/features/preview/components/MultipleAssetFieldEditor.js +334 -246
  161. package/dist/features/preview/components/OverlayTracker.js +10 -42
  162. package/dist/features/preview/components/Overlays.js +81 -69
  163. package/dist/features/preview/components/PageInfo.js +1748 -0
  164. package/dist/features/preview/components/PageInfoSkeleton.js +51 -0
  165. package/dist/features/preview/components/PageLocationFieldset.js +112 -116
  166. package/dist/features/preview/components/PageNavigatorHeader.js +24 -0
  167. package/dist/features/preview/components/PageNicknameField.js +22 -26
  168. package/dist/features/preview/components/PagePicker.js +145 -147
  169. package/dist/features/preview/components/PageTree.js +525 -532
  170. package/dist/features/preview/components/PreviewActivation.js +28 -32
  171. package/dist/features/preview/components/PreviewPanel.js +620 -319
  172. package/dist/features/preview/components/PreviewToolbar.js +111 -340
  173. package/dist/features/preview/components/RecordCard.js +80 -0
  174. package/dist/features/preview/components/RecordCombobox.js +178 -0
  175. package/dist/features/preview/components/ReferenceFieldEditor.js +149 -0
  176. package/dist/features/preview/components/ReferenceListFieldEditor.js +432 -0
  177. package/dist/features/preview/components/RemovedBlockPlaceholder.js +95 -0
  178. package/dist/features/preview/components/RepeatableItemsList.js +125 -138
  179. package/dist/features/preview/components/SelectionEditor.js +993 -0
  180. package/dist/features/preview/components/ShikiMarkdown.js +14 -18
  181. package/dist/features/preview/components/SidebarSection.js +52 -64
  182. package/dist/features/preview/components/StudioEditingFallback.js +56 -0
  183. package/dist/features/preview/components/UnlinkAssetButton.js +104 -107
  184. package/dist/features/preview/components/blockGridNavigation.js +15 -0
  185. package/dist/features/preview/components/contentFieldSchema.js +8 -0
  186. package/dist/features/preview/components/useFieldWriter.js +200 -0
  187. package/dist/features/preview/components/useRecordView.js +383 -0
  188. package/dist/features/preview/components/useRepeatableItemActions.js +35 -37
  189. package/dist/features/preview/components/useUpdateBlockPosition.js +37 -37
  190. package/dist/features/preview/editingFrame.js +197 -0
  191. package/dist/features/preview/overlayHighlights.js +27 -0
  192. package/dist/features/preview/overlayMessages.js +72 -1
  193. package/dist/features/preview/pageRecord.js +189 -0
  194. package/dist/features/preview/previewConstants.js +6 -2
  195. package/dist/features/preview/previewPageFrame.js +95 -0
  196. package/dist/features/preview/previewPreparation.js +7 -0
  197. package/dist/features/preview/previewSelection.js +33 -59
  198. package/dist/features/preview/previewStore.js +225 -123
  199. package/dist/features/preview/referencePickerFocus.js +29 -0
  200. package/dist/features/preview/repeatableItemTargets.js +56 -0
  201. package/dist/features/preview/selectionReveal.js +21 -0
  202. package/dist/features/preview/usePreviewViewportActions.js +114 -0
  203. package/dist/features/preview/viewportContinuity.js +109 -0
  204. package/dist/features/provider/AuthenticatedCamoxProvider.js +69 -44
  205. package/dist/features/provider/CamoxProvider.d.ts +3 -6
  206. package/dist/features/provider/CamoxProvider.js +38 -50
  207. package/dist/features/provider/CoreCamoxProvider.js +71 -23
  208. package/dist/features/provider/LocalhostPreviewProvider.js +73 -73
  209. package/dist/features/provider/PreviewSignInPage.js +54 -0
  210. package/dist/features/provider/components/CamoxAppContext.js +11 -11
  211. package/dist/features/provider/components/CommandPalette.js +87 -143
  212. package/dist/features/provider/useAdminShortcuts.js +7 -11
  213. package/dist/features/routes/pageRuntime.d.ts +44 -4
  214. package/dist/features/routes/pageRuntime.js +244 -137
  215. package/dist/features/runtime/RuntimeChrome.js +117 -115
  216. package/dist/features/runtime/alternates.js +64 -0
  217. package/dist/features/runtime/analytics.d.ts +7 -0
  218. package/dist/features/runtime/analytics.js +66 -0
  219. package/dist/features/runtime/artifactSources.js +87 -0
  220. package/dist/features/runtime/editingFallback.js +20 -0
  221. package/dist/features/runtime/linkPrefetch.js +97 -0
  222. package/dist/features/runtime/livePages.js +62 -0
  223. package/dist/features/runtime/llmsTxt.js +40 -0
  224. package/dist/features/runtime/loopbackFetch.js +18 -0
  225. package/dist/features/runtime/navigationAccessibility.js +51 -0
  226. package/dist/features/runtime/navigationTarget.js +23 -2
  227. package/dist/features/runtime/pageApp.js +216 -141
  228. package/dist/features/runtime/pageBlockTypes.js +20 -0
  229. package/dist/features/runtime/pageClient.d.ts +2 -4
  230. package/dist/features/runtime/pageClient.js +18 -3
  231. package/dist/features/runtime/pageNavigation.js +254 -32
  232. package/dist/features/runtime/pageServer.d.ts +2 -4
  233. package/dist/features/runtime/pageServer.js +8 -1
  234. package/dist/features/runtime/pageViewport.js +7 -0
  235. package/dist/features/runtime/runtime.d.ts +60 -23
  236. package/dist/features/runtime/runtime.js +779 -166
  237. package/dist/features/runtime/runtimeChromeLoader.js +58 -0
  238. package/dist/features/runtime/runtimeRoutes.d.ts +19 -0
  239. package/dist/features/runtime/runtimeRoutes.js +25 -0
  240. package/dist/features/runtime/scrollRestoration.js +133 -0
  241. package/dist/features/runtime/studioApp.d.ts +4 -3
  242. package/dist/features/runtime/studioApp.js +5 -9
  243. package/dist/features/runtime/studioClient.d.ts +2 -4
  244. package/dist/features/runtime/studioClient.js +12 -3
  245. package/dist/features/runtime/studioServer.d.ts +2 -4
  246. package/dist/features/runtime/studioServer.js +6 -1
  247. package/dist/features/runtime/worker.d.ts +6 -0
  248. package/dist/features/runtime/worker.js +23 -0
  249. package/dist/features/runtime/workerRequest.js +44 -0
  250. package/dist/features/studio/CamoxStudio.d.ts +2 -6
  251. package/dist/features/studio/CamoxStudio.js +34 -49
  252. package/dist/features/studio/FollowAgentProvider.js +338 -0
  253. package/dist/features/studio/components/ChatSidebar.js +356 -0
  254. package/dist/features/studio/components/DraftsControl.js +676 -0
  255. package/dist/features/studio/components/FaviconDialog.js +318 -0
  256. package/dist/features/studio/components/LocalePicker.js +785 -0
  257. package/dist/features/studio/components/Navbar.js +123 -103
  258. package/dist/features/studio/components/ProjectMenu.js +239 -260
  259. package/dist/features/studio/components/RenameDraftDialog.js +206 -0
  260. package/dist/features/studio/components/ReviewDraftSheet.js +1472 -0
  261. package/dist/features/studio/components/ThreadComposer.js +157 -0
  262. package/dist/features/studio/components/ThreadMarkdown.js +113 -0
  263. package/dist/features/studio/components/ThreadMessages.js +270 -0
  264. package/dist/features/studio/components/UserButton.js +34 -23
  265. package/dist/features/studio/draftLabel.js +33 -0
  266. package/dist/features/studio/draftNotice.js +13 -0
  267. package/dist/features/studio/followAgent.js +250 -0
  268. package/dist/features/studio/followAgentContext.js +7 -0
  269. package/dist/features/studio/localeOptions.js +118 -0
  270. package/dist/features/studio/localeSwitch.js +19 -0
  271. package/dist/features/studio/routes.js +48 -1
  272. package/dist/features/studio/studioStore.js +64 -1
  273. package/dist/features/studio/useTheme.js +24 -27
  274. package/dist/features/vite/artifactRuntimeMinification.js +36 -0
  275. package/dist/features/vite/auth.js +1 -50
  276. package/dist/features/vite/blockBoilerplate.js +6 -5
  277. package/dist/features/vite/blockChunks.js +31 -0
  278. package/dist/features/vite/bundlerLogs.js +17 -0
  279. package/dist/features/vite/definitionsSync.js +158 -226
  280. package/dist/features/vite/devAuthentication.js +4 -16
  281. package/dist/features/vite/devSignIn.js +87 -0
  282. package/dist/features/vite/iconGeneration.js +16 -0
  283. package/dist/features/vite/layoutGeneration.js +11 -1
  284. package/dist/features/vite/publishSeededContent.js +30 -0
  285. package/dist/features/vite/routeGeneration.js +6 -7
  286. package/dist/features/vite/runtimeDev.js +80 -99
  287. package/dist/features/vite/vite.d.ts +53 -31
  288. package/dist/features/vite/vite.js +230 -116
  289. package/dist/features/vite/workerBuild.js +39 -0
  290. package/dist/hooks/use-debounced-field.js +116 -47
  291. package/dist/hooks/use-file-upload.js +114 -67
  292. package/dist/hooks/use-marquee-selection.js +32 -36
  293. package/dist/hooks/use-page-destinations.js +69 -37
  294. package/dist/lib/api-client-server.d.ts +5 -0
  295. package/dist/lib/api-client-server.js +10 -4
  296. package/dist/lib/api-client.d.ts +6 -0
  297. package/dist/lib/api-client.js +51 -16
  298. package/dist/lib/api-socket.js +17 -0
  299. package/dist/lib/auth-page-styles.js +39 -0
  300. package/dist/lib/auth.js +139 -60
  301. package/dist/lib/collection-cache.js +23 -0
  302. package/dist/lib/draft-scope.d.ts +4 -0
  303. package/dist/lib/draft-scope.js +222 -0
  304. package/dist/lib/html.js +12 -0
  305. package/dist/lib/link-pages.js +154 -0
  306. package/dist/lib/locale-path.d.ts +22 -0
  307. package/dist/lib/locale-path.js +57 -0
  308. package/dist/lib/normalized-data.js +252 -152
  309. package/dist/lib/preview-handoff.js +13 -0
  310. package/dist/lib/queries.d.ts +11 -0
  311. package/dist/lib/queries.js +71 -131
  312. package/dist/lib/studio-reads.d.ts +14 -0
  313. package/dist/lib/studio-reads.js +574 -0
  314. package/dist/lib/use-project-room.js +50 -47
  315. package/dist/lib/use-thread.js +310 -0
  316. package/dist/lib/utils.js +25 -29
  317. package/dist/lib/view-locale-context.js +22 -0
  318. package/dist/lib/view-locale.js +46 -0
  319. package/dist/package.js +5 -0
  320. package/dist/studio-overlays.css +41 -147
  321. package/dist/studio.css +2 -2
  322. package/package.json +70 -72
  323. package/skills/camox/SKILL.md +16 -5
  324. package/skills/camox/references/artifacts.md +179 -0
  325. package/skills/camox/references/block-definitions.md +98 -55
  326. package/skills/camox/references/cli-common.md +18 -6
  327. package/skills/camox/references/cli-content.md +92 -58
  328. package/skills/camox/references/cli-publishing.md +26 -0
  329. package/skills/camox/references/dom-integrations.md +27 -0
  330. package/skills/camox/references/field-styling.md +7 -7
  331. package/skills/camox/references/layout-definitions.md +16 -14
  332. package/skills/camox/references/localization.md +56 -0
  333. package/skills/camox/references/routed-layouts.md +133 -15
  334. package/skills/camox-expand/SKILL.md +69 -0
  335. package/skills/camox-expand/references/jobs.md +39 -0
  336. package/skills/camoxify/SKILL.md +38 -2
  337. package/dist/core/components/AddBlockControlBar.js +0 -78
  338. package/dist/core/hooks/useRequireDraftSource.js +0 -23
  339. package/dist/features/metadata/sitemap.d.ts +0 -8
  340. package/dist/features/metadata/sitemap.js +0 -17
  341. package/dist/features/og/imageResponse.node.d.ts +0 -12
  342. package/dist/features/og/imageResponse.node.js +0 -49
  343. package/dist/features/og/imageResponse.workerd.d.ts +0 -12
  344. package/dist/features/og/imageResponse.workerd.js +0 -40
  345. package/dist/features/page/PublishedPageContent.js +0 -205
  346. package/dist/features/page/PublishedPageExperience.js +0 -23
  347. package/dist/features/preview/DerivedPreview.js +0 -134
  348. package/dist/features/preview/commentsEnabled.js +0 -8
  349. package/dist/features/preview/components/AddBlockSidebar.js +0 -381
  350. package/dist/features/preview/components/AttachedComments.js +0 -194
  351. package/dist/features/preview/components/CommentHeader.js +0 -122
  352. package/dist/features/preview/components/CommentSidebar.js +0 -29
  353. package/dist/features/preview/components/CommentTargetQuote.js +0 -166
  354. package/dist/features/preview/components/DerivedLayoutSidebar.d.ts +0 -5
  355. package/dist/features/preview/components/DerivedLayoutSidebar.js +0 -179
  356. package/dist/features/preview/components/DerivedPageInfoSidebar.js +0 -69
  357. package/dist/features/preview/components/LeftSidebar.js +0 -68
  358. package/dist/features/preview/components/PageEditorSidebar.js +0 -563
  359. package/dist/features/preview/components/PageInfoSidebar.js +0 -1551
  360. package/dist/features/preview/components/PageNavigatorSidebar.js +0 -78
  361. package/dist/features/preview/components/PageStatusBadge.js +0 -97
  362. package/dist/features/preview/components/PeekedBlock.js +0 -217
  363. package/dist/features/preview/components/PreviewComments.js +0 -56
  364. package/dist/features/preview/components/PreviewToolbarSpacer.js +0 -43
  365. package/dist/features/preview/components/PublicationRow.js +0 -422
  366. package/dist/features/preview/components/PublishDialog.js +0 -194
  367. package/dist/features/preview/components/RightSidebar.js +0 -59
  368. package/dist/features/preview/previewCommentsStore.js +0 -115
  369. package/dist/features/preview/publication.js +0 -81
  370. package/dist/features/runtime/SharedChromeContext.js +0 -7
  371. package/dist/features/studio/components/EnvironmentMenu.js +0 -413
  372. package/dist/features/vite/appGeneration.js +0 -89
  373. package/dist/lib/view-queries.js +0 -28
  374. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/Matches.d.ts +0 -114
  375. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/RouterProvider.d.ts +0 -25
  376. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/config.d.ts +0 -1
  377. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/defer.d.ts +0 -1
  378. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/fileRoute.d.ts +0 -20
  379. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/global.d.ts +0 -8
  380. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/hash-scroll.d.ts +0 -1
  381. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/index.d.ts +0 -18
  382. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/link.d.ts +0 -161
  383. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/load-matches.d.ts +0 -6
  384. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/location.d.ts +0 -54
  385. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/lru-cache.d.ts +0 -8
  386. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/manifest.d.ts +0 -32
  387. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/new-process-route-tree.d.ts +0 -115
  388. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/not-found.d.ts +0 -1
  389. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/redirect.d.ts +0 -49
  390. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/rewrite.d.ts +0 -1
  391. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/root.d.ts +0 -6
  392. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/route.d.ts +0 -343
  393. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/routeInfo.d.ts +0 -48
  394. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/router.d.ts +0 -695
  395. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/scroll-restoration.d.ts +0 -1
  396. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/searchMiddleware.d.ts +0 -1
  397. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/searchParams.d.ts +0 -5
  398. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/ssr/serializer/RawStream.d.ts +0 -38
  399. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/ssr/serializer/seroval-plugins.d.ts +0 -1
  400. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/ssr/serializer/transformer.d.ts +0 -65
  401. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/stores.d.ts +0 -69
  402. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/structuralSharing.d.ts +0 -1
  403. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/typePrimitives.d.ts +0 -1
  404. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useLoaderData.d.ts +0 -1
  405. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useLoaderDeps.d.ts +0 -1
  406. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useNavigate.d.ts +0 -10
  407. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useParams.d.ts +0 -1
  408. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useRouteContext.d.ts +0 -1
  409. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useSearch.d.ts +0 -1
  410. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/utils.d.ts +0 -28
  411. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/validators.d.ts +0 -49
  412. package/skills/camox/references/cli-environments.md +0 -37
@@ -1,16 +1,28 @@
1
+ import { serializeJsonForHtml } from "../../lib/html.js";
2
+ import { createRequestContext } from "../../core/requestContext.js";
1
3
  import { buildClearServerAuthCookieHeader, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
2
- import { getPageDestinations } from "../../core/pageDestinations.js";
4
+ import { UNNAMED_DEFAULT_LOCALE, localizedPath, parseLocalePath } from "../../lib/locale-path.js";
5
+ import { draftNamedBy } from "../../lib/draft-scope.js";
6
+ import { RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
7
+ import { reads } from "../../lib/studio-reads.js";
8
+ import { asCollectionItem, recordMarkdownTemplate, recordPlainText } from "../../core/recordItem.js";
3
9
  import { PLATFORM_SCRIPT } from "../../lib/platform.js";
10
+ import { STUDIO_ASSETS_PATH, STUDIO_CONTENT_PATH, matchCanvasOnlyPath, matchCollectionContentPath } from "../studio/routes.js";
4
11
  import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
5
- import { matchDerivedLayout } from "../../core/derivedRoutes.js";
6
- import { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isAuthSessionError, isNotFoundError, loadCamoxPageForRequest, seedBlockCaches } from "../routes/pageRuntime.js";
12
+ import { discoverLayoutPages, matchDerivedLayout } from "../../core/derivedRoutes.js";
13
+ import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
14
+ import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
15
+ import { configuredProviders, renderAnalyticsScripts } from "./analytics.js";
16
+ import { omitArtifactSources } from "./artifactSources.js";
17
+ import { RouteDiscoveryError, listLivePages } from "./livePages.js";
18
+ import { formatLlmsTxt } from "./llmsTxt.js";
19
+ import { getPageBlockTypes } from "./pageBlockTypes.js";
20
+ import { DEVELOPMENT_DRAFT, LOCALE_HEADER } from "@camox/api-contract/drafts";
7
21
  import { QueryClient, dehydrate } from "@tanstack/react-query";
8
22
  import { createHead, renderSSRHead } from "@unhead/react/server";
9
23
 
10
24
  //#region src/features/runtime/runtime.ts
11
25
  const DEFAULT_RUNTIME_BASE_PATH = "";
12
- const RUNTIME_HEALTH_PATH = "/_camox/health";
13
- const RUNTIME_REGISTRY_PATH = "/_camox/registry";
14
26
  function normalizeRuntimeBasePath(basePath) {
15
27
  if (basePath === void 0) return DEFAULT_RUNTIME_BASE_PATH;
16
28
  if (!basePath || basePath === "/") return "";
@@ -24,6 +36,21 @@ function getRuntimePathname(url, runtimeBasePath = DEFAULT_RUNTIME_BASE_PATH) {
24
36
  if (!pathname.startsWith(`${basePath}/`)) return null;
25
37
  return pathname.slice(basePath.length) || "/";
26
38
  }
39
+ /**
40
+ * The draft the request's URL names, if any, or in dev mode the development draft. Sitemaps and
41
+ * Markdown ignore it: they read Live.
42
+ */
43
+ function getRequestDraftId(request, options) {
44
+ if (options.developmentDraft) return DEVELOPMENT_DRAFT;
45
+ return draftNamedBy(request.url) ?? void 0;
46
+ }
47
+ /**
48
+ * Whether a response may show what only this visitor sees: a session, or a draft named in the
49
+ * URL. Shared caches must never keep it.
50
+ */
51
+ function isPrivateRender(request, options) {
52
+ return !!getServerAuthCookieHeader(request.headers) || getRequestDraftId(request, options) !== void 0;
53
+ }
27
54
  function escapeXml(value) {
28
55
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
29
56
  }
@@ -31,34 +58,29 @@ function normalizePagePath(path) {
31
58
  if (!path) return "/";
32
59
  return path.startsWith("/") ? path : `/${path}`;
33
60
  }
34
- function getLayoutIdentity(layout, source) {
61
+ function getLayoutIdentity(layout) {
35
62
  if (!layout) return null;
36
- const version = source === "live" ? String(layout.livePublishedCheckpointId ?? "none") : String(layout.contentUpdatedAt ?? layout.updatedAt);
37
63
  return {
38
64
  blockIds: [...layout.beforeBlockIds, ...layout.afterBlockIds],
39
- id: layout.id,
40
65
  layoutId: layout.layoutId,
41
- version
66
+ version: String(layout.contentUpdatedAt ?? layout.updatedAt)
42
67
  };
43
68
  }
44
69
  function matchRuntimeRoute(pathname) {
45
- if (pathname === RUNTIME_HEALTH_PATH) return {
46
- kind: "health",
47
- pathname
48
- };
49
- if (pathname === RUNTIME_REGISTRY_PATH) return {
50
- kind: "registry",
51
- pathname
52
- };
53
- if (pathname === "/_camox/data") return {
54
- kind: "data",
70
+ const ownedRoute = matchRuntimeOwnedRoute(pathname);
71
+ if (ownedRoute) return {
72
+ kind: ownedRoute,
55
73
  pathname
56
74
  };
57
75
  if (pathname === "/camox") return {
58
76
  kind: "studio",
59
77
  pathname
60
78
  };
61
- if (pathname === "/camox/content") return {
79
+ if (matchCanvasOnlyPath(pathname)) return {
80
+ kind: "canvas",
81
+ pathname
82
+ };
83
+ if (pathname === "/camox/content" || pathname === `${"/camox/content"}/` || pathname === "/camox/content/assets" || matchCollectionContentPath(pathname)) return {
62
84
  kind: "studio-content",
63
85
  pathname
64
86
  };
@@ -66,14 +88,6 @@ function matchRuntimeRoute(pathname) {
66
88
  kind: "studio-nested",
67
89
  pathname
68
90
  };
69
- if (pathname === "/og") return {
70
- kind: "og",
71
- pathname
72
- };
73
- if (pathname === "/sitemap.xml") return {
74
- kind: "sitemap",
75
- pathname
76
- };
77
91
  return {
78
92
  kind: "page",
79
93
  pathname
@@ -85,24 +99,156 @@ function withRuntimeBasePath(pathname, runtimeBasePath) {
85
99
  if (pathname === "/") return `${basePath}/`;
86
100
  return `${basePath}${pathname}`;
87
101
  }
88
- async function createSitemapResponse(request, options) {
89
- if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox sitemap is not configured." }, { status: 500 });
90
- const api = createServerApiClient(options.apiUrl, options.environmentName);
91
- const origin = new URL(request.url).origin;
92
- const pages = await api.pages.listBySlug({ projectSlug: options.projectSlug });
93
- const destinations = getPageDestinations(pages, (await options.getCamoxApp?.())?.getLayouts() ?? []);
94
- const updatedAtById = new Map(pages.map((page) => [page.id, page.updatedAt]));
95
- const entries = destinations.map((page) => {
96
- const updatedAt = page.kind === "curated" ? updatedAtById.get(page.pageId) : void 0;
97
- const lastmod = updatedAt == null ? "" : `\n <lastmod>${escapeXml(new Date(updatedAt).toISOString())}</lastmod>`;
98
- return ` <url>
99
- <loc>${escapeXml(`${origin}${withRuntimeBasePath(page.fullPath, options.runtimeBasePath)}`)}</loc>${lastmod}
102
+ /**
103
+ * Answers with an index of the website's Live pages, or a 500 saying why they can't be listed.
104
+ * Sitemaps and llms.txt read Live whoever asks: crawlers only ever see published content. With
105
+ * `everyLocale`, it reads the pages of each of Live's locales. Any read failing, whether Live's
106
+ * locales, a locale's pages or route discovery, fails the whole index: an index silently missing
107
+ * pages is worse than one that visibly fails.
108
+ */
109
+ async function respondWithLiveWebsite(options, name, respond, { everyLocale = false } = {}) {
110
+ const { apiUrl, projectUuid } = options;
111
+ if (!apiUrl || !projectUuid) return Response.json({ message: `Camox ${name} is not configured.` }, { status: 500 });
112
+ const app = await options.getCamoxApp?.();
113
+ const readLocale = async (locale) => {
114
+ const { projectName, pages } = await createServerApiClient(apiUrl, { locale }).pages.listBySlug({ projectUuid });
115
+ const records = createRecordLookup(options, "live", { locale });
116
+ return {
117
+ locale,
118
+ projectName,
119
+ pages: await listLivePages({
120
+ pages,
121
+ app,
122
+ records
123
+ })
124
+ };
125
+ };
126
+ let locales;
127
+ let localizations;
128
+ try {
129
+ locales = everyLocale ? [...await readScopeLocales({
130
+ apiUrl,
131
+ projectUuid
132
+ })].sort((a, b) => Number(b.isDefault) - Number(a.isDefault)) : [UNNAMED_DEFAULT_LOCALE];
133
+ localizations = await Promise.all(locales.map(readLocale));
134
+ } catch (error) {
135
+ const discovery = error instanceof RouteDiscoveryError;
136
+ const cause = discovery ? error.cause : error;
137
+ console.error(`[camox] ${discovery ? "Route discovery" : "Reading Live"} failed while building the ${name}.`, cause);
138
+ const reason = error instanceof Error ? error.message : String(error);
139
+ return Response.json({ message: `Camox ${name} failed: ${reason}` }, { status: 500 });
140
+ }
141
+ const [defaultLocale, ...otherLocales] = localizations;
142
+ return respond({
143
+ app,
144
+ projectName: defaultLocale.projectName,
145
+ localizations: [defaultLocale, ...otherLocales],
146
+ locales
147
+ });
148
+ }
149
+ /** A path's absolute URL, with the runtime base path. */
150
+ function absoluteUrl(request, options) {
151
+ const { origin } = new URL(request.url);
152
+ return (path) => `${origin}${withRuntimeBasePath(path, options.runtimeBasePath)}`;
153
+ }
154
+ /** `body` with an ETag derived from it, or a 304 when the request's `If-None-Match` names it. */
155
+ async function withEtag(request, body, headers) {
156
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
157
+ const etag = `"${[...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
158
+ const taggedHeaders = {
159
+ ...headers,
160
+ ETag: etag
161
+ };
162
+ if ((request.headers.get("If-None-Match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === etag || tag === "*")) return new Response(null, {
163
+ status: 304,
164
+ headers: taggedHeaders
165
+ });
166
+ return new Response(body, { headers: taggedHeaders });
167
+ }
168
+ function createLlmsTxtResponse(request, options) {
169
+ return respondWithLiveWebsite(options, "llms.txt", (website) => withEtag(request, formatLlmsTxt({
170
+ projectName: website.projectName,
171
+ pages: website.localizations[0].pages,
172
+ layouts: (website.app?.getLayouts() ?? []).map(({ _internal }) => _internal),
173
+ url: absoluteUrl(request, options)
174
+ }), {
175
+ "Content-Type": "text/plain; charset=utf-8",
176
+ "Cache-Control": "public, max-age=300, stale-while-revalidate=3600"
177
+ }));
178
+ }
179
+ /** Tells LLM tools fetching a page's HTML where the website's llms.txt is. */
180
+ function llmsTxtLinkHeader(options) {
181
+ return `<${withRuntimeBasePath(RUNTIME_ROUTE_PATHS["llms-txt"], options.runtimeBasePath)}>; rel="llms-txt"`;
182
+ }
183
+ /**
184
+ * One sitemap for every locale: each localization of a page once, with the page's alternates as
185
+ * `xhtml:link`s, as its `hreflang` links list them.
186
+ */
187
+ function createSitemapResponse(request, options) {
188
+ return respondWithLiveWebsite(options, "sitemap", (website) => {
189
+ const url = absoluteUrl(request, options);
190
+ const alternates = alternatesByPage(website.localizations);
191
+ const entries = website.localizations.flatMap(({ locale, pages }) => pages.map(({ key, fullPath, updatedAt }) => {
192
+ const lastmod = updatedAt == null ? "" : `\n <lastmod>${escapeXml(new Date(updatedAt).toISOString())}</lastmod>`;
193
+ const links = hreflangLinks(alternates.get(key) ?? [], website.locales, url).map(({ hreflang, href }) => `\n <xhtml:link rel="alternate" hreflang="${escapeXml(hreflang)}" href="${escapeXml(href)}"/>`).join("");
194
+ return ` <url>
195
+ <loc>${escapeXml(url(localizedPath(fullPath, locale)))}</loc>${lastmod}${links}
100
196
  </url>`;
101
- }).join("\n");
102
- return new Response(`<?xml version="1.0" encoding="UTF-8"?>
103
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
197
+ })).join("\n");
198
+ return new Response(`<?xml version="1.0" encoding="UTF-8"?>
199
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
104
200
  ${entries}
105
201
  </urlset>`, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
202
+ }, { everyLocale: true });
203
+ }
204
+ /** The request's member project, if its session is valid. Without an API, nobody is a member. */
205
+ async function resolveRequestMember(request, options) {
206
+ if (!options.apiUrl || !options.projectUuid) return { clearAuthCookie: false };
207
+ return resolveMemberProject({
208
+ apiUrl: options.apiUrl,
209
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
210
+ projectUuid: options.projectUuid
211
+ });
212
+ }
213
+ /**
214
+ * The first discovered page of a derived layout, which the Canvas previews by default. With
215
+ * `recordId`, the page rendering that record, which the Studio follows when its slug changes.
216
+ * Records are found by their values in the locale `LOCALE_HEADER` names, the default locale
217
+ * without it, and the path is that locale's, without its prefix: route patterns aren't localized.
218
+ */
219
+ async function createRoutesResponse(request, options) {
220
+ const camoxApp = await options.getCamoxApp?.();
221
+ if (!camoxApp) return Response.json({ message: "Camox app registry is not available." }, { status: 500 });
222
+ const { searchParams } = new URL(request.url);
223
+ const layoutId = searchParams.get("layoutId") ?? "";
224
+ const recordId = searchParams.get("recordId");
225
+ const layout = camoxApp.getLayoutById(layoutId);
226
+ if (layout?._internal.kind !== "derived") return new Response("Not found", { status: 404 });
227
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
228
+ const headers = {
229
+ "Cache-Control": "private, no-store",
230
+ Vary: `Cookie, ${LOCALE_HEADER}`,
231
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
232
+ };
233
+ const tag = request.headers.get(LOCALE_HEADER);
234
+ try {
235
+ const pages = await discoverLayoutPages(layout, createLayoutRequestContext(options, project ? "draft" : "live", {
236
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
237
+ locale: tag ? {
238
+ tag,
239
+ isDefault: false
240
+ } : void 0
241
+ }));
242
+ const page = recordId ? pages.find((entry) => entry.recordId === recordId) : pages[0];
243
+ return Response.json({ path: page?.path ?? null }, { headers });
244
+ } catch (error) {
245
+ console.error(`[camox] Route discovery failed for layout "${layoutId}".`, error);
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ return Response.json({ message }, {
248
+ status: 500,
249
+ headers
250
+ });
251
+ }
106
252
  }
107
253
  async function createOgResponse(request, options) {
108
254
  const camoxApp = await options.getCamoxApp?.();
@@ -156,9 +302,19 @@ function createPageHeadInput(head) {
156
302
  }
157
303
  const pageLinks = [];
158
304
  for (const entry of links) {
159
- if (entry.rel !== "icon" || !entry.href) continue;
160
- pageLinks.push({
161
- rel: "icon",
305
+ if (!entry.href) continue;
306
+ if (entry.rel === "icon") {
307
+ pageLinks.push({
308
+ rel: "icon",
309
+ href: entry.href,
310
+ "data-camox-page-head": "true"
311
+ });
312
+ continue;
313
+ }
314
+ if (entry.rel === "alternate" && (entry.hreflang || entry.type)) pageLinks.push({
315
+ rel: "alternate",
316
+ ...entry.hreflang && { hreflang: entry.hreflang },
317
+ ...entry.type && { type: entry.type },
162
318
  href: entry.href,
163
319
  "data-camox-page-head": "true"
164
320
  });
@@ -169,10 +325,11 @@ function createPageHeadInput(head) {
169
325
  link: pageLinks
170
326
  };
171
327
  }
172
- function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl) {
328
+ function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl, locale) {
173
329
  const head = createHead();
174
330
  head.push(createDefaultHeadInput(stylesheetUrl));
175
331
  head.push(document);
332
+ if (locale) head.push({ htmlAttrs: { lang: locale.tag } });
176
333
  if (studioStylesheetUrl) head.push({ link: [{
177
334
  rel: "stylesheet",
178
335
  href: studioStylesheetUrl,
@@ -181,15 +338,28 @@ function renderHead(document, pageHead, stylesheetUrl, studioStylesheetUrl) {
181
338
  head.push(pageHead);
182
339
  return renderSSRHead(head);
183
340
  }
184
- function serializeJsonForHtml(value) {
185
- return JSON.stringify(value).replaceAll("<", "\\u003c");
341
+ /**
342
+ * The browser loads a page's blocks before hydrating it. Preloading them alongside the entry
343
+ * script avoids a request waterfall.
344
+ */
345
+ function renderBlockPreloads(options, dehydratedState) {
346
+ const preloads = options.blockPreloads;
347
+ if (!preloads) return "";
348
+ const urls = /* @__PURE__ */ new Set();
349
+ for (const type of getPageBlockTypes(dehydratedState)) {
350
+ const modulePath = preloads.modulePaths[type];
351
+ if (!modulePath) continue;
352
+ if (!preloads.chunks) urls.add(modulePath);
353
+ for (const chunk of preloads.chunks?.[modulePath] ?? []) urls.add(`/${chunk}`);
354
+ }
355
+ return [...urls].map((url) => `<link rel="modulepreload" href="${escapeXml(url)}">`).join("");
186
356
  }
187
357
  function renderClientEntryScript(src) {
188
358
  if (!src) return "";
189
359
  return `<script type="module" src="${escapeXml(src)}"><\/script>`;
190
360
  }
191
361
  async function preparePreviewDocument(input, options, document, content) {
192
- const siteHead = renderHead(document, createPageHeadInput(input.head ?? {}), options.stylesheetUrl);
362
+ const siteHead = renderHead(document, createPageHeadInput(input.head ?? {}), options.stylesheetUrl, void 0, input.locale);
193
363
  const href = new URL(withRuntimeBasePath(input.pathname, options.runtimeBasePath), input.href).href;
194
364
  const siteHtml = content ?? (options.renderPage ? await options.renderPage({
195
365
  ...input,
@@ -199,63 +369,193 @@ async function preparePreviewDocument(input, options, document, content) {
199
369
  }) : "");
200
370
  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>`;
201
371
  }
372
+ /**
373
+ * The render input a page's HTML embeds for hydration. Visitors load public pages, whose HTML
374
+ * already holds each rendered artifact source in its frame, so their copy leaves those out.
375
+ */
376
+ function hydrationInputFor(input, html) {
377
+ if (input.presentation === "studio") return input;
378
+ return {
379
+ ...input,
380
+ dehydratedState: omitArtifactSources(input.dehydratedState, html)
381
+ };
382
+ }
202
383
  function renderStudioThemeScript(enabled) {
203
384
  return enabled ? `<script data-camox-studio>${STUDIO_THEME_SCRIPT}${PLATFORM_SCRIPT}<\/script>` : "";
204
385
  }
205
- async function createPageHtmlResponse({ options, pathname, request }) {
206
- if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !options.renderPage) return createPageDataResponse({
386
+ /**
387
+ * The analytics providers a render counts its reader with: only a visitor, meaning someone the
388
+ * API didn't confirm as a member, on a Live page. A development draft never counts anyone.
389
+ */
390
+ function visitorAnalytics(options, render, project) {
391
+ if (render.studio || render.draftId || options.developmentDraft) return void 0;
392
+ return configuredProviders(project.analytics);
393
+ }
394
+ /** Whether a request is a preview link's token handoff, which signs in before reading anything. */
395
+ function isPreviewHandoff(request, options) {
396
+ const { searchParams } = new URL(request.url);
397
+ return searchParams.has("camox-preview") && searchParams.has("ott") && !!options.apiUrl && !!options.authenticationUrl && !!options.projectUuid && !!options.renderPage;
398
+ }
399
+ /**
400
+ * The alternates of the page a request renders, where it exists in each of the scope's locales,
401
+ * and its head with their `hreflang` links. Without a head, there is none to add them to.
402
+ */
403
+ async function readAlternates(request, options, { located, identity, head, ...reading }) {
404
+ const alternates = await findRequestAlternates(options, located, identity, reading);
405
+ const links = hreflangLinks(alternates, located.locales, absoluteUrl(request, options));
406
+ return {
407
+ alternates,
408
+ head: head && withHreflangs(head, links)
409
+ };
410
+ }
411
+ /** Where the page a request renders exists in each of the scope's locales: itself alone in one. */
412
+ async function findRequestAlternates(options, located, identity, { authCookieHeader, draftId }) {
413
+ const { apiUrl, projectUuid } = options;
414
+ const here = [{
415
+ locale: located.locale.tag,
416
+ path: localizedPath(located.path, located.locale)
417
+ }];
418
+ if (!apiUrl || !projectUuid || located.locales.length < 2) return here;
419
+ const app = await options.getCamoxApp?.();
420
+ const reading = {
421
+ authCookieHeader: draftId ? authCookieHeader : void 0,
422
+ draftId
423
+ };
424
+ return findAlternates({
425
+ identity,
426
+ located,
427
+ layouts: app?.getLayouts() ?? [],
428
+ reads: {
429
+ pages: async (locale) => (await createServerApiClient(apiUrl, {
430
+ ...reading,
431
+ locale
432
+ }).pages.listBySlug({ projectUuid })).pages,
433
+ context: (locale) => createLayoutRequestContext(options, draftId ? "draft" : "live", {
434
+ ...reading,
435
+ locale
436
+ })
437
+ }
438
+ });
439
+ }
440
+ /** A page's head with its alternates' `hreflang` links. */
441
+ function withHreflangs(head, hreflangs) {
442
+ if (!hreflangs.length) return head;
443
+ const links = hreflangs.map((link) => ({
444
+ rel: "alternate",
445
+ ...link
446
+ }));
447
+ return {
448
+ ...head,
449
+ links: [...head.links ?? [], ...links]
450
+ };
451
+ }
452
+ /**
453
+ * The sign-in page a preview link opens on. No page, project or locale lookup yet: the browser
454
+ * authenticates first, then loads the page again.
455
+ */
456
+ async function createPreviewHandoffResponse({ options, pathname, request }) {
457
+ const locale = UNNAMED_DEFAULT_LOCALE;
458
+ const input = {
459
+ previewHandoff: true,
460
+ presentation: "public",
461
+ apiUrl: options.apiUrl,
462
+ authenticationUrl: options.authenticationUrl,
463
+ projectUuid: options.projectUuid,
464
+ href: request.url,
465
+ pathname,
466
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
467
+ locale,
468
+ locales: [locale],
469
+ head: {},
470
+ loaderData: null,
471
+ layoutIdentity: null,
472
+ dehydratedState: dehydrate(new QueryClient())
473
+ };
474
+ const html = await options.renderPage(input);
475
+ return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="referrer" content="no-referrer"><title>Camox draft preview</title><script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(input)}<\/script></head><body><div id="root">${html}</div>${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
476
+ "Content-Type": "text/html; charset=utf-8",
477
+ "Cache-Control": "private, no-store",
478
+ "Referrer-Policy": "no-referrer"
479
+ } });
480
+ }
481
+ async function createPageHtmlResponse({ options, pathname, located, request }) {
482
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !options.renderPage) return createPageDataResponse({
207
483
  options,
208
484
  pathname,
485
+ located,
209
486
  request
210
487
  });
211
- const singleton = await createDerivedResponse(options, pathname, request, false, true);
488
+ const singleton = await createDerivedResponse(options, pathname, located, request, { singletonOnly: true });
212
489
  if (singleton) return singleton;
213
490
  const queryClient = new QueryClient();
214
491
  try {
215
492
  const result = await loadCamoxPageForRequest({
216
493
  apiUrl: options.apiUrl,
217
494
  authCookieHeader: getServerAuthCookieHeader(request.headers),
218
- environmentName: options.environmentName,
495
+ draftId: getRequestDraftId(request, options),
496
+ locale: located.locale,
219
497
  origin: new URL(request.url).origin,
220
- pathname,
221
- projectSlug: options.projectSlug,
498
+ pathname: located.path,
499
+ projectUuid: options.projectUuid,
222
500
  queryClient
223
501
  });
224
502
  const headers = new Headers({
225
503
  "Content-Type": "text/html; charset=utf-8",
226
- Vary: "Cookie"
504
+ Vary: "Cookie",
505
+ Link: llmsTxtLinkHeader(options)
227
506
  });
228
- if (getServerAuthCookieHeader(request.headers)) headers.set("Cache-Control", "private, no-store");
507
+ if (isPrivateRender(request, options)) headers.set("Cache-Control", "private, no-store");
229
508
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
230
509
  const camoxApp = await options.getCamoxApp?.();
231
510
  const document = await options.getDocument?.() ?? {};
232
- const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : {};
511
+ const { alternates, head } = await readAlternates(request, options, {
512
+ located,
513
+ identity: {
514
+ kind: "curated",
515
+ pageId: result.data.page.page.id
516
+ },
517
+ head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : {},
518
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
519
+ draftId: result.draftId
520
+ });
233
521
  const dehydratedState = dehydrate(queryClient);
522
+ const { project } = result;
523
+ const studio = project !== void 0;
524
+ const analytics = visitorAnalytics(options, {
525
+ studio,
526
+ draftId: result.draftId
527
+ }, result.data.page.project);
234
528
  const pageRenderInput = {
235
- project: result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
236
- presentation: result.source === "draft" ? "studio" : "public",
529
+ alternates,
530
+ project,
531
+ presentation: studio ? "studio" : "public",
532
+ ...analytics && { analytics },
237
533
  apiUrl: options.apiUrl,
238
534
  authenticationUrl: options.authenticationUrl,
239
535
  dehydratedState,
240
- environmentName: options.environmentName,
536
+ draftId: result.draftId,
537
+ refusedDraft: result.refusedDraft,
241
538
  head,
242
- href: new URL(request.url).href,
243
- layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
539
+ href: renderedHref(request.url, result),
540
+ layoutIdentity: getLayoutIdentity(result.data.page.layout),
244
541
  loaderData: result.data,
245
542
  pathname,
246
- projectSlug: options.projectSlug,
543
+ projectUuid: options.projectUuid,
247
544
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
248
- source: result.source
545
+ locale: located.locale,
546
+ locales: located.locales
249
547
  };
250
- await preparePreviewDocument(pageRenderInput, options, document, result.source === "live" ? "" : void 0);
548
+ await preparePreviewDocument(pageRenderInput, options, document, studio ? void 0 : "");
251
549
  const appHtml = await options.renderPage(pageRenderInput);
252
- const renderedHead = renderHead(result.source === "draft" ? {} : document, createPageHeadInput(head), result.source === "draft" ? void 0 : options.stylesheetUrl, result.source === "draft" ? options.studioStylesheetUrl : void 0);
550
+ const renderedHead = renderHead(studio ? {} : document, createPageHeadInput(head), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
253
551
  return new Response(`<!doctype html>
254
552
  <html${renderedHead.htmlAttrs}>
255
553
  <head>
256
- ${renderStudioThemeScript(result.source === "draft")}
554
+ ${renderStudioThemeScript(studio)}
257
555
  ${renderedHead.headTags}
258
- <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(pageRenderInput)}<\/script>
556
+ ${renderAnalyticsScripts(analytics)}
557
+ ${renderBlockPreloads(options, dehydratedState)}
558
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(pageRenderInput, appHtml))}<\/script>
259
559
  </head>
260
560
  <body${renderedHead.bodyAttrs}>
261
561
  ${renderedHead.bodyTagsOpen}
@@ -266,71 +566,252 @@ ${renderClientEntryScript(options.pageClientEntryUrl)}
266
566
  </html>`, { headers });
267
567
  } catch (error) {
268
568
  if (isNotFoundError(error)) {
269
- const derived = await createDerivedResponse(options, pathname, request);
569
+ const derived = await createDerivedResponse(options, pathname, located, request);
270
570
  if (derived) return derived;
271
571
  return new Response("No Camox page on this path", { status: 404 });
272
572
  }
273
573
  throw error;
274
574
  }
275
575
  }
276
- async function createDerivedResponse(options, pathname, request, dataOnly = false, singletonOnly = false) {
576
+ /**
577
+ * The context `loader` and `routes` get. Its record lookup reads each record's current content;
578
+ * only draft reads carry the editor's session, and `draftId` names the draft they read.
579
+ */
580
+ function createLayoutRequestContext(options, source, reading) {
581
+ return createRequestContext({ records: createRecordLookup(options, source, reading) });
582
+ }
583
+ /**
584
+ * Reads the records `loader` and `routes` ask for, in `locale` or the default locale; none
585
+ * without an API.
586
+ */
587
+ function createRecordLookup(options, source, { authCookieHeader, draftId, locale } = {}) {
588
+ const { apiUrl, projectUuid } = options;
589
+ if (!apiUrl || !projectUuid) return void 0;
590
+ const api = createServerApiClient(apiUrl, {
591
+ authCookieHeader: source === "draft" ? authCookieHeader : void 0,
592
+ draftId,
593
+ locale
594
+ });
595
+ return {
596
+ findByField: (input) => api.collectionDefinitions.findRecordsByField({
597
+ ...input,
598
+ projectUuid
599
+ }),
600
+ listPageValues: (input) => api.collectionDefinitions.listRecordPageValues({
601
+ ...input,
602
+ projectUuid
603
+ })
604
+ };
605
+ }
606
+ const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
607
+ /** An envelope names its record; it doesn't authorize anything, the API does. */
608
+ function isValidCollectionItem(app, result) {
609
+ const data = result.data;
610
+ return typeof result.collectionId === "string" && !!app?.getCollectionById(result.collectionId) && typeof data?.id === "string" && RECORD_ID.test(data.id) && typeof data.content === "object" && data.content !== null;
611
+ }
612
+ /**
613
+ * Runs a code-owned page's loader in the page's locale. The envelope survives SSR, navigation and
614
+ * hydration, and its data renders as returned: Camox never translates it.
615
+ */
616
+ async function loadLayoutResult(app, match, context, locale) {
617
+ const { loader, id } = match.layout._internal;
618
+ if (!loader) return void 0;
619
+ const result = await loader({
620
+ params: match.params,
621
+ context,
622
+ locale: locale.tag
623
+ });
624
+ const serializedResult = result === void 0 ? void 0 : JSON.parse(JSON.stringify(result));
625
+ const record = asCollectionItem(serializedResult);
626
+ if (!(record ? isValidCollectionItem(app, record) : serializedResult?.kind === "data" && Object.hasOwn(serializedResult, "data"))) throw new Error(`Layout "${id}" loader must return { kind: "data", data } with JSON-serializable data, or a collection-item envelope naming a registered collection and a record id`);
627
+ return serializedResult;
628
+ }
629
+ /**
630
+ * The Markdown of a singleton or derived page: front matter, its before blocks, the layout's
631
+ * toMarkdown output, then its after blocks. Without toMarkdown, the page is HTML only.
632
+ */
633
+ async function createLayoutMarkdownResponse(options, match, locale, page) {
634
+ const { toMarkdown, id } = match.layout._internal;
635
+ const { apiUrl, projectUuid } = options;
636
+ if (!toMarkdown || !apiUrl || !projectUuid) return null;
637
+ const api = createServerApiClient(apiUrl, { locale });
277
638
  const app = await options.getCamoxApp?.();
278
- const match = app && matchDerivedLayout(app.getLayouts(), pathname);
639
+ /** The layout's own Markdown, between its before and after blocks, and what built it. */
640
+ const layoutMarkdown = async () => {
641
+ const result = await loadLayoutResult(app, match, createLayoutRequestContext(options, "live", { locale }), locale);
642
+ const record = asCollectionItem(result);
643
+ const collection = record && app?.getCollectionById(record.collectionId);
644
+ if (!record || !collection) return {
645
+ body: toMarkdown({
646
+ data: result?.data,
647
+ params: match.params
648
+ }),
649
+ data: result?.data
650
+ };
651
+ const { data } = record;
652
+ const template = recordMarkdownTemplate(collection, (item) => toMarkdown({
653
+ data,
654
+ params: match.params,
655
+ item
656
+ }));
657
+ return {
658
+ body: [await api.collectionDefinitions.recordMarkdown({
659
+ projectUuid,
660
+ collectionId: record.collectionId,
661
+ id: data.id,
662
+ toMarkdown: template
663
+ })],
664
+ data,
665
+ item: recordPlainText(collection, data.content)
666
+ };
667
+ };
668
+ let rendered;
669
+ try {
670
+ rendered = await layoutMarkdown();
671
+ } catch (error) {
672
+ if (isNotFoundError(error)) return new Response("Page not found", { status: 404 });
673
+ throw error;
674
+ }
675
+ const slots = await api.blocks.getLayoutMarkdown({
676
+ projectUuid,
677
+ layoutId: id
678
+ }).catch((error) => {
679
+ if (isNotFoundError(error)) return {
680
+ before: "",
681
+ after: "",
682
+ projectName: ""
683
+ };
684
+ throw error;
685
+ });
686
+ const meta = buildPageMeta({
687
+ layout: match.layout._internal,
688
+ pageMetaTitle: match.layout._internal.title,
689
+ pageMetaDescription: void 0,
690
+ data: rendered.data,
691
+ item: rendered.item,
692
+ params: match.params,
693
+ projectName: slots.projectName,
694
+ pageFullPath: page.pathname
695
+ });
696
+ const markdown = [
697
+ slots.before,
698
+ ...rendered.body,
699
+ slots.after
700
+ ].filter(Boolean).join("\n\n");
701
+ return new Response(withFrontMatter(markdown, {
702
+ ...meta,
703
+ url: page.url
704
+ }), { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
705
+ }
706
+ async function createDerivedResponse(options, pathname, located, request, { dataOnly = false, singletonOnly = false } = {}) {
707
+ const app = await options.getCamoxApp?.();
708
+ const match = app && matchDerivedLayout(app.getLayouts(), located.path);
279
709
  if (!match || !dataOnly && !options.renderPage) return null;
280
710
  if (singletonOnly && match.layout._internal.kind !== "singleton") return null;
281
711
  try {
282
- const data = await match.layout._internal.loader?.({ params: match.params });
283
- const serializedData = data === void 0 ? void 0 : JSON.parse(JSON.stringify(data));
284
712
  const queryClient = new QueryClient();
285
- const authCookieHeader = getServerAuthCookieHeader(request.headers);
286
- let source = authCookieHeader ? "draft" : "live";
287
- let clearAuthCookie = false;
288
- const loadLayout = (source) => createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: source === "draft" ? authCookieHeader : void 0 }).layouts.get({
289
- projectSlug: options.projectSlug,
290
- layoutId: match.layout._internal.id,
291
- source
713
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
714
+ const studio = project !== void 0;
715
+ const authCookieHeader = studio ? getServerAuthCookieHeader(request.headers) : void 0;
716
+ const requestedDraft = getRequestDraftId(request, options);
717
+ const readLayout = () => readDraftOrLive(requestedDraft, studio, (draftId) => createServerApiClient(options.apiUrl, {
718
+ authCookieHeader,
719
+ draftId,
720
+ locale: located.locale
721
+ }).layouts.get({
722
+ projectUuid: options.projectUuid,
723
+ layoutId: match.layout._internal.id
724
+ }));
725
+ const runLoader = (draftId) => loadLayoutResult(app, match, createLayoutRequestContext(options, studio ? "draft" : "live", {
726
+ authCookieHeader,
727
+ draftId,
728
+ locale: located.locale
729
+ }), located.locale);
730
+ const [serializedResult, { value: shared, ...read }] = studio && requestedDraft ? await readLayout().then(async (read) => [await runLoader(read.draftId), read]) : await Promise.all([runLoader(), readLayout()]);
731
+ const studioCache = createServerStudioCache(queryClient, {
732
+ apiUrl: options.apiUrl,
733
+ authCookieHeader,
734
+ locale: located.locale
292
735
  });
293
- const shared = await loadLayout(source).catch(async (error) => {
294
- if (source !== "draft" || !isAuthSessionError(error)) throw error;
295
- source = "live";
296
- clearAuthCookie = true;
297
- return loadLayout("live");
736
+ const projectUuid = options.projectUuid;
737
+ const shownIn = { in: read.draftId ?? null };
738
+ studioCache.land(reads.layoutStructure({
739
+ projectUuid,
740
+ layoutId: match.layout._internal.id
741
+ }), shared, shownIn);
742
+ studioCache.set(reads.derivedPageData({
743
+ projectUuid,
744
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
745
+ pathname
746
+ }), serializedResult ?? null, shownIn);
747
+ const record = asCollectionItem(serializedResult);
748
+ const recordCollection = record && app.getCollectionById(record.collectionId);
749
+ const { alternates, head: pageHead } = await readAlternates(request, options, {
750
+ located,
751
+ identity: record && match.layout._internal.kind === "derived" ? {
752
+ kind: "record",
753
+ layout: match.layout,
754
+ recordId: record.data.id
755
+ } : { kind: "code-owned" },
756
+ head: buildPageHead({
757
+ layout: match.layout._internal,
758
+ pageMetaTitle: match.layout._internal.title,
759
+ pageMetaDescription: void 0,
760
+ data: serializedResult?.data,
761
+ item: record && recordCollection ? recordPlainText(recordCollection, record.data.content) : void 0,
762
+ params: match.params,
763
+ projectName: shared.projectName,
764
+ pageFullPath: pathname,
765
+ origin: new URL(request.url).origin,
766
+ faviconUrl: buildFaviconUrl(options.apiUrl, shared.project),
767
+ hasMarkdown: match.layout._internal.toMarkdown !== void 0
768
+ }),
769
+ authCookieHeader,
770
+ draftId: read.draftId
298
771
  });
299
- seedBlockCaches(queryClient, shared, source);
772
+ const analytics = dataOnly ? void 0 : visitorAnalytics(options, {
773
+ studio,
774
+ draftId: read.draftId
775
+ }, shared.project);
300
776
  const input = {
301
- project: source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
302
- presentation: source === "draft" ? "studio" : "public",
777
+ alternates,
778
+ project,
779
+ presentation: studio ? "studio" : "public",
780
+ ...analytics && { analytics },
303
781
  apiUrl: options.apiUrl,
304
782
  authenticationUrl: options.authenticationUrl,
305
- projectSlug: options.projectSlug,
306
- environmentName: options.environmentName,
783
+ projectUuid: options.projectUuid,
307
784
  dehydratedState: dehydrate(queryClient),
308
- head: { meta: [{ title: match.layout._internal.title }] },
309
- href: request.url,
310
- layoutIdentity: getLayoutIdentity(shared.layout, source),
785
+ draftId: read.draftId,
786
+ refusedDraft: read.refusedDraft,
787
+ head: pageHead,
788
+ href: renderedHref(request.url, read),
789
+ layoutIdentity: getLayoutIdentity(shared.layout),
311
790
  loaderData: null,
312
791
  pathname,
313
792
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
314
- source,
793
+ locale: located.locale,
794
+ locales: located.locales,
315
795
  derived: {
316
796
  layoutId: match.layout._internal.id,
317
- data: serializedData,
797
+ result: serializedResult,
318
798
  layout: shared.layout
319
799
  }
320
800
  };
321
801
  const document = await options.getDocument?.() ?? {};
322
- await preparePreviewDocument(input, options, document, source === "live" && !dataOnly ? "" : void 0);
802
+ await preparePreviewDocument(input, options, document, !studio && !dataOnly ? "" : void 0);
323
803
  if (dataOnly) return Response.json(input, { headers: {
324
804
  "Cache-Control": "private, no-store",
325
805
  Vary: "Cookie",
326
806
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
327
807
  } });
328
808
  const html = await options.renderPage(input);
329
- const head = renderHead(source === "draft" ? {} : document, { title: match.layout._internal.title }, source === "draft" ? void 0 : options.stylesheetUrl, source === "draft" ? options.studioStylesheetUrl : void 0);
330
- 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: {
809
+ const head = renderHead(studio ? {} : document, createPageHeadInput(pageHead), studio ? void 0 : options.stylesheetUrl, studio ? options.studioStylesheetUrl : void 0, studio ? void 0 : located.locale);
810
+ return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(studio)}${head.headTags}${renderAnalyticsScripts(analytics)}${renderBlockPreloads(options, input.dehydratedState)}<script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, html))}<\/script></head><body${head.bodyAttrs}>${head.bodyTagsOpen}<div id="root">${html}</div>${head.bodyTags}${renderClientEntryScript(options.pageClientEntryUrl)}</body></html>`, { headers: {
331
811
  "Content-Type": "text/html; charset=utf-8",
332
812
  Vary: "Cookie",
333
- ...authCookieHeader ? { "Cache-Control": "private, no-store" } : {},
813
+ Link: llmsTxtLinkHeader(options),
814
+ ...isPrivateRender(request, options) ? { "Cache-Control": "private, no-store" } : {},
334
815
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
335
816
  } });
336
817
  } catch (error) {
@@ -338,26 +819,16 @@ async function createDerivedResponse(options, pathname, request, dataOnly = fals
338
819
  throw error;
339
820
  }
340
821
  }
341
- async function createStudioHtmlResponse({ match, options, request, dataOnly = false }) {
342
- if (!options.apiUrl || !options.authenticationUrl || !options.projectSlug || !dataOnly && !options.renderStudio) return Response.json({
822
+ async function createStudioHtmlResponse({ match, options, pathname, located, request, dataOnly = false }) {
823
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !dataOnly && !options.renderStudio) return Response.json({
343
824
  message: "Camox Studio renderer is not configured.",
344
825
  ...match
345
826
  }, { status: 500 });
346
827
  const queryClient = new QueryClient();
347
- const authCookieHeader = getServerAuthCookieHeader(request.headers);
348
- let project;
349
- let presentation = "public";
350
- let clearAuthCookie = false;
351
- if (authCookieHeader) try {
352
- project = await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug });
353
- presentation = "studio";
354
- } catch (error) {
355
- if (!isAuthSessionError(error)) throw error;
356
- clearAuthCookie = true;
357
- }
828
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
829
+ const presentation = project ? "studio" : "public";
358
830
  const studioRenderInput = {
359
831
  project,
360
- source: "live",
361
832
  head: { meta: [{ title: "Camox Studio" }] },
362
833
  layoutIdentity: null,
363
834
  loaderData: null,
@@ -365,12 +836,13 @@ async function createStudioHtmlResponse({ match, options, request, dataOnly = fa
365
836
  apiUrl: options.apiUrl,
366
837
  authenticationUrl: options.authenticationUrl,
367
838
  dehydratedState: dehydrate(queryClient),
368
- environmentName: options.environmentName,
369
839
  href: new URL(request.url).href,
370
- pathname: match.pathname,
371
- projectSlug: options.projectSlug,
840
+ pathname,
841
+ projectUuid: options.projectUuid,
372
842
  routeKind: match.kind,
373
- runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
843
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
844
+ locale: located.locale,
845
+ locales: located.locales
374
846
  };
375
847
  if (dataOnly) return Response.json(studioRenderInput, { headers: {
376
848
  "Cache-Control": "private, no-store",
@@ -399,19 +871,80 @@ ${renderClientEntryScript(options.studioClientEntryUrl)}
399
871
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
400
872
  } });
401
873
  }
402
- async function createPageDataResponse({ options, pathname, request }) {
403
- if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
404
- const singleton = await createDerivedResponse(options, pathname, request, true, true);
874
+ /**
875
+ * The Canvas with nothing selected, or a block of the Block Catalog, renders the
876
+ * page app, like a Page, so selecting it keeps the Canvas mounted. A block path
877
+ * without a page-content block of that id redirects home.
878
+ */
879
+ async function createCanvasOnlyResponse({ options, pathname, located, request, dataOnly = false }) {
880
+ const { blockId } = matchCanvasOnlyPath(located.path);
881
+ const app = blockId ? await options.getCamoxApp?.() : void 0;
882
+ const block = blockId ? app?.getBlockById(blockId) : void 0;
883
+ if (blockId && (!block || block._internal.layoutOnly)) {
884
+ if (dataOnly) return Response.json({
885
+ kind: "canvas",
886
+ message: "No block of the Block Catalog on this path",
887
+ pathname
888
+ }, { status: 404 });
889
+ return Response.redirect(new URL(withRuntimeBasePath(localizedPath("/", located.locale), options.runtimeBasePath), request.url), 302);
890
+ }
891
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !dataOnly && !options.renderPage) return Response.json({
892
+ message: "Camox page renderer is not configured.",
893
+ kind: "canvas",
894
+ pathname
895
+ }, { status: 500 });
896
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
897
+ const studio = project !== void 0;
898
+ const title = block?._internal.title ?? "Canvas";
899
+ const input = {
900
+ project,
901
+ presentation: studio ? "studio" : "public",
902
+ apiUrl: options.apiUrl,
903
+ authenticationUrl: options.authenticationUrl,
904
+ projectUuid: options.projectUuid,
905
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
906
+ locale: located.locale,
907
+ locales: located.locales,
908
+ href: request.url,
909
+ pathname,
910
+ head: { meta: [{ title }] },
911
+ layoutIdentity: null,
912
+ loaderData: null,
913
+ dehydratedState: dehydrate(new QueryClient()),
914
+ canvasOnly: blockId ? { blockId } : {},
915
+ draftId: studio ? getRequestDraftId(request, options) : void 0
916
+ };
917
+ await preparePreviewDocument(input, options, await options.getDocument?.() ?? {}, "");
918
+ const headers = {
919
+ "Cache-Control": "private, no-store",
920
+ Vary: "Cookie",
921
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
922
+ };
923
+ if (dataOnly) return Response.json(input, { headers });
924
+ const html = await options.renderPage(input);
925
+ const head = renderHead({}, { title }, void 0, studio ? options.studioStylesheetUrl : void 0);
926
+ return new Response(`<!doctype html><html${head.htmlAttrs}><head>${renderStudioThemeScript(studio)}${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: {
927
+ "Content-Type": "text/html; charset=utf-8",
928
+ ...headers
929
+ } });
930
+ }
931
+ async function createPageDataResponse({ options, pathname, located, request }) {
932
+ if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
933
+ const singleton = await createDerivedResponse(options, pathname, located, request, {
934
+ dataOnly: true,
935
+ singletonOnly: true
936
+ });
405
937
  if (singleton) return singleton;
406
938
  const queryClient = new QueryClient();
407
939
  try {
408
940
  const result = await loadCamoxPageForRequest({
409
941
  apiUrl: options.apiUrl,
410
942
  authCookieHeader: getServerAuthCookieHeader(request.headers),
411
- environmentName: options.environmentName,
943
+ draftId: getRequestDraftId(request, options),
944
+ locale: located.locale,
412
945
  origin: new URL(request.url).origin,
413
- pathname,
414
- projectSlug: options.projectSlug,
946
+ pathname: located.path,
947
+ projectUuid: options.projectUuid,
415
948
  queryClient
416
949
  });
417
950
  const headers = new Headers({
@@ -421,21 +954,32 @@ async function createPageDataResponse({ options, pathname, request }) {
421
954
  });
422
955
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
423
956
  const camoxApp = await options.getCamoxApp?.();
424
- const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : null;
957
+ const { alternates, head } = await readAlternates(request, options, {
958
+ located,
959
+ identity: {
960
+ kind: "curated",
961
+ pageId: result.data.page.page.id
962
+ },
963
+ head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
964
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
965
+ draftId: result.draftId
966
+ });
425
967
  const dehydratedState = dehydrate(queryClient);
426
- const project = result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0;
427
968
  const input = {
969
+ alternates,
428
970
  apiUrl: options.apiUrl,
429
971
  authenticationUrl: options.authenticationUrl ?? "",
430
- environmentName: options.environmentName,
431
- projectSlug: options.projectSlug,
972
+ projectUuid: options.projectUuid,
432
973
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
433
- href: request.url,
434
- presentation: result.source === "draft" ? "studio" : "public",
435
- source: result.source,
974
+ locale: located.locale,
975
+ locales: located.locales,
976
+ href: renderedHref(request.url, result),
977
+ draftId: result.draftId,
978
+ refusedDraft: result.refusedDraft,
979
+ presentation: result.project ? "studio" : "public",
436
980
  pathname,
437
- layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
438
- project,
981
+ layoutIdentity: getLayoutIdentity(result.data.page.layout),
982
+ project: result.project,
439
983
  loaderData: result.data,
440
984
  head,
441
985
  dehydratedState
@@ -444,7 +988,7 @@ async function createPageDataResponse({ options, pathname, request }) {
444
988
  return Response.json(input, { headers });
445
989
  } catch (error) {
446
990
  if (isNotFoundError(error)) {
447
- const derived = await createDerivedResponse(options, pathname, request, true);
991
+ const derived = await createDerivedResponse(options, pathname, located, request, { dataOnly: true });
448
992
  if (derived) return derived;
449
993
  return Response.json({
450
994
  kind: "page",
@@ -470,48 +1014,117 @@ async function handleCamoxRequest(request, options = {}) {
470
1014
  }
471
1015
  if (match.kind === "og") return createOgResponse(request, options);
472
1016
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1017
+ if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
1018
+ if (match.kind === "routes") return createRoutesResponse(request, options);
473
1019
  if (match.kind === "data") {
474
1020
  const targetPathname = normalizePagePath(new URL(request.url).searchParams.get("path"));
475
- const targetMatch = matchRuntimeRoute(targetPathname);
476
- if (targetMatch.kind.startsWith("studio")) return createStudioHtmlResponse({
477
- match: targetMatch,
478
- options,
479
- request,
480
- dataOnly: true
481
- });
482
- if (targetMatch.kind !== "page") return new Response("Not found", { status: 404 });
483
- return createPageDataResponse({
484
- options,
485
- pathname: targetPathname,
486
- request
487
- });
1021
+ if (matchRuntimeOwnedRoute(targetPathname)) return new Response("Not found", { status: 404 });
1022
+ return respondInLocale(options, request, targetPathname, { dataOnly: true });
488
1023
  }
489
- if (match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested") return createStudioHtmlResponse({
490
- match,
1024
+ if (isPreviewHandoff(request, options) && match.kind === "page") return createPreviewHandoffResponse({
491
1025
  options,
1026
+ pathname,
492
1027
  request
493
1028
  });
494
- if (match.kind === "page") {
495
- if (options.apiUrl && options.projectSlug) {
496
- const markdownResponse = await createMarkdownResponse({
497
- apiUrl: options.apiUrl,
498
- environmentName: options.environmentName,
499
- pathname: match.pathname,
500
- projectSlug: options.projectSlug,
1029
+ const { apiUrl, projectUuid } = options;
1030
+ const markdownPathname = parseMarkdownPath(pathname);
1031
+ if (apiUrl && projectUuid && (markdownPathname || prefersMarkdown(request.headers.get("Accept") ?? ""))) {
1032
+ const located = await locateRequest(request, options, markdownPathname ?? pathname, { live: true });
1033
+ if (markdownPathname || matchLocalizedRoute(located.path)?.kind === "page") {
1034
+ const markdownResponse = await createPageMarkdownResponse({
1035
+ ...options,
1036
+ apiUrl,
1037
+ projectUuid
1038
+ }, located, {
1039
+ pathname: markdownPathname ?? pathname,
501
1040
  request
502
1041
  });
503
1042
  if (markdownResponse) return markdownResponse;
1043
+ if (markdownPathname) return new Response("Page not found", { status: 404 });
504
1044
  }
505
- return createPageHtmlResponse({
506
- options,
507
- pathname: match.pathname,
508
- request
509
- });
510
1045
  }
511
- return Response.json({
512
- message: "Camox runtime route matched.",
513
- ...match
514
- }, { status: 501 });
1046
+ return respondInLocale(options, request, pathname);
1047
+ }
1048
+ /**
1049
+ * Answers a path under its locale's prefix: the Canvas, a studio route or a page, in that locale.
1050
+ * With `dataOnly`, as the JSON client navigation loads.
1051
+ */
1052
+ async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
1053
+ const located = await locateRequest(request, options, pathname);
1054
+ const localized = matchLocalizedRoute(located.path);
1055
+ if (!localized) return new Response("Not found", { status: 404 });
1056
+ const target = {
1057
+ options,
1058
+ pathname,
1059
+ located,
1060
+ request
1061
+ };
1062
+ if (localized.kind === "canvas") return createCanvasOnlyResponse({
1063
+ ...target,
1064
+ dataOnly
1065
+ });
1066
+ if (isStudioRoute(localized)) return createStudioHtmlResponse({
1067
+ ...target,
1068
+ match: localized,
1069
+ dataOnly
1070
+ });
1071
+ return dataOnly ? createPageDataResponse(target) : createPageHtmlResponse(target);
1072
+ }
1073
+ /**
1074
+ * A page's Markdown Representation in its locale, with the same precedence as its HTML:
1075
+ * singleton, curated page, then derived layout. Null when the page has none.
1076
+ */
1077
+ async function createPageMarkdownResponse(options, { locale, path }, { pathname, request }) {
1078
+ const page = {
1079
+ pathname,
1080
+ url: absoluteUrl(request, options)(pathname)
1081
+ };
1082
+ const app = await options.getCamoxApp?.();
1083
+ const layoutMatch = app && matchDerivedLayout(app.getLayouts(), path);
1084
+ if (layoutMatch?.layout._internal.kind === "singleton") return createLayoutMarkdownResponse(options, layoutMatch, locale, page);
1085
+ return createMarkdownResponse({
1086
+ apiUrl: options.apiUrl,
1087
+ camoxApp: app,
1088
+ locale,
1089
+ pathname: path,
1090
+ projectUuid: options.projectUuid,
1091
+ request,
1092
+ url: page.url,
1093
+ onNotFound: async () => layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null
1094
+ });
1095
+ }
1096
+ /** Whether a route is one of the studio's own, rather than a page or the Canvas. */
1097
+ function isStudioRoute(match) {
1098
+ return match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested";
1099
+ }
1100
+ /**
1101
+ * The studio or page route a locale-relative path names, or null for a runtime-owned route
1102
+ * under a locale's prefix (`/de/sitemap.xml`), which names nothing.
1103
+ */
1104
+ function matchLocalizedRoute(path) {
1105
+ const match = matchRuntimeRoute(path);
1106
+ return match.kind === "page" || match.kind === "canvas" || isStudioRoute(match) ? match : null;
1107
+ }
1108
+ /**
1109
+ * The locale a request's path names, among the locales of the scope it reads: the URL's draft's
1110
+ * for a session naming one, else Live's. `live` reads Live's whatever the request names. Without
1111
+ * an API there are none to read: every path is the default locale's, which then says it can't
1112
+ * render.
1113
+ */
1114
+ async function locateRequest(request, options, pathname, { live = false } = {}) {
1115
+ const { apiUrl, projectUuid } = options;
1116
+ const locales = apiUrl && projectUuid ? await readScopeLocales({
1117
+ apiUrl,
1118
+ projectUuid,
1119
+ ...!live && {
1120
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
1121
+ draftId: getRequestDraftId(request, options)
1122
+ }
1123
+ }) : [UNNAMED_DEFAULT_LOCALE];
1124
+ return {
1125
+ ...parseLocalePath(pathname, locales),
1126
+ locales
1127
+ };
515
1128
  }
516
1129
 
517
1130
  //#endregion