camox 0.44.0 → 0.46.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 (407) hide show
  1. package/dist/components/AuthGate.js +0 -6
  2. package/dist/components/DeviceSelector.js +96 -0
  3. package/dist/components/PlatformLabel.js +20 -24
  4. package/dist/core/components/lexical/HighlightNode.js +1 -1
  5. package/dist/core/components/lexical/InlineContentEditable.js +15 -19
  6. package/dist/core/components/lexical/InlineLexicalEditor.js +54 -74
  7. package/dist/core/components/lexical/InlineStylesPlugin.js +53 -53
  8. package/dist/core/components/lexical/MaxLengthPlugin.js +59 -0
  9. package/dist/core/components/lexical/SelectionBroadcaster.js +53 -54
  10. package/dist/core/components/lexical/SidebarLexicalEditor.js +322 -229
  11. package/dist/core/components/lexical/SummaryToggleGuard.js +42 -0
  12. package/dist/core/components/lexical/TextLinkPopover.js +169 -180
  13. package/dist/core/createApp.d.ts +241 -312
  14. package/dist/core/createApp.js +123 -29
  15. package/dist/core/createBlock.d.ts +5 -5
  16. package/dist/core/createBlock.js +385 -152
  17. package/dist/core/createCollection.d.ts +55 -0
  18. package/dist/core/createCollection.js +93 -0
  19. package/dist/core/createLayout.d.ts +197 -61
  20. package/dist/core/createLayout.js +114 -56
  21. package/dist/core/defineDocument.d.ts +3 -5
  22. package/dist/core/derivedRoutes.js +49 -1
  23. package/dist/core/dom.d.ts +13 -4
  24. package/dist/core/dom.js +44 -1
  25. package/dist/core/editing/BlockEditingRuntime.d.ts +16 -0
  26. package/dist/core/editing/BlockEditingRuntime.js +6 -15
  27. package/dist/core/editing/CompleteBlockEditingRuntime.js +28 -23
  28. package/dist/core/editing/createEditableBlock.d.ts +754 -633
  29. package/dist/core/editing/createEditableBlock.js +2113 -1066
  30. package/dist/core/editing/referenceWrites.js +51 -0
  31. package/dist/core/editing/useLatestRecord.js +90 -0
  32. package/dist/core/hooks/useArtifactHost.js +139 -0
  33. package/dist/core/hooks/useFieldSelection.js +43 -18
  34. package/dist/core/hooks/useIsEditable.js +12 -10
  35. package/dist/core/hooks/useOverlayMessage.js +15 -19
  36. package/dist/core/lib/artifact.d.ts +21 -0
  37. package/dist/core/lib/artifact.js +154 -0
  38. package/dist/core/lib/artifactColors.js +123 -0
  39. package/dist/core/lib/artifactData.d.ts +1 -0
  40. package/dist/core/lib/artifactData.js +131 -0
  41. package/dist/core/lib/artifactHost.js +205 -0
  42. package/dist/core/lib/artifactRuntime.js +36 -0
  43. package/dist/core/lib/artifactTheme.js +57 -0
  44. package/dist/core/lib/contentType.d.ts +302 -106
  45. package/dist/core/lib/contentType.js +201 -173
  46. package/dist/core/lib/fieldTypes.js +3 -0
  47. package/dist/core/lib/iconTypes.d.ts +3 -3
  48. package/dist/core/lib/icons.d.ts +4 -4
  49. package/dist/core/lib/icons.js +40 -44
  50. package/dist/core/lib/imagePlaceholder.js +177 -0
  51. package/dist/core/lib/imageTransform.js +1 -1
  52. package/dist/core/lib/lexicalReact.d.ts +3 -1
  53. package/dist/core/lib/lexicalState.js +1 -1
  54. package/dist/core/lib/previewPaused.js +9 -0
  55. package/dist/core/lib/recordScope.js +316 -0
  56. package/dist/core/lib/reference.d.ts +81 -0
  57. package/dist/core/lib/reference.js +29 -0
  58. package/dist/core/lib/textLinks.js +7 -5
  59. package/dist/core/lib/textStyles.d.ts +5 -7
  60. package/dist/core/lib/textStyles.js +2 -2
  61. package/dist/core/notFound.d.ts +7 -0
  62. package/dist/core/notFound.js +11 -0
  63. package/dist/core/pageDestinations.js +6 -3
  64. package/dist/core/recordItem.js +34 -0
  65. package/dist/core/requestContext.d.ts +26 -0
  66. package/dist/core/requestContext.js +20 -0
  67. package/dist/features/canvas/CamoxCanvas.js +1401 -0
  68. package/dist/features/canvas/CanvasLoadingScreen.js +35 -0
  69. package/dist/features/canvas/CanvasOverlays.js +254 -0
  70. package/dist/features/canvas/CanvasPageEdge.js +87 -0
  71. package/dist/features/canvas/CanvasPageFrame.js +333 -0
  72. package/dist/features/canvas/CanvasPageHeader.js +125 -0
  73. package/dist/features/canvas/CanvasPreviewPageSlot.js +123 -0
  74. package/dist/features/canvas/DerivedPagePathInput.js +238 -0
  75. package/dist/features/canvas/canvasCamera.js +229 -0
  76. package/dist/features/canvas/canvasFlight.js +104 -0
  77. package/dist/features/canvas/canvasFrameCapture.js +364 -0
  78. package/dist/features/canvas/canvasFrameDocument.js +302 -0
  79. package/dist/features/canvas/canvasFrameMode.js +53 -0
  80. package/dist/features/canvas/canvasInsertionSeams.js +44 -0
  81. package/dist/features/canvas/canvasItems.js +30 -0
  82. package/dist/features/canvas/canvasOverlayGeometry.js +240 -0
  83. package/dist/features/canvas/canvasPages.js +79 -0
  84. package/dist/features/canvas/canvasScreenSpace.js +26 -0
  85. package/dist/features/canvas/canvasSelection.js +18 -0
  86. package/dist/features/canvas/canvasStore.js +60 -0
  87. package/dist/features/canvas/canvasThumbnails.js +304 -0
  88. package/dist/features/canvas/canvasZoom.js +10 -0
  89. package/dist/features/canvas/sidebar/CanvasInspector.js +202 -0
  90. package/dist/features/canvas/sidebar/CanvasNavigator.js +124 -0
  91. package/dist/features/canvas/sidebar/CanvasSidebar.js +54 -0
  92. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +48 -0
  93. package/dist/features/canvas/sidebar/CanvasSidebarSkeleton.js +39 -0
  94. package/dist/features/canvas/sidebar/CuratedPageInspector.js +69 -0
  95. package/dist/features/canvas/sidebar/DerivedPageInspector.js +37 -0
  96. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +78 -0
  97. package/dist/features/canvas/sidebar/LayoutPageOverview.js +71 -0
  98. package/dist/features/canvas/sidebar/PageInspector.js +157 -0
  99. package/dist/features/canvas/sidebar/PageInspectorSkeleton.js +90 -0
  100. package/dist/features/canvas/sidebar/PageOverview.js +27 -0
  101. package/dist/features/canvas/sidebar/SingletonPageInspector.js +31 -0
  102. package/dist/features/canvas/useCanvasCamera.js +783 -0
  103. package/dist/features/canvas/useCanvasFrame.js +148 -0
  104. package/dist/features/canvas/useCanvasFrameInput.js +97 -0
  105. package/dist/features/canvas/useCanvasNearness.js +57 -0
  106. package/dist/features/canvas/useDiscoveredPreviewPaths.js +62 -0
  107. package/dist/features/content/CamoxContent.d.ts +2 -5
  108. package/dist/features/content/CamoxContent.js +174 -271
  109. package/dist/features/content/CollectionItemSheetContext.js +106 -0
  110. package/dist/features/content/ContentAssets.js +315 -0
  111. package/dist/features/content/ContentCollection.js +663 -0
  112. package/dist/features/content/collection-form.js +58 -0
  113. package/dist/features/content/components/AssetCard.js +80 -84
  114. package/dist/features/content/components/AssetCardSkeleton.js +4 -8
  115. package/dist/features/content/components/ContentSidebar.js +129 -18
  116. package/dist/features/content/components/ContentSidebarGroup.js +37 -0
  117. package/dist/features/content/components/DeleteCollectionItemButton.js +202 -0
  118. package/dist/features/content/components/MissingFromLocale.js +282 -0
  119. package/dist/features/content/components/UploadDropZone.js +27 -31
  120. package/dist/features/content/components/UploadProgressDrawer.js +90 -98
  121. package/dist/features/navigation/navigation.d.ts +66 -20
  122. package/dist/features/navigation/navigation.js +257 -90
  123. package/dist/features/og/defaultFonts.js +8 -8
  124. package/dist/features/og/geist-variable-ttf-base64.js +1 -1
  125. package/dist/features/og/imageResponse.d.ts +8 -0
  126. package/dist/features/og/imageResponse.js +26 -0
  127. package/dist/features/page/CuratedPageContent.js +167 -0
  128. package/dist/features/page/DerivedPageContent.js +26 -30
  129. package/dist/features/page/SharedLayoutContent.js +25 -27
  130. package/dist/features/preview/CamoxPreview.d.ts +23 -61
  131. package/dist/features/preview/CamoxPreview.js +816 -537
  132. package/dist/features/preview/EditablePageContent.js +1 -1
  133. package/dist/features/preview/EditablePageExperience.js +16 -22
  134. package/dist/features/preview/components/AddBlockSheet.js +273 -0
  135. package/dist/features/preview/components/AssetFieldEditor.js +173 -154
  136. package/dist/features/preview/components/AssetLightbox.js +181 -134
  137. package/dist/features/preview/components/AssetPickerModal.js +153 -170
  138. package/dist/features/preview/components/BlockActionsPopover.js +150 -149
  139. package/dist/features/preview/components/BlockErrorBoundary.js +8 -12
  140. package/dist/features/preview/components/BlockThumbnail.js +172 -98
  141. package/dist/features/preview/components/CanvasPreview.js +243 -0
  142. package/dist/features/preview/components/CreatePageModal.js +128 -137
  143. package/dist/features/preview/components/DebouncedFieldEditor.js +29 -33
  144. package/dist/features/preview/components/DerivedLayoutDialog.js +21 -19
  145. package/dist/features/preview/components/DrillRow.js +150 -0
  146. package/dist/features/preview/components/FieldOverlayStyles.js +11 -8
  147. package/dist/features/preview/components/FieldToolbar.js +160 -110
  148. package/dist/features/preview/components/Frame.js +108 -20
  149. package/dist/features/preview/components/IconFieldEditor.js +62 -66
  150. package/dist/features/preview/components/ItemFieldsEditor.js +231 -205
  151. package/dist/features/preview/components/LinkFieldEditor.js +135 -139
  152. package/dist/features/preview/components/Metadata.js +22 -30
  153. package/dist/features/preview/components/MobilePreviewDrawer.js +89 -108
  154. package/dist/features/preview/components/MultipleAssetFieldEditor.js +334 -246
  155. package/dist/features/preview/components/OverlayTracker.js +9 -41
  156. package/dist/features/preview/components/Overlays.js +81 -69
  157. package/dist/features/preview/components/PageInfo.js +1748 -0
  158. package/dist/features/preview/components/PageInfoSkeleton.js +51 -0
  159. package/dist/features/preview/components/PageLocationFieldset.js +112 -116
  160. package/dist/features/preview/components/PageNavigatorHeader.js +24 -0
  161. package/dist/features/preview/components/PageNicknameField.js +22 -26
  162. package/dist/features/preview/components/PagePicker.js +128 -130
  163. package/dist/features/preview/components/PageTree.js +525 -532
  164. package/dist/features/preview/components/PreviewActivation.js +28 -32
  165. package/dist/features/preview/components/PreviewPanel.js +620 -319
  166. package/dist/features/preview/components/PreviewToolbar.js +105 -333
  167. package/dist/features/preview/components/RecordCard.js +80 -0
  168. package/dist/features/preview/components/RecordCombobox.js +178 -0
  169. package/dist/features/preview/components/ReferenceFieldEditor.js +149 -0
  170. package/dist/features/preview/components/ReferenceListFieldEditor.js +432 -0
  171. package/dist/features/preview/components/RemovedBlockPlaceholder.js +95 -0
  172. package/dist/features/preview/components/RepeatableItemsList.js +125 -138
  173. package/dist/features/preview/components/SelectionEditor.js +993 -0
  174. package/dist/features/preview/components/ShikiMarkdown.js +14 -18
  175. package/dist/features/preview/components/SidebarSection.js +52 -64
  176. package/dist/features/preview/components/StudioEditingFallback.js +56 -0
  177. package/dist/features/preview/components/UnlinkAssetButton.js +104 -107
  178. package/dist/features/preview/components/blockGridNavigation.js +15 -0
  179. package/dist/features/preview/components/contentFieldSchema.js +8 -0
  180. package/dist/features/preview/components/useFieldWriter.js +200 -0
  181. package/dist/features/preview/components/useRecordView.js +383 -0
  182. package/dist/features/preview/components/useRepeatableItemActions.js +35 -37
  183. package/dist/features/preview/components/useUpdateBlockPosition.js +37 -37
  184. package/dist/features/preview/editingFrame.js +197 -0
  185. package/dist/features/preview/overlayHighlights.js +17 -44
  186. package/dist/features/preview/overlayMessages.js +72 -1
  187. package/dist/features/preview/pageRecord.js +189 -0
  188. package/dist/features/preview/previewConstants.js +6 -2
  189. package/dist/features/preview/previewPageFrame.js +95 -0
  190. package/dist/features/preview/previewPreparation.js +7 -0
  191. package/dist/features/preview/previewSelection.js +32 -63
  192. package/dist/features/preview/previewStore.js +225 -123
  193. package/dist/features/preview/referencePickerFocus.js +29 -0
  194. package/dist/features/preview/repeatableItemTargets.js +56 -0
  195. package/dist/features/preview/selectionReveal.js +21 -0
  196. package/dist/features/preview/usePreviewViewportActions.js +114 -0
  197. package/dist/features/preview/viewportContinuity.js +109 -0
  198. package/dist/features/provider/AuthenticatedCamoxProvider.js +69 -44
  199. package/dist/features/provider/CamoxProvider.d.ts +3 -6
  200. package/dist/features/provider/CamoxProvider.js +38 -50
  201. package/dist/features/provider/CoreCamoxProvider.js +69 -50
  202. package/dist/features/provider/LocalhostPreviewProvider.js +78 -114
  203. package/dist/features/provider/PreviewSignInPage.js +54 -0
  204. package/dist/features/provider/components/CamoxAppContext.js +11 -11
  205. package/dist/features/provider/components/CommandPalette.js +75 -82
  206. package/dist/features/provider/useAdminShortcuts.js +7 -11
  207. package/dist/features/routes/pageRuntime.d.ts +44 -4
  208. package/dist/features/routes/pageRuntime.js +244 -137
  209. package/dist/features/runtime/RuntimeChrome.js +117 -115
  210. package/dist/features/runtime/alternates.js +64 -0
  211. package/dist/features/runtime/analytics.d.ts +7 -0
  212. package/dist/features/runtime/analytics.js +66 -0
  213. package/dist/features/runtime/artifactSources.js +87 -0
  214. package/dist/features/runtime/editingFallback.js +20 -0
  215. package/dist/features/runtime/linkPrefetch.js +97 -0
  216. package/dist/features/runtime/livePages.js +62 -0
  217. package/dist/features/runtime/llmsTxt.js +40 -0
  218. package/dist/features/runtime/loopbackFetch.js +18 -0
  219. package/dist/features/runtime/navigationAccessibility.js +51 -0
  220. package/dist/features/runtime/navigationTarget.js +23 -2
  221. package/dist/features/runtime/pageApp.js +229 -158
  222. package/dist/features/runtime/pageBlockTypes.js +20 -0
  223. package/dist/features/runtime/pageClient.d.ts +2 -4
  224. package/dist/features/runtime/pageClient.js +18 -3
  225. package/dist/features/runtime/pageNavigation.js +254 -32
  226. package/dist/features/runtime/pageServer.d.ts +2 -4
  227. package/dist/features/runtime/pageServer.js +6 -2
  228. package/dist/features/runtime/pageViewport.js +7 -0
  229. package/dist/features/runtime/runtime.d.ts +54 -24
  230. package/dist/features/runtime/runtime.js +766 -191
  231. package/dist/features/runtime/runtimeChromeLoader.js +58 -0
  232. package/dist/features/runtime/runtimeRoutes.d.ts +19 -0
  233. package/dist/features/runtime/runtimeRoutes.js +25 -0
  234. package/dist/features/runtime/scrollRestoration.js +133 -0
  235. package/dist/features/runtime/studioApp.d.ts +4 -3
  236. package/dist/features/runtime/studioApp.js +5 -9
  237. package/dist/features/runtime/studioClient.d.ts +2 -4
  238. package/dist/features/runtime/studioClient.js +12 -3
  239. package/dist/features/runtime/studioServer.d.ts +2 -4
  240. package/dist/features/runtime/studioServer.js +6 -1
  241. package/dist/features/runtime/worker.d.ts +6 -0
  242. package/dist/features/runtime/worker.js +23 -0
  243. package/dist/features/runtime/workerRequest.js +44 -0
  244. package/dist/features/studio/CamoxStudio.d.ts +2 -6
  245. package/dist/features/studio/CamoxStudio.js +34 -49
  246. package/dist/features/studio/FollowAgentProvider.js +327 -0
  247. package/dist/features/studio/components/ChatSidebar.js +356 -0
  248. package/dist/features/studio/components/DraftsControl.js +676 -0
  249. package/dist/features/studio/components/FaviconDialog.js +318 -0
  250. package/dist/features/studio/components/LocalePicker.js +785 -0
  251. package/dist/features/studio/components/Navbar.js +123 -103
  252. package/dist/features/studio/components/ProjectMenu.js +239 -260
  253. package/dist/features/studio/components/RenameDraftDialog.js +206 -0
  254. package/dist/features/studio/components/ReviewDraftSheet.js +1472 -0
  255. package/dist/features/studio/components/ThreadComposer.js +157 -0
  256. package/dist/features/studio/components/ThreadMarkdown.js +113 -0
  257. package/dist/features/studio/components/ThreadMessages.js +270 -0
  258. package/dist/features/studio/components/UserButton.js +34 -23
  259. package/dist/features/studio/draftLabel.js +33 -0
  260. package/dist/features/studio/draftNotice.js +13 -0
  261. package/dist/features/studio/followAgent.js +241 -0
  262. package/dist/features/studio/followAgentContext.js +7 -0
  263. package/dist/features/studio/localeOptions.js +118 -0
  264. package/dist/features/studio/localeSwitch.js +19 -0
  265. package/dist/features/studio/routes.js +26 -1
  266. package/dist/features/studio/studioStore.js +64 -1
  267. package/dist/features/studio/useTheme.js +24 -27
  268. package/dist/features/vite/artifactRuntimeMinification.js +36 -0
  269. package/dist/features/vite/blockBoilerplate.js +6 -5
  270. package/dist/features/vite/blockChunks.js +31 -0
  271. package/dist/features/vite/bundlerLogs.js +17 -0
  272. package/dist/features/vite/definitionsSync.js +158 -226
  273. package/dist/features/vite/devAuthentication.js +4 -16
  274. package/dist/features/vite/devSignIn.js +87 -0
  275. package/dist/features/vite/iconGeneration.js +1 -1
  276. package/dist/features/vite/layoutGeneration.js +11 -1
  277. package/dist/features/vite/publishSeededContent.js +30 -0
  278. package/dist/features/vite/routeGeneration.js +6 -7
  279. package/dist/features/vite/runtimeDev.js +80 -99
  280. package/dist/features/vite/vite.d.ts +51 -31
  281. package/dist/features/vite/vite.js +224 -127
  282. package/dist/features/vite/workerBuild.js +39 -0
  283. package/dist/hooks/use-debounced-field.js +116 -47
  284. package/dist/hooks/use-file-upload.js +114 -67
  285. package/dist/hooks/use-marquee-selection.js +32 -36
  286. package/dist/hooks/use-page-destinations.js +69 -37
  287. package/dist/lib/api-client-server.d.ts +5 -0
  288. package/dist/lib/api-client-server.js +10 -4
  289. package/dist/lib/api-client.d.ts +6 -0
  290. package/dist/lib/api-client.js +51 -16
  291. package/dist/lib/api-socket.js +17 -0
  292. package/dist/lib/auth-page-styles.js +39 -0
  293. package/dist/lib/auth.js +129 -61
  294. package/dist/lib/collection-cache.js +23 -0
  295. package/dist/lib/draft-scope.d.ts +4 -0
  296. package/dist/lib/draft-scope.js +222 -0
  297. package/dist/lib/html.js +12 -0
  298. package/dist/lib/link-pages.js +154 -0
  299. package/dist/lib/locale-path.d.ts +22 -0
  300. package/dist/lib/locale-path.js +57 -0
  301. package/dist/lib/normalized-data.js +252 -152
  302. package/dist/lib/preview-handoff.js +2 -2
  303. package/dist/lib/queries.d.ts +11 -0
  304. package/dist/lib/queries.js +70 -141
  305. package/dist/lib/studio-reads.d.ts +14 -0
  306. package/dist/lib/studio-reads.js +554 -0
  307. package/dist/lib/use-project-room.js +50 -50
  308. package/dist/lib/use-thread.js +310 -0
  309. package/dist/lib/utils.js +25 -29
  310. package/dist/lib/view-locale-context.js +22 -0
  311. package/dist/lib/view-locale.js +46 -0
  312. package/dist/package.js +5 -0
  313. package/dist/studio-overlays.css +41 -151
  314. package/dist/studio.css +2 -2
  315. package/package.json +70 -73
  316. package/skills/camox/SKILL.md +16 -11
  317. package/skills/camox/references/artifacts.md +179 -0
  318. package/skills/camox/references/block-definitions.md +75 -59
  319. package/skills/camox/references/cli-common.md +9 -11
  320. package/skills/camox/references/cli-content.md +92 -69
  321. package/skills/camox/references/cli-publishing.md +26 -0
  322. package/skills/camox/references/dom-integrations.md +27 -0
  323. package/skills/camox/references/field-styling.md +4 -4
  324. package/skills/camox/references/layout-definitions.md +16 -14
  325. package/skills/camox/references/localization.md +56 -0
  326. package/skills/camox/references/routed-layouts.md +133 -15
  327. package/skills/camox-expand/SKILL.md +70 -0
  328. package/skills/camox-expand/references/jobs.md +39 -0
  329. package/skills/camoxify/SKILL.md +29 -7
  330. package/dist/core/components/AddBlockControlBar.js +0 -78
  331. package/dist/core/hooks/useRequireDraftSource.js +0 -23
  332. package/dist/features/metadata/sitemap.d.ts +0 -8
  333. package/dist/features/metadata/sitemap.js +0 -17
  334. package/dist/features/og/imageResponse.node.d.ts +0 -12
  335. package/dist/features/og/imageResponse.node.js +0 -49
  336. package/dist/features/og/imageResponse.workerd.d.ts +0 -12
  337. package/dist/features/og/imageResponse.workerd.js +0 -40
  338. package/dist/features/page/PublishedPageContent.js +0 -205
  339. package/dist/features/page/PublishedPageExperience.js +0 -23
  340. package/dist/features/preview/DerivedPreview.js +0 -134
  341. package/dist/features/preview/commentsEnabled.js +0 -8
  342. package/dist/features/preview/components/AddBlockSidebar.js +0 -381
  343. package/dist/features/preview/components/AttachedComments.js +0 -331
  344. package/dist/features/preview/components/CommentHeader.js +0 -122
  345. package/dist/features/preview/components/CommentSidebar.js +0 -29
  346. package/dist/features/preview/components/CommentTargetQuote.js +0 -167
  347. package/dist/features/preview/components/DerivedLayoutSidebar.d.ts +0 -5
  348. package/dist/features/preview/components/DerivedLayoutSidebar.js +0 -179
  349. package/dist/features/preview/components/DerivedPageInfoSidebar.js +0 -69
  350. package/dist/features/preview/components/LeftSidebar.js +0 -68
  351. package/dist/features/preview/components/PageEditorSidebar.js +0 -563
  352. package/dist/features/preview/components/PageInfoSidebar.js +0 -1551
  353. package/dist/features/preview/components/PageNavigatorSidebar.js +0 -78
  354. package/dist/features/preview/components/PageStatusBadge.js +0 -97
  355. package/dist/features/preview/components/PeekedBlock.js +0 -217
  356. package/dist/features/preview/components/PreviewComments.js +0 -56
  357. package/dist/features/preview/components/PreviewToolbarSpacer.js +0 -43
  358. package/dist/features/preview/components/PublicationRow.js +0 -422
  359. package/dist/features/preview/components/PublishDialog.js +0 -194
  360. package/dist/features/preview/components/RightSidebar.js +0 -59
  361. package/dist/features/preview/components/SendFeedbackDialog.js +0 -83
  362. package/dist/features/preview/previewCommentsStore.js +0 -135
  363. package/dist/features/preview/publication.js +0 -81
  364. package/dist/features/preview/usePageComments.js +0 -39
  365. package/dist/features/runtime/SharedChromeContext.js +0 -7
  366. package/dist/features/studio/components/EnvironmentMenu.js +0 -425
  367. package/dist/features/vite/appGeneration.js +0 -89
  368. package/dist/lib/view-queries.js +0 -28
  369. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/Matches.d.ts +0 -114
  370. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/RouterProvider.d.ts +0 -25
  371. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/config.d.ts +0 -1
  372. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/defer.d.ts +0 -1
  373. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/fileRoute.d.ts +0 -20
  374. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/global.d.ts +0 -8
  375. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/hash-scroll.d.ts +0 -1
  376. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/index.d.ts +0 -18
  377. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/link.d.ts +0 -161
  378. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/load-matches.d.ts +0 -6
  379. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/location.d.ts +0 -54
  380. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/lru-cache.d.ts +0 -8
  381. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/manifest.d.ts +0 -32
  382. 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
  383. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/not-found.d.ts +0 -1
  384. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/redirect.d.ts +0 -49
  385. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/rewrite.d.ts +0 -1
  386. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/root.d.ts +0 -6
  387. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/route.d.ts +0 -343
  388. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/routeInfo.d.ts +0 -48
  389. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/router.d.ts +0 -695
  390. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/scroll-restoration.d.ts +0 -1
  391. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/searchMiddleware.d.ts +0 -1
  392. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/searchParams.d.ts +0 -5
  393. 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
  394. 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
  395. 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
  396. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/stores.d.ts +0 -69
  397. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/structuralSharing.d.ts +0 -1
  398. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/typePrimitives.d.ts +0 -1
  399. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useLoaderData.d.ts +0 -1
  400. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useLoaderDeps.d.ts +0 -1
  401. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useNavigate.d.ts +0 -10
  402. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useParams.d.ts +0 -1
  403. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useRouteContext.d.ts +0 -1
  404. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/useSearch.d.ts +0 -1
  405. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/utils.d.ts +0 -28
  406. package/dist/node_modules/.pnpm/@tanstack_router-core@1.168.14/node_modules/@tanstack/router-core/dist/esm/validators.d.ts +0 -49
  407. 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_CANVAS_PATH, STUDIO_CONTENT_PATH, 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 (pathname === "/camox/canvas") 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,88 +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 requestUrl = new URL(request.url);
212
- if (requestUrl.searchParams.has("camox-preview") && requestUrl.searchParams.has("ott")) {
213
- const input = {
214
- previewHandoff: true,
215
- presentation: "public",
216
- source: "live",
217
- apiUrl: options.apiUrl,
218
- authenticationUrl: options.authenticationUrl,
219
- environmentName: options.environmentName,
220
- projectSlug: options.projectSlug,
221
- href: requestUrl.href,
222
- pathname,
223
- runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
224
- head: {},
225
- loaderData: null,
226
- layoutIdentity: null,
227
- dehydratedState: dehydrate(new QueryClient())
228
- };
229
- const html = await options.renderPage(input);
230
- return new Response(`<!doctype html><html><head><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: {
231
- "Content-Type": "text/html; charset=utf-8",
232
- "Cache-Control": "private, no-store",
233
- "Referrer-Policy": "no-referrer"
234
- } });
235
- }
236
- const singleton = await createDerivedResponse(options, pathname, request, false, true);
488
+ const singleton = await createDerivedResponse(options, pathname, located, request, { singletonOnly: true });
237
489
  if (singleton) return singleton;
238
490
  const queryClient = new QueryClient();
239
491
  try {
240
492
  const result = await loadCamoxPageForRequest({
241
493
  apiUrl: options.apiUrl,
242
494
  authCookieHeader: getServerAuthCookieHeader(request.headers),
243
- environmentName: options.environmentName,
495
+ draftId: getRequestDraftId(request, options),
496
+ locale: located.locale,
244
497
  origin: new URL(request.url).origin,
245
- pathname,
246
- projectSlug: options.projectSlug,
498
+ pathname: located.path,
499
+ projectUuid: options.projectUuid,
247
500
  queryClient
248
501
  });
249
502
  const headers = new Headers({
250
503
  "Content-Type": "text/html; charset=utf-8",
251
- Vary: "Cookie"
504
+ Vary: "Cookie",
505
+ Link: llmsTxtLinkHeader(options)
252
506
  });
253
- if (getServerAuthCookieHeader(request.headers)) headers.set("Cache-Control", "private, no-store");
507
+ if (isPrivateRender(request, options)) headers.set("Cache-Control", "private, no-store");
254
508
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
255
509
  const camoxApp = await options.getCamoxApp?.();
256
510
  const document = await options.getDocument?.() ?? {};
257
- 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
+ });
258
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);
259
528
  const pageRenderInput = {
260
- project: result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
261
- presentation: result.source === "draft" ? "studio" : "public",
529
+ alternates,
530
+ project,
531
+ presentation: studio ? "studio" : "public",
532
+ ...analytics && { analytics },
262
533
  apiUrl: options.apiUrl,
263
534
  authenticationUrl: options.authenticationUrl,
264
535
  dehydratedState,
265
- environmentName: options.environmentName,
536
+ draftId: result.draftId,
537
+ refusedDraft: result.refusedDraft,
266
538
  head,
267
- href: new URL(request.url).href,
268
- layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
539
+ href: renderedHref(request.url, result),
540
+ layoutIdentity: getLayoutIdentity(result.data.page.layout),
269
541
  loaderData: result.data,
270
542
  pathname,
271
- projectSlug: options.projectSlug,
543
+ projectUuid: options.projectUuid,
272
544
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
273
- source: result.source
545
+ locale: located.locale,
546
+ locales: located.locales
274
547
  };
275
- await preparePreviewDocument(pageRenderInput, options, document, result.source === "live" ? "" : void 0);
548
+ await preparePreviewDocument(pageRenderInput, options, document, studio ? void 0 : "");
276
549
  const appHtml = await options.renderPage(pageRenderInput);
277
- 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);
278
551
  return new Response(`<!doctype html>
279
552
  <html${renderedHead.htmlAttrs}>
280
553
  <head>
281
- ${renderStudioThemeScript(result.source === "draft")}
554
+ ${renderStudioThemeScript(studio)}
282
555
  ${renderedHead.headTags}
283
- <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>
284
559
  </head>
285
560
  <body${renderedHead.bodyAttrs}>
286
561
  ${renderedHead.bodyTagsOpen}
@@ -291,71 +566,252 @@ ${renderClientEntryScript(options.pageClientEntryUrl)}
291
566
  </html>`, { headers });
292
567
  } catch (error) {
293
568
  if (isNotFoundError(error)) {
294
- const derived = await createDerivedResponse(options, pathname, request);
569
+ const derived = await createDerivedResponse(options, pathname, located, request);
295
570
  if (derived) return derived;
296
571
  return new Response("No Camox page on this path", { status: 404 });
297
572
  }
298
573
  throw error;
299
574
  }
300
575
  }
301
- 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 });
638
+ const app = await options.getCamoxApp?.();
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 } = {}) {
302
707
  const app = await options.getCamoxApp?.();
303
- const match = app && matchDerivedLayout(app.getLayouts(), pathname);
708
+ const match = app && matchDerivedLayout(app.getLayouts(), located.path);
304
709
  if (!match || !dataOnly && !options.renderPage) return null;
305
710
  if (singletonOnly && match.layout._internal.kind !== "singleton") return null;
306
711
  try {
307
- const data = await match.layout._internal.loader?.({ params: match.params });
308
- const serializedData = data === void 0 ? void 0 : JSON.parse(JSON.stringify(data));
309
712
  const queryClient = new QueryClient();
310
- const authCookieHeader = getServerAuthCookieHeader(request.headers);
311
- let source = authCookieHeader ? "draft" : "live";
312
- let clearAuthCookie = false;
313
- const loadLayout = (source) => createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: source === "draft" ? authCookieHeader : void 0 }).layouts.get({
314
- projectSlug: options.projectSlug,
315
- layoutId: match.layout._internal.id,
316
- 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
317
735
  });
318
- const shared = await loadLayout(source).catch(async (error) => {
319
- if (source !== "draft" || !isAuthSessionError(error)) throw error;
320
- source = "live";
321
- clearAuthCookie = true;
322
- 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
323
771
  });
324
- seedBlockCaches(queryClient, shared, source);
772
+ const analytics = dataOnly ? void 0 : visitorAnalytics(options, {
773
+ studio,
774
+ draftId: read.draftId
775
+ }, shared.project);
325
776
  const input = {
326
- project: source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug }) : void 0,
327
- presentation: source === "draft" ? "studio" : "public",
777
+ alternates,
778
+ project,
779
+ presentation: studio ? "studio" : "public",
780
+ ...analytics && { analytics },
328
781
  apiUrl: options.apiUrl,
329
782
  authenticationUrl: options.authenticationUrl,
330
- projectSlug: options.projectSlug,
331
- environmentName: options.environmentName,
783
+ projectUuid: options.projectUuid,
332
784
  dehydratedState: dehydrate(queryClient),
333
- head: { meta: [{ title: match.layout._internal.title }] },
334
- href: request.url,
335
- 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),
336
790
  loaderData: null,
337
791
  pathname,
338
792
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
339
- source,
793
+ locale: located.locale,
794
+ locales: located.locales,
340
795
  derived: {
341
796
  layoutId: match.layout._internal.id,
342
- data: serializedData,
797
+ result: serializedResult,
343
798
  layout: shared.layout
344
799
  }
345
800
  };
346
801
  const document = await options.getDocument?.() ?? {};
347
- await preparePreviewDocument(input, options, document, source === "live" && !dataOnly ? "" : void 0);
802
+ await preparePreviewDocument(input, options, document, !studio && !dataOnly ? "" : void 0);
348
803
  if (dataOnly) return Response.json(input, { headers: {
349
804
  "Cache-Control": "private, no-store",
350
805
  Vary: "Cookie",
351
806
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
352
807
  } });
353
808
  const html = await options.renderPage(input);
354
- const head = renderHead(source === "draft" ? {} : document, { title: match.layout._internal.title }, source === "draft" ? void 0 : options.stylesheetUrl, source === "draft" ? options.studioStylesheetUrl : void 0);
355
- 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: {
356
811
  "Content-Type": "text/html; charset=utf-8",
357
812
  Vary: "Cookie",
358
- ...authCookieHeader ? { "Cache-Control": "private, no-store" } : {},
813
+ Link: llmsTxtLinkHeader(options),
814
+ ...isPrivateRender(request, options) ? { "Cache-Control": "private, no-store" } : {},
359
815
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
360
816
  } });
361
817
  } catch (error) {
@@ -363,26 +819,16 @@ async function createDerivedResponse(options, pathname, request, dataOnly = fals
363
819
  throw error;
364
820
  }
365
821
  }
366
- async function createStudioHtmlResponse({ match, options, request, dataOnly = false }) {
367
- 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({
368
824
  message: "Camox Studio renderer is not configured.",
369
825
  ...match
370
826
  }, { status: 500 });
371
827
  const queryClient = new QueryClient();
372
- const authCookieHeader = getServerAuthCookieHeader(request.headers);
373
- let project;
374
- let presentation = "public";
375
- let clearAuthCookie = false;
376
- if (authCookieHeader) try {
377
- project = await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader }).projects.getBySlug({ slug: options.projectSlug });
378
- presentation = "studio";
379
- } catch (error) {
380
- if (!isAuthSessionError(error)) throw error;
381
- clearAuthCookie = true;
382
- }
828
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
829
+ const presentation = project ? "studio" : "public";
383
830
  const studioRenderInput = {
384
831
  project,
385
- source: "live",
386
832
  head: { meta: [{ title: "Camox Studio" }] },
387
833
  layoutIdentity: null,
388
834
  loaderData: null,
@@ -390,12 +836,13 @@ async function createStudioHtmlResponse({ match, options, request, dataOnly = fa
390
836
  apiUrl: options.apiUrl,
391
837
  authenticationUrl: options.authenticationUrl,
392
838
  dehydratedState: dehydrate(queryClient),
393
- environmentName: options.environmentName,
394
839
  href: new URL(request.url).href,
395
- pathname: match.pathname,
396
- projectSlug: options.projectSlug,
840
+ pathname,
841
+ projectUuid: options.projectUuid,
397
842
  routeKind: match.kind,
398
- runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath)
843
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
844
+ locale: located.locale,
845
+ locales: located.locales
399
846
  };
400
847
  if (dataOnly) return Response.json(studioRenderInput, { headers: {
401
848
  "Cache-Control": "private, no-store",
@@ -424,19 +871,67 @@ ${renderClientEntryScript(options.studioClientEntryUrl)}
424
871
  ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
425
872
  } });
426
873
  }
427
- async function createPageDataResponse({ options, pathname, request }) {
428
- if (!options.apiUrl || !options.projectSlug) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
429
- const singleton = await createDerivedResponse(options, pathname, request, true, true);
874
+ /**
875
+ * The Canvas with nothing selected renders the page app, like a Page, so
876
+ * selecting it keeps the Canvas mounted.
877
+ */
878
+ async function createCanvasOnlyResponse({ options, pathname, located, request, dataOnly = false }) {
879
+ if (!options.apiUrl || !options.authenticationUrl || !options.projectUuid || !dataOnly && !options.renderPage) return Response.json({
880
+ message: "Camox page renderer is not configured.",
881
+ kind: "canvas",
882
+ pathname
883
+ }, { status: 500 });
884
+ const { project, clearAuthCookie } = await resolveRequestMember(request, options);
885
+ const studio = project !== void 0;
886
+ const title = "Canvas";
887
+ const input = {
888
+ project,
889
+ presentation: studio ? "studio" : "public",
890
+ apiUrl: options.apiUrl,
891
+ authenticationUrl: options.authenticationUrl,
892
+ projectUuid: options.projectUuid,
893
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
894
+ locale: located.locale,
895
+ locales: located.locales,
896
+ href: request.url,
897
+ pathname,
898
+ head: { meta: [{ title }] },
899
+ layoutIdentity: null,
900
+ loaderData: null,
901
+ dehydratedState: dehydrate(new QueryClient()),
902
+ canvasOnly: true,
903
+ draftId: studio ? getRequestDraftId(request, options) : void 0
904
+ };
905
+ const headers = {
906
+ "Cache-Control": "private, no-store",
907
+ Vary: "Cookie",
908
+ ...clearAuthCookie ? { "Set-Cookie": buildClearServerAuthCookieHeader() } : {}
909
+ };
910
+ if (dataOnly) return Response.json(input, { headers });
911
+ const html = await options.renderPage(input);
912
+ const head = renderHead({}, { title }, void 0, studio ? options.studioStylesheetUrl : void 0);
913
+ 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: {
914
+ "Content-Type": "text/html; charset=utf-8",
915
+ ...headers
916
+ } });
917
+ }
918
+ async function createPageDataResponse({ options, pathname, located, request }) {
919
+ if (!options.apiUrl || !options.projectUuid) return Response.json({ message: "Camox page loader is not configured." }, { status: 500 });
920
+ const singleton = await createDerivedResponse(options, pathname, located, request, {
921
+ dataOnly: true,
922
+ singletonOnly: true
923
+ });
430
924
  if (singleton) return singleton;
431
925
  const queryClient = new QueryClient();
432
926
  try {
433
927
  const result = await loadCamoxPageForRequest({
434
928
  apiUrl: options.apiUrl,
435
929
  authCookieHeader: getServerAuthCookieHeader(request.headers),
436
- environmentName: options.environmentName,
930
+ draftId: getRequestDraftId(request, options),
931
+ locale: located.locale,
437
932
  origin: new URL(request.url).origin,
438
- pathname,
439
- projectSlug: options.projectSlug,
933
+ pathname: located.path,
934
+ projectUuid: options.projectUuid,
440
935
  queryClient
441
936
  });
442
937
  const headers = new Headers({
@@ -446,21 +941,32 @@ async function createPageDataResponse({ options, pathname, request }) {
446
941
  });
447
942
  if (result.shouldClearAuthCookie) headers.append("Set-Cookie", buildClearServerAuthCookieHeader());
448
943
  const camoxApp = await options.getCamoxApp?.();
449
- const head = camoxApp ? buildCamoxPageHead(camoxApp, result.data) : null;
944
+ const { alternates, head } = await readAlternates(request, options, {
945
+ located,
946
+ identity: {
947
+ kind: "curated",
948
+ pageId: result.data.page.page.id
949
+ },
950
+ head: camoxApp ? buildCamoxPageHead(camoxApp, result.data, located.locale) : null,
951
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
952
+ draftId: result.draftId
953
+ });
450
954
  const dehydratedState = dehydrate(queryClient);
451
- const project = result.source === "draft" ? await createServerApiClient(options.apiUrl, options.environmentName, { authCookieHeader: getServerAuthCookieHeader(request.headers) }).projects.getBySlug({ slug: options.projectSlug }) : void 0;
452
955
  const input = {
956
+ alternates,
453
957
  apiUrl: options.apiUrl,
454
958
  authenticationUrl: options.authenticationUrl ?? "",
455
- environmentName: options.environmentName,
456
- projectSlug: options.projectSlug,
959
+ projectUuid: options.projectUuid,
457
960
  runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
458
- href: request.url,
459
- presentation: result.source === "draft" ? "studio" : "public",
460
- source: result.source,
961
+ locale: located.locale,
962
+ locales: located.locales,
963
+ href: renderedHref(request.url, result),
964
+ draftId: result.draftId,
965
+ refusedDraft: result.refusedDraft,
966
+ presentation: result.project ? "studio" : "public",
461
967
  pathname,
462
- layoutIdentity: getLayoutIdentity(result.data.page.layout, result.source),
463
- project,
968
+ layoutIdentity: getLayoutIdentity(result.data.page.layout),
969
+ project: result.project,
464
970
  loaderData: result.data,
465
971
  head,
466
972
  dehydratedState
@@ -469,7 +975,7 @@ async function createPageDataResponse({ options, pathname, request }) {
469
975
  return Response.json(input, { headers });
470
976
  } catch (error) {
471
977
  if (isNotFoundError(error)) {
472
- const derived = await createDerivedResponse(options, pathname, request, true);
978
+ const derived = await createDerivedResponse(options, pathname, located, request, { dataOnly: true });
473
979
  if (derived) return derived;
474
980
  return Response.json({
475
981
  kind: "page",
@@ -495,48 +1001,117 @@ async function handleCamoxRequest(request, options = {}) {
495
1001
  }
496
1002
  if (match.kind === "og") return createOgResponse(request, options);
497
1003
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1004
+ if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
1005
+ if (match.kind === "routes") return createRoutesResponse(request, options);
498
1006
  if (match.kind === "data") {
499
1007
  const targetPathname = normalizePagePath(new URL(request.url).searchParams.get("path"));
500
- const targetMatch = matchRuntimeRoute(targetPathname);
501
- if (targetMatch.kind.startsWith("studio")) return createStudioHtmlResponse({
502
- match: targetMatch,
503
- options,
504
- request,
505
- dataOnly: true
506
- });
507
- if (targetMatch.kind !== "page") return new Response("Not found", { status: 404 });
508
- return createPageDataResponse({
509
- options,
510
- pathname: targetPathname,
511
- request
512
- });
1008
+ if (matchRuntimeOwnedRoute(targetPathname)) return new Response("Not found", { status: 404 });
1009
+ return respondInLocale(options, request, targetPathname, { dataOnly: true });
513
1010
  }
514
- if (match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested") return createStudioHtmlResponse({
515
- match,
1011
+ if (isPreviewHandoff(request, options) && match.kind === "page") return createPreviewHandoffResponse({
516
1012
  options,
1013
+ pathname,
517
1014
  request
518
1015
  });
519
- if (match.kind === "page") {
520
- if (options.apiUrl && options.projectSlug) {
521
- const markdownResponse = await createMarkdownResponse({
522
- apiUrl: options.apiUrl,
523
- environmentName: options.environmentName,
524
- pathname: match.pathname,
525
- projectSlug: options.projectSlug,
1016
+ const { apiUrl, projectUuid } = options;
1017
+ const markdownPathname = parseMarkdownPath(pathname);
1018
+ if (apiUrl && projectUuid && (markdownPathname || prefersMarkdown(request.headers.get("Accept") ?? ""))) {
1019
+ const located = await locateRequest(request, options, markdownPathname ?? pathname, { live: true });
1020
+ if (markdownPathname || matchLocalizedRoute(located.path)?.kind === "page") {
1021
+ const markdownResponse = await createPageMarkdownResponse({
1022
+ ...options,
1023
+ apiUrl,
1024
+ projectUuid
1025
+ }, located, {
1026
+ pathname: markdownPathname ?? pathname,
526
1027
  request
527
1028
  });
528
1029
  if (markdownResponse) return markdownResponse;
1030
+ if (markdownPathname) return new Response("Page not found", { status: 404 });
529
1031
  }
530
- return createPageHtmlResponse({
531
- options,
532
- pathname: match.pathname,
533
- request
534
- });
535
1032
  }
536
- return Response.json({
537
- message: "Camox runtime route matched.",
538
- ...match
539
- }, { status: 501 });
1033
+ return respondInLocale(options, request, pathname);
1034
+ }
1035
+ /**
1036
+ * Answers a path under its locale's prefix: the Canvas, a studio route or a page, in that locale.
1037
+ * With `dataOnly`, as the JSON client navigation loads.
1038
+ */
1039
+ async function respondInLocale(options, request, pathname, { dataOnly = false } = {}) {
1040
+ const located = await locateRequest(request, options, pathname);
1041
+ const localized = matchLocalizedRoute(located.path);
1042
+ if (!localized) return new Response("Not found", { status: 404 });
1043
+ const target = {
1044
+ options,
1045
+ pathname,
1046
+ located,
1047
+ request
1048
+ };
1049
+ if (localized.kind === "canvas") return createCanvasOnlyResponse({
1050
+ ...target,
1051
+ dataOnly
1052
+ });
1053
+ if (isStudioRoute(localized)) return createStudioHtmlResponse({
1054
+ ...target,
1055
+ match: localized,
1056
+ dataOnly
1057
+ });
1058
+ return dataOnly ? createPageDataResponse(target) : createPageHtmlResponse(target);
1059
+ }
1060
+ /**
1061
+ * A page's Markdown Representation in its locale, with the same precedence as its HTML:
1062
+ * singleton, curated page, then derived layout. Null when the page has none.
1063
+ */
1064
+ async function createPageMarkdownResponse(options, { locale, path }, { pathname, request }) {
1065
+ const page = {
1066
+ pathname,
1067
+ url: absoluteUrl(request, options)(pathname)
1068
+ };
1069
+ const app = await options.getCamoxApp?.();
1070
+ const layoutMatch = app && matchDerivedLayout(app.getLayouts(), path);
1071
+ if (layoutMatch?.layout._internal.kind === "singleton") return createLayoutMarkdownResponse(options, layoutMatch, locale, page);
1072
+ return createMarkdownResponse({
1073
+ apiUrl: options.apiUrl,
1074
+ camoxApp: app,
1075
+ locale,
1076
+ pathname: path,
1077
+ projectUuid: options.projectUuid,
1078
+ request,
1079
+ url: page.url,
1080
+ onNotFound: async () => layoutMatch ? createLayoutMarkdownResponse(options, layoutMatch, locale, page) : null
1081
+ });
1082
+ }
1083
+ /** Whether a route is one of the studio's own, rather than a page or the Canvas. */
1084
+ function isStudioRoute(match) {
1085
+ return match.kind === "studio" || match.kind === "studio-content" || match.kind === "studio-nested";
1086
+ }
1087
+ /**
1088
+ * The studio or page route a locale-relative path names, or null for a runtime-owned route
1089
+ * under a locale's prefix (`/de/sitemap.xml`), which names nothing.
1090
+ */
1091
+ function matchLocalizedRoute(path) {
1092
+ const match = matchRuntimeRoute(path);
1093
+ return match.kind === "page" || match.kind === "canvas" || isStudioRoute(match) ? match : null;
1094
+ }
1095
+ /**
1096
+ * The locale a request's path names, among the locales of the scope it reads: the URL's draft's
1097
+ * for a session naming one, else Live's. `live` reads Live's whatever the request names. Without
1098
+ * an API there are none to read: every path is the default locale's, which then says it can't
1099
+ * render.
1100
+ */
1101
+ async function locateRequest(request, options, pathname, { live = false } = {}) {
1102
+ const { apiUrl, projectUuid } = options;
1103
+ const locales = apiUrl && projectUuid ? await readScopeLocales({
1104
+ apiUrl,
1105
+ projectUuid,
1106
+ ...!live && {
1107
+ authCookieHeader: getServerAuthCookieHeader(request.headers),
1108
+ draftId: getRequestDraftId(request, options)
1109
+ }
1110
+ }) : [UNNAMED_DEFAULT_LOCALE];
1111
+ return {
1112
+ ...parseLocalePath(pathname, locales),
1113
+ locales
1114
+ };
540
1115
  }
541
1116
 
542
1117
  //#endregion