canopycms 0.0.68-int.98 → 0.1.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 (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
package/CHANGELOG.md ADDED
@@ -0,0 +1,2131 @@
1
+ # Changelog
2
+
3
+ What changed in CanopyCMS, what you must do to adopt it, and **what you can now delete**. The five
4
+ packages release together, at one version.
5
+
6
+ ## This build
7
+
8
+ Version `0.1.0`, built from [`39a755c`](https://github.com/safeinsights/canopycms/tree/39a755c116a52b6d16e0b09e9dde03c67a081754).
9
+
10
+ Its entries are [0.1.0](#010), under **Released**.
11
+
12
+ If you upgraded from a build on another branch, compare the `<!-- changeset: … -->` slugs in your previous build's `CHANGELOG.md` (`npm pack canopycms@<old>` fetches it) with this one's: entries whose slug your old file lacks are new to you.
13
+
14
+ ## How to use this document
15
+
16
+ Each entry has up to three parts:
17
+
18
+ - **What changed**: the package-side change.
19
+ - **To adopt**: what you do in your repo.
20
+ - **Now deletable**: the kind of local code the change supersedes. **This part is the point.**
21
+ Adding the new API without removing what it replaces leaves two implementations to drift apart.
22
+
23
+ **Read the copy your build ships.** From the first `0.1.0` prerelease on, every published version carries this file as
24
+ `node_modules/canopycms/CHANGELOG.md`, assembled for that build: its **This build** section lists
25
+ what is new since the previous int on the same branch, or on a stable release links its entries, and its links point at the commit the build
26
+ came from. The copy in the repository holds released history and the entries from before changesets;
27
+ every entry written since is a file in [`.changeset/`](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/.changeset/README.md) until a stable release
28
+ moves it here.
29
+
30
+ **Find your starting point.** From a stable pin, start at the first version under **Released**
31
+ newer than it, and below `0.0.63` read the notes for `0.0.62 and earlier` too. From an int
32
+ prerelease such as `0.0.68-int.101`, read only the **Unreleased** entries, or labelled parts of
33
+ entries, carrying a higher int number (the table lists later parts in parentheses). Moving from
34
+ an int prerelease to a stable release, read that release's section the same way: its entries keep
35
+ their int labels.
36
+
37
+ **Find your target.** `npm view canopycms version` reports the `latest` dist-tag, the newest
38
+ stable release. On the int channel, the target's own `CHANGELOG.md` is the one to read.
39
+
40
+ **Read every window between the two, oldest first.** Deletable-code lists compound. Where a later
41
+ entry replaces an earlier one's advice, the earlier one points to it.
42
+
43
+ ---
44
+
45
+ ## Unreleased
46
+
47
+ **int** is the first int prerelease carrying an entry, and each part of the entry is labelled
48
+ `(int.N)` the same way; a number in parentheses after a change is a later int that added a part.
49
+ **Action** says who must act; **Template first** means that with `workerCode: { source: 'parameter' }`,
50
+ you deploy the stack template before CI rolls that int's bundle.
51
+
52
+ Nothing in this section is edited by hand: each entry is a `.changeset/` file, which a build
53
+ assembles here and labels with the first int that carries it, and a stable release moves the
54
+ entries under **Released**.
55
+
56
+ ---
57
+
58
+ ## Released
59
+
60
+ ### 0.1.0
61
+
62
+ <!-- changeset: legacy-unreleased -->
63
+ <!-- changeset: nice-boxes-battle -->
64
+
65
+ #### Preview URLs take one prefix, follow `trailingSlash`, and load each entry's own page — **breaking (env)**
66
+
67
+ **What changed.**
68
+
69
+ - (int.94) `editor.previewPrefix` (a path or `https://` URL) goes in front of every preview URL,
70
+ site-relative `previewBase` routes included. Preview URLs follow `trailingSlash`, and the preview
71
+ bridge matches a page with or without one. `CANOPY_API_TRAILING_SLASH` is renamed
72
+ `CANOPY_TRAILING_SLASH`.
73
+ - (int.98) The preview pane loads each entry's `urlPath`: `content/about` previews `/about`, not
74
+ `/`. A `previewBase` value of `false` marks an entry with no page, and the pane says so.
75
+
76
+ **To adopt.** Set `editor: { previewPrefix: '/preview' }` once that route exists (next entry), and
77
+ drop the prefix from any `previewBase` value that spells it. Rename a `CANOPY_API_TRAILING_SLASH`
78
+ you set yourself. Key an entry without a page `false` (`'content/settings': false`). A root entry
79
+ served at `/` without an `index` slug needs `'content/<slug>': '/'`, or re-model it as a root index
80
+ ([0.0.64](#sitemap-pathfor-and-modelling-a-page-served-at--as-a-root-index-entry)).
81
+
82
+ **Now deletable.** `previewBase` keys that only added a shared prefix or restate a root entry's own
83
+ URL.
84
+
85
+ #### The preview route and `/preview` entries — **breaking (imports)**
86
+
87
+ **What changed.**
88
+
89
+ - (int.94) The context's `createPreviewPage({ views })` serves a `[[...path]]` route at
90
+ `editor.previewPrefix`, rendering `?branch=` through `views[entryType]`, so a static-export site
91
+ can preview a branch.
92
+ - (int.95) `previewView({ view, load })` feeds a view server-read `extras`.
93
+ - (int.101) `withCanopyPreview` and `CanopyPreviewViewProps` come from `canopycms-next/preview`, and
94
+ preview hooks like `useCanopyPreview` from `canopycms/preview`. The `/client` entries are the
95
+ editor: they put Mantine's unlayered CSS over the site's styles in the preview, and ship the
96
+ editor to public pages.
97
+ - (int.103) On that route a `withCanopyPreview` view is not server-rendered. It renders right after
98
+ hydration with its `/assets/t/` URLs behind the signed-in asset route, so a crop no build made
99
+ never returns 403; server components from `load` get the same prefix. Until hydration the view's
100
+ area is empty, so content below it can shift. A view that throws fails in the browser, and its
101
+ effects run after the preview's ready message. Pages calling `useCanopyPreview` themselves are
102
+ unchanged.
103
+
104
+ **To adopt.**
105
+
106
+ 1. Import preview APIs, and point their test mocks, at the `/preview` entries. A
107
+ `vi.mock('canopycms-next/client', …)` silently stops applying.
108
+ 2. For a branch preview: wrap views with `withCanopyPreview` in a `'use client'` module, add
109
+ `app/preview/[[...path]]/page.server.tsx`, set `editor.previewPrefix: '/preview'`, and serve that
110
+ route with `frame-ancestors 'self'`, not `X-Frame-Options: DENY` (see
111
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#live-preview)).
112
+ 3. A test reading view content from the route's server HTML must load the page in a browser.
113
+ Server output rendered during a preview request carries the signed-in asset prefix; keep it out
114
+ of cross-request caches (`unstable_cache`, module memos).
115
+
116
+ **Now deletable.** A hand-built branch-preview route, and CSS layer or specificity workarounds that
117
+ kept site styles ahead of Mantine's in the preview.
118
+
119
+ #### Preview `fieldProps` is typed, with server-safe helpers — **breaking (types and schemas)**
120
+
121
+ **What changed.** (int.109) `fieldProps` checks each path against the view's content type, and
122
+ `canopycms` exports `FieldProps`, `FieldAttrs`, `fieldAttrs` and `scopeFieldProps`, callable from
123
+ server components. A string path marks the form's spelling (`'a.0.b'` marks `a[0].b`), and an empty
124
+ path marks nothing. The editor counts and logs marks naming no field. `createEntrySchemaRegistry`
125
+ refuses a field name that is empty, all digits or contains `.`, `[` or `]`. See
126
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#live-preview).
127
+
128
+ **To adopt.** Fix each path that stops compiling; the error lists valid ones. Rewrite computed paths
129
+ as literal segments, or type that component's prop as plain `FieldProps`. Pass `fieldProps` as
130
+ `undefined` on public pages. Rename refused fields and their content keys.
131
+
132
+ **Now deletable.** A local `FieldProps` type, `fieldAttrs`/`scopeFieldProps` helpers and no-op
133
+ `fieldProps` defaults; schema-walking tests for misspelled mark paths, once components are typed
134
+ (the editor flags another block template's field).
135
+
136
+ #### A reference resolves to its target, a restricted stub, or a missing stub — **security fix; breaking (types and build)**
137
+
138
+ **What changed.**
139
+
140
+ - (int.94) A target the reader may not read resolves to
141
+ `{ id, slug, collection, urlPath, title, unavailable: true, reason: 'restricted' }`
142
+ (`RestrictedReference`), never its data. Static builds are unchanged. A schema may not declare a
143
+ top-level field named `unavailable`.
144
+ - (int.106) An id naming no entry resolves to `{ id, unavailable: true, reason: 'missing' }`
145
+ (`MissingReference`), not `null`, and a production build fails on one, naming entry, field path
146
+ and id. Saving the referring entry keeps the id; a save adding a new dangling id is refused.
147
+ - (int.106) The live preview resolves references inside objects, object lists and blocks too. One
148
+ still resolving is `null`, even in an entry's first draft. `isLoading` mirrors the data at depth,
149
+ typed `PreviewLoadingState<T>`, so a reference's entry is `boolean | undefined`.
150
+
151
+ A `resolvedSchema` reference infers as `Target | UnavailableReference | null` (`RestrictedReference`
152
+ before int.106).
153
+
154
+ **To adopt.** Narrow on `unavailable`, then on `reason`; only a restricted reference has a title
155
+ and URL:
156
+
157
+ ```tsx
158
+ if (!ref) return null
159
+ if (ref.unavailable) {
160
+ return ref.reason === 'restricted' ? <a href={ref.urlPath}>{ref.title}</a> : null // 'missing'
161
+ }
162
+ ```
163
+
164
+ `isResolvedReference(ref)` (from `canopycms`) is true only for the target itself. Test
165
+ `reason === 'missing'` where you tested `null`, and write `Exclude<…, UnavailableReference>` where
166
+ you wrote `Exclude<…, RestrictedReference>`. Rename any `unavailable` field. Repoint or clear each
167
+ reference the build lists, or set `danglingReferences: 'warn'`. Make a prop fed from `isLoading`
168
+ optional if it was required.
169
+
170
+ **Now deletable.** Hand-written reference narrowing (string id, `null`, `unavailable` checks), a
171
+ content-integrity test checking reference fields against entry ids, and top-level-only reference
172
+ fields kept for the preview's sake.
173
+
174
+ #### AI content links a reference to its target — **behaviour change for `/ai` output and AI config callbacks**
175
+
176
+ **What changed.** (int.106) A `reference` field renders as a link to its target, not the stored id:
177
+ a byline reading `**Author:** 5NVkkrB1MJUv` now links the name `Alice` to that entry's page, then to
178
+ its markdown copy under the AI config's new `mountPath` (default `/ai`). A gone target renders as
179
+ `(missing entry <id>)`, one left out by `exclude` as `(unavailable entry <id>)`. An md/mdx entry's
180
+ object or block field renders as a section, not `[object Object]`.
181
+
182
+ `where` predicates and transforms receive an `AIReferenceValue` (from `canopycms/ai`), not the id:
183
+
184
+ ```ts
185
+ // before
186
+ where: (entry) => entry.data.author === ALICE_ID
187
+ // after
188
+ where: (entry) => (entry.data.author as AIReferenceValue | undefined)?.id === ALICE_ID
189
+ ```
190
+
191
+ **To adopt.** `entry.data` is untyped, so an old comparison still compiles and silently stops
192
+ matching: search your AI config for each reference field's name.
193
+
194
+ **Now deletable.** A `fieldTransforms` entry that looked up a reference id to print its title.
195
+
196
+ #### AI content leaves out `noindex` entries — **behaviour change for `/ai` output**
197
+
198
+ **What changed.** (int.114) AI content leaves out entries marked `noindex`, both from
199
+ `generate-ai-content` and from the `/ai` route, using the same predicate as the sitemap and robots
200
+ metadata. An inline body `entry:` link to an entry the export leaves out (by `exclude`, `noindex`
201
+ or a failed render) renders as its plain text, with no URL; any other form of the link points at
202
+ `#`.
203
+
204
+ **To adopt.** To keep exporting `noindex` entries, set `exclude: { noindex: false }`. If your SEO
205
+ fields are nested or renamed, pass the AI config's `seo` the same object you pass
206
+ `createNextCanopyContext({ seo })`, so the sitemap and AI content read the flag from the same
207
+ place. A field that links to a hidden section by plain URL needs a field transform; the README
208
+ shows [the recipe](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#ai-content-configuration).
209
+
210
+ **Now deletable.** An `exclude.where` that re-read the `noindex` flag by hand.
211
+
212
+ #### Deleting a referenced entry asks first — **behaviour change on the delete API**
213
+
214
+ **What changed.** (int.106) Deleting an entry that other entries reference (by field or `entry:`
215
+ link) returns 409 with `data.referencedBy`: the referencing entries the user may read, and a
216
+ `hiddenCount` of the rest. `?confirmReferenced=true` deletes anyway; the editor's dialog offers
217
+ **Delete anyway**.
218
+
219
+ **To adopt.** Only scripts calling the delete endpoint: pass the flag or handle the 409.
220
+
221
+ #### `mdx` content that runs code is refused at save — **breaking (behaviour)**
222
+
223
+ **What changed.** (int.108) An `mdx` field or body refuses `{…}` expressions other than comments and
224
+ plain values, `import`/`export`, and tags, attributes and URL schemes outside a safe set; other
225
+ markdown refuses those URLs. New: field option `executable`; type `MarkdownFieldConfig`. (int.109)
226
+ A `markdown` field with `renderAs: 'mdx'` is checked as MDX, and `mdxAllow` narrows the safe set per
227
+ field or site-wide. New: field options `renderAs`, `mdxAllow`; config key `mdxAllow`; type
228
+ `MdxAllowlist`. (int.110) A component prop allowance can be `'string'` (or
229
+ `{ type: 'string', maxLength }`): a quoted value only, never bare or `{…}`. See
230
+ [MDX content cannot run code](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#mdx-content-cannot-run-code).
231
+
232
+ **To adopt.** Set `renderAs: 'mdx'` on each `markdown` field your site compiles as MDX, and
233
+ `mdxAllow` to what your renderer takes. Set `executable: true` only on a field whose editors you
234
+ trust as code authors; an entry type with no `isBody` field needs one declared to opt its body out.
235
+ Content already there is kept, with a warning, while its field is saved unchanged; a production
236
+ build lists it.
237
+
238
+ **Now deletable.** A `validateEntry` rule refusing expressions, ESM, tags, components or
239
+ `javascript:` links in MDX or markdown rendered as MDX, and the path-prefix matching behind its
240
+ entry-type gate. Keep a rule checking that the body compiles.
241
+
242
+ #### A save rewrites only what changed
243
+
244
+ **What changed.**
245
+
246
+ - (int.101) In YAML and JSON entries and md/mdx frontmatter, untouched values, comments, blank
247
+ lines and (in `.yaml` and `.json` files) CRLF line endings stay as written, and an edited `>-`,
248
+ `|` or quoted value keeps that style where it can. Anchors, aliases and rare layouts still
249
+ re-serialise the whole file. This extends [0.0.64's comment
250
+ preservation](#editor-saves-keep-comments-and-report-content-keys-the-schema-does-not-define-29).
251
+ - (int.103) An md/mdx body save keeps every untouched block verbatim (markers, escapes, JSX, blank
252
+ lines, CRLF), so `prettier --check` passes. New and edited blocks use Prettier's markers (`-`,
253
+ `_emphasis_`, `---`); a new entry's body starts after a blank line. A hard break or a bare URL
254
+ before punctuation still changes on the first edit. A body with text after a nested list in a
255
+ list item opens as source.
256
+
257
+ **To adopt.** Nothing.
258
+
259
+ **Now deletable.** A formatter pass or `.prettierignore` entry over CMS-written content that only
260
+ undoes the editor's re-folding, restyling or date rewriting.
261
+
262
+ #### The markdown editor runs MDXEditor 4.3
263
+
264
+ **What changed.** (int.105) `@mdxeditor/editor` is `^4.3.2` (Lexical 0.48), up from `^3.52.4`.
265
+ Two-paragraph list items, multi-block quotes and code fences in any language edit in rich text.
266
+
267
+ **To adopt.** Nothing, unless your app depends on `@mdxeditor/editor` or `lexical` directly: align
268
+ those versions so one copy loads.
269
+
270
+ #### Unsaved group and permission changes show, and closing asks first
271
+
272
+ **What changed.** (int.113) Manage Groups and Manage Permissions still save as one batch, but now
273
+ mark unsaved changes, keep the save bar in view, and ask before closing or leaving drops them. The
274
+ group dialog's buttons read "Add" and "Apply", since neither saves.
275
+
276
+ **To adopt.** Nothing.
277
+
278
+ #### Field editing: descriptions, list-card titles, and `public/` images
279
+
280
+ **What changed.**
281
+
282
+ - (int.93) An `object` field with `list: true` takes `itemTitleField`, naming a direct `string` or
283
+ `number` child whose value titles each card (else `<label> #N`). Cards no longer repeat the label
284
+ as an inner legend.
285
+ - (int.96) A field's `description` renders under its label for every type; a list field shows it
286
+ once, never per item. A block template's `description` still does not render.
287
+ - (int.103) An `image` field whose `src` is outside `/assets/`, such as `/logos/x.svg`, previews at
288
+ that src and has no Crop button: a crop never applies outside the asset store.
289
+
290
+ **To adopt.** Nothing. Read your existing descriptions, which editors now see. To crop a `public/`
291
+ image, upload a raster copy to the media library. For card titles:
292
+ `{ type: 'object', list: true, itemTitleField: 'label', fields: [...] }`.
293
+
294
+ **Now deletable.** Editor hints worked into a field's `label` because `description` never showed.
295
+
296
+ #### Content reads: listings take a `branch`, and reads report `slug` and `urlPath`
297
+
298
+ **What changed.**
299
+
300
+ - (int.93) `listEntries()`/`buildContentTree()` accept `branch`, like `read()`.
301
+ - (int.102) `read()`/`readByUrlPath()` return `meta.slug` and `meta.urlPath`, and a preview `load`
302
+ gets `entry.slug` and `entry.urlPath`, each computed as `listEntries` computes them.
303
+
304
+ **To adopt.** Index pages pass `searchParams`' `branch` through, so editor previews list that
305
+ branch.
306
+
307
+ **Now deletable.** Code parsing a slug or URL back out of `path` or `entry.path`.
308
+
309
+ #### Path-permission rules now take effect below the content root — **security fix; breaking for rules written with on-disk names**
310
+
311
+ **What changed.** (int.93) Rules match an entry's logical path (`content/blog/my-post`). Enforcement
312
+ checked the id-suffixed on-disk path, so collection and entry rules matched nothing; only a glob
313
+ that also matched on-disk names, such as `content/**`, worked. Renaming also requires edit access
314
+ at the new path.
315
+
316
+ **To adopt.** Review your rules (Settings → Manage Permissions) before upgrading: collection rules
317
+ that never took effect will start to. Rewrite any rule naming an id-suffixed directory or a file
318
+ extension in logical form (`content/blog/**`, `content/about`).
319
+
320
+ **Now deletable.** A content-root grant working around inert collection grants.
321
+
322
+ #### Reads never create a requested branch — **security fix; breaking for some direct `createContentReader` callers**
323
+
324
+ **What changed.** (int.94) A branch other than the active one must exist and be readable by the
325
+ user, or `read()`/`readByUrlPath()` treat it as not found; no workspace is created per `?branch=`
326
+ value. `createContentReader` defaults `allowCreateBranch` to `false`.
327
+
328
+ **To adopt.** Nothing via `getCanopy()`. A script creating a branch through `createContentReader`
329
+ passes `allowCreateBranch: true`, never with a request's branch.
330
+
331
+ **Now deletable.** Checks that allow-list `?branch=` before it reaches `read()`.
332
+
333
+ #### Fast, crash-safe branch creation — **behaviour change: branch clones hold only the content root**
334
+
335
+ **What changed.** (int.100) A branch is built under a staging name and appears only when complete.
336
+ The worker repairs branch directories an interrupted create or delete left wedged, and owns
337
+ `remote.git`'s gc config and repacking. A content branch's clone checks out only the content root,
338
+ `.canopy-meta` and root-level files, so request-time reads elsewhere in it find nothing. The editor
339
+ recovers a create that timed out instead of reporting an HTTP 504.
340
+
341
+ **To adopt.** Upgrade `canopycms` and `canopycms-cdk` together; no config, no deploy order. A
342
+ changed `contentRoot` reaches existing clones within a worker cycle of the API restarting.
343
+
344
+ **Now deletable.** Manual EFS cleanup of provisioning locks or half-made branch directories, and
345
+ `remote.git` gc scripts.
346
+
347
+ #### `.canopy-meta/` must not be committed, and System health says so
348
+
349
+ **What changed.** (int.93) The schema cache lives in each branch clone's `.git/canopycms/`. Sync,
350
+ editor submits and `canopycms sync` ignore `.canopy-meta/`, and System health shows the base
351
+ branch's refresh outcome and warns when your repo tracks `.canopy-meta/`.
352
+
353
+ **To adopt.** If `git ls-files .canopy-meta` lists anything, run `git rm -r --cached .canopy-meta`,
354
+ add `.canopy-meta/` to `.gitignore`, and commit. The base branch's workspace follows by itself; an
355
+ editing branch whose copy of that state changed shows a rebase failure until its workspace is
356
+ repaired or re-created.
357
+
358
+ **Now deletable.** Any local step that resets or reformats `.canopy-meta/` files before a commit or
359
+ a format check.
360
+
361
+ #### Commits name their editors, and submit refuses a branch with nothing to submit
362
+
363
+ **What changed.**
364
+
365
+ - (int.93) Submit commits gain an `Edited-by: Name (id)` trailer; PR bodies gain a section that
366
+ re-submits replace, keeping human text. Options: `gitEditedByTrailers`, `gitCoAuthoredByTrailers`
367
+ ([reference](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#definecanopyconfig-options)).
368
+ - (int.110) Submit-commit trailers name every user who saved since the last submit commit, and
369
+ the PR-body section every user who edited the branch, not only the submitter; settings commits
370
+ (permissions, groups) carry the acting user's trailer.
371
+ - (int.108) Submit answers 400 when a branch's saved content matches its base. `BranchMetadata`
372
+ gains optional `submittedAt` and `pushedToGitHubAt`.
373
+
374
+ **To adopt.** Scripts calling submit: save a change first, or handle the 400.
375
+
376
+ #### Settings saves only push the settings branch — **breaking (config): `autoCreateSettingsPR` is removed**
377
+
378
+ **What changed.** (int.93) The orphan settings branch can never get a PR, so groups and permissions
379
+ saves only push the branch, and no failing PR task lands in System health.
380
+
381
+ **To adopt.** Delete `autoCreateSettingsPR` from `canopycms.config.ts`.
382
+
383
+ **Now deletable.** Filters that hid the failed settings-PR tasks.
384
+
385
+ #### A schema the running code lacks costs one entry type, and holds the worker — **behaviour change**
386
+
387
+ **What changed.** (int.107) When synced content names an entry schema the running code lacks, only
388
+ that entry type goes unavailable: the editor says so, and its entries answer 503 with
389
+ `code: 'SCHEMA_UNAVAILABLE'`. Builds, static deploys and `generate-ai-content` still fail. The
390
+ worker holds the base branch for up to 30 minutes while that lasts; see
391
+ [New schemas wait for the editor deploy](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#new-schemas-wait-for-the-editor-deploy).
392
+ Public types gain optional `unavailable` on `EntryTypeConfig` and `FlatSchemaItem`, optional
393
+ `ApiResponse.code`, and a required `SchemaResolutionResult.issues`.
394
+
395
+ **To adopt.** Code that builds a `SchemaResolutionResult` adds `issues`. A custom worker entrypoint
396
+ can pass `schemaHoldMaxMs` to `CmsWorker`.
397
+
398
+ #### `canopycms-cdk`: the worker drains before replacement and runs on-demand — **breaking (props): `spotMaxPrice` is removed; behaviour and cost change**
399
+
400
+ **What changed.** (int.108) The worker is one on-demand `t4g.nano` by default (about $3 a month,
401
+ against spot's $1–2); a spot shortage could leave no worker: on a first deploy the editor's API answers 503 "CMS worker not
402
+ ready", and later publishes wait. Spot is
403
+ opt-in: `workerCapacity: { type: 'spot' }`, a mixed-instances policy that still cannot guarantee a
404
+ worker. A terminating lifecycle hook (`canopycms-worker-drain`, heartbeat
405
+ `workerTerminationHeartbeat`, default 5 minutes) lets the old worker finish in-flight work for up
406
+ to 90 seconds and requeue the rest with no retry spent. The systemd unit gains `KillMode=mixed`,
407
+ `TimeoutStopSec=120` and exit status 75 handling; the worker role may complete its own group's hook.
408
+
409
+ **To adopt.** Replace `spotMaxPrice: '…'` with `workerCapacity: { type: 'spot', maxPrice: '…' }`,
410
+ or drop it. A hand-installed unit copies the new lines from `worker/canopy-worker.service`. The
411
+ drain applies from the deploy after this one.
412
+
413
+ **Now deletable.** Any override stripping `InstanceMarketOptions` from the worker's launch template.
414
+
415
+ #### `canopycms-cdk`: the worker instance is hardened — **an existing stack upgrades in two deploys; behaviour and cost change**
416
+
417
+ **Upgrade an existing stack in two deploys:**
418
+
419
+ 1. Deploy with `efsEnforceIamAndTls: false` on `CanopyCmsService`. The worker is replaced by one
420
+ that mounts EFS with IAM, and no file-system policy exists yet.
421
+ 2. Remove the prop and deploy again. The policy lands while every client already uses IAM and TLS.
422
+
423
+ In a single deploy, CloudFormation updates the file system's policy before it replaces the worker.
424
+ The outgoing worker still holds an anonymous mount then, and if its connection drops, its NFS calls
425
+ hang until the drain heartbeat lets termination continue. Its in-flight task is retried later. A
426
+ new stack needs no steps.
427
+
428
+ **What changed.** (int.109) Detailed in
429
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#the-worker-instance): IMDSv2 with a hop limit of 1; the
430
+ bundle is one file whose sha256 user data checks, readable as that one object; a file-system policy
431
+ refusing clients without TLS or IAM, replacing the worker's
432
+ `AmazonElasticFileSystemClientReadWriteAccess`; an encrypted gp3 root volume; a boot-time upgrade to
433
+ the latest AL2023 release; a weekly replacement (`workerMaxInstanceLifetime`); a sandboxed systemd
434
+ unit; and daily EFS backups (`efsBackup`, billed per GB-month).
435
+
436
+ **To adopt.** The two deploys above. Anything else that mounts this file system needs `tls,iam` and
437
+ `elasticfilesystem:ClientMount`. A hand-installed unit copies the sandbox lines from
438
+ `worker/canopy-worker.service`. If your account encrypts EBS by default with a customer-managed
439
+ key, grant the Auto Scaling service-linked role on it. Pass `workerMaxInstanceLifetime: null` or
440
+ `efsBackup: false` to opt out.
441
+
442
+ **Now deletable.** Any override adding `MetadataOptions`, an encrypted root volume or an EFS
443
+ `FileSystemPolicy`, or narrowing the worker role's asset-bucket grant.
444
+
445
+ #### `canopycms-cdk`: the worker needs a state directory — **deploy the template before the bundle**
446
+
447
+ **What changed.** (int.110) The worker's git keeps the GitHub credential in a private mirror
448
+ under the unit's `StateDirectory=`, and the worker does not start without it.
449
+
450
+ **To adopt.** Default `workerCode`: nothing. `workerCode: { source: 'parameter' }`: `cdk deploy` the
451
+ new template before CI rolls an int.110 or later bundle; a parameter-only change set never applies
452
+ the new unit, so the new bundle exits at start. A hand-installed unit needs
453
+ `StateDirectory=canopy-worker` under `[Service]`.
454
+
455
+ #### A poisoned `remote.git` re-clones itself
456
+
457
+ **What changed.** (int.112) A `remote.git` missing its base branch is re-cloned unless an unpushed ref would be
458
+ lost; the refusal names those refs. A tampered config still refuses and needs an operator.
459
+
460
+ **To adopt.** Nothing.
461
+
462
+ #### A failed or stopped worker says why — **behaviour change on the not-ready 503; new worker APIs**
463
+
464
+ **What changed.** (int.109) While the latest start's recorded failure stands, the prod not-ready 503
465
+ is `WORKER_FAILED`: no `Retry-After`, the failure named to admins, account ids masked.
466
+ `CmsWorker.selfStopped` settles when the worker stops itself (a lost EFS lock); the `canopycms-cdk`
467
+ entrypoint then exits 69. `recordWorkerStartupFailure` records a failure before `start()`.
468
+
469
+ **To adopt.** A hand-written entrypoint calls `recordWorkerStartupFailure` on a pre-`start()`
470
+ failure and exits non-zero on `selfStopped`, with a code outside `RestartPreventExitStatus=`.
471
+
472
+ **Now deletable.** A watchdog that restarts an idle worker process.
473
+
474
+ #### `canopycms-cdk`: CI can roll the worker with a parameter, and alarm when it stops syncing — **new, opt-in**
475
+
476
+ **What changed.** (int.109)
477
+
478
+ - `workerCode: { source: 'parameter' }` on `CanopyCmsService` selects the worker bundle by a
479
+ sha256 template parameter (its logical id is the `WorkerBundleSha256ParameterName` stack output), from a bucket the construct creates. The package ships the bundle
480
+ with its hash, as `worker/dist/index.js` and `index.js.sha256`. The default, `'asset'`, is
481
+ unchanged.
482
+ - `CanopyCmsService` alarms when the worker logs no git sync for 30 minutes. See
483
+ [Worker-down alarm](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#worker-down-alarm).
484
+
485
+ **To adopt.** Optional. For parameter-only change sets, see
486
+ [Rolling the worker from CI](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#rolling-the-worker-from-ci). For the alarm, pass
487
+ `alarmTopic`.
488
+
489
+ **Now deletable.** A manual `cdk deploy` after each canopycms bump whose only purpose is moving the
490
+ worker, and a hand-built alarm on the worker log group.
491
+
492
+ #### `canopycms-cdk`: a `t4g.nano` worker boots reliably — **behaviour change: a deploy replaces the worker**
493
+
494
+ **What changed.** (int.112) The boot's `dnf upgrade` could be OOM-killed on every retry. A 1 GiB
495
+ swap file now goes on first; an upgrade that still fails starts the worker unpatched and notifies
496
+ `alarmTopic` if set; a retried step names itself when it gives up. See
497
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#the-worker-instance).
498
+
499
+ **To adopt.** Deploy, which replaces the worker instance. The bundle needs none of this, so it is
500
+ not Template first and the worker contract stays at 1.
501
+
502
+ **Now deletable.** An instance type above `t4g.nano` chosen only to get the boot through.
503
+
504
+ #### `canopycms-cdk`: the worker's log is root-owned — **security fix**
505
+
506
+ **What changed.** (int.112) `/var/log/canopy-worker` and `worker.log` are root-owned, and the unit
507
+ drops `LogsDirectory=`. systemd opens the log as root, following symlinks, before the sandbox
508
+ applies, so a worker owning the directory could swap `worker.log` for a symlink and have its
509
+ output appended to any file on the instance. The log stays readable without `sudo`.
510
+
511
+ **To adopt.** Deploy, which replaces the worker instance. It is not Template first, and the worker
512
+ contract is unchanged. A hand-installed unit needs the same change:
513
+
514
+ 1. Delete its `LogsDirectory=` line, which chowns the directory on every start.
515
+ 2. `systemctl daemon-reload`.
516
+ 3. `chown root:root` the directory and `worker.log*`.
517
+ 4. `chmod 0755` the directory.
518
+ 5. Restart.
519
+
520
+ **Now deletable.** Nothing.
521
+
522
+ #### The worker never pushes the base or default branch — **security fix**
523
+
524
+ **What changed.** (int.112) The worker refuses to push, plainly or replacing history, to the base
525
+ branch or GitHub's default branch, whatever a task asks, and fails the task permanently.
526
+ CanopyCMS never queues such a push, but a compromised CMS Lambda can write the task queue.
527
+
528
+ (int.114) It protects what the construct's `protectedBranches` prop lists (at most 1024, no settings
529
+ branch), never deletes or opens a pull request from a protected or settings branch, and re-reads
530
+ GitHub's default branch every 10 minutes.
531
+
532
+ **To adopt.** Upgrade, and roll the worker. Protect your base branch on GitHub with a rule or
533
+ ruleset CanopyCMS's credential cannot bypass. A repository admin's classic token often can: check
534
+ its bypass list. Set `CANOPYCMS_BASE_BRANCH` (the construct's `baseBranch`) explicitly: without
535
+ it, the gateway protects only GitHub's default branch and your list; the worker still refuses the
536
+ base it detected, warning when that base is neither.
537
+
538
+ #### The worker's push logs say whether GitHub moved
539
+
540
+ **What changed.** (int.113) `Pushed … to GitHub` is logged only when the branch moved, saying
541
+ where from; an unmoved branch logs at debug level. An unchanged settings branch is not pushed.
542
+
543
+ **To adopt.** Nothing.
544
+
545
+ #### The worker hands git its GitHub credential in a per-command file — **security fix; dev behaviour change**
546
+
547
+ **What changed.** (int.113) The token leaves git URLs: each GitHub fetch and push reads it from a
548
+ `0600` file named by `GIT_CONFIG_GLOBAL`, deleted afterwards. For those commands the file replaces
549
+ your global git config and clears credential helpers, so a global `http.proxy` or `http.sslCAInfo`
550
+ stops applying and no keychain credential is tried.
551
+
552
+ **To adopt.** Nothing in production. In dev, move a global-config proxy or CA to `HTTPS_PROXY` or
553
+ `GIT_SSL_CAINFO`.
554
+
555
+ #### The worker moves objects between GitHub and `remote.git` as bundles
556
+
557
+ **What changed.** (int.114) The worker moves objects between GitHub and
558
+ `remote.git` as git bundles. A new instance's first sync fetches the whole repository from GitHub,
559
+ and operations may reuse GitHub fetches under 30 seconds old.
560
+
561
+ **To adopt.** Nothing. Seeding `remote.git` stages a copy of the repository beside the gateway's
562
+ mirror, so the root volume needs room for two
563
+ ([details](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#the-worker-instance)).
564
+
565
+ #### The worker follows a rebased or deleted GitHub branch
566
+
567
+ **What changed.** (int.114) When a branch is rebased and force-pushed on GitHub, or deleted there, the
568
+ worker now updates or deletes its own copy of it instead of warning about it on every sync. A rebased
569
+ branch's copy that holds editor work is left as it is: the worker warns once, and System
570
+ health lists it with what to do.
571
+
572
+ **To adopt.** Nothing.
573
+
574
+ #### `canopycms-cdk`: a worker bundle states the template it needs — **template first for the gate only**
575
+
576
+ **What changed.** (int.112) The worker unit carries a contract version
577
+ (`Environment=CANOPYCMS_WORKER_CONTRACT=<n>`). Parameter mode outputs it as `WorkerContract`, and
578
+ the package ships the bundle's need as `worker/dist/index.js.contract`. A bundle refuses to start
579
+ under an older unit, logging `template too old for this bundle`; an unstamped unit with
580
+ `StateDirectory=` (int.110 and later) counts as contract 1, so it runs this bundle. A new bundle
581
+ changes the template too (the fallback bundle's key and hash), so only the contract says whether a
582
+ bundle-only roll is safe.
583
+
584
+ **To adopt.** Parameter mode: gate bundle-only rolls on the contract
585
+ ([recipe](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#rolling-the-worker-from-ci)). The gate refuses a stack without the
586
+ `WorkerContract` output, so `cdk deploy` the template once, which adds it. A hand-installed unit
587
+ copies its `Environment=CANOPYCMS_WORKER_CONTRACT=` line from `worker/canopy-worker.service`.
588
+
589
+ **Now deletable.** Deciding from the template diff whether a bundle-only roll is safe.
590
+
591
+ #### `canopycms-cdk`: the worker holds no secret — **security fix; template first (contract 2)**
592
+
593
+ **What changed.** (int.114) The worker runs with no network and no secret. `canopy-github`, a gateway
594
+ service under its own user with no view of EFS, holds the GitHub credential and the Clerk key, and
595
+ is the only CanopyCMS process that can reach the instance's AWS role. Core dumps are off. User data picks the
596
+ units from the bundle's contract: 2 gets the split, an older bundle the single unit.
597
+
598
+ **To adopt.** Default `workerCode`: one `cdk deploy` does both. Parameter mode: `cdk deploy` the
599
+ template, then roll the bundle; on an older template it stops with `template too old for this
600
+ bundle`. Inspect EFS only through `sudo /opt/canopy-worker/canopy-inspect <command>`, never from a root shell. A hand install
601
+ takes `worker/`'s three units and `gateway.env.example`
602
+ ([steps](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#installing-the-units-by-hand)). After the first deploy, run the
603
+ [checklist](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#first-deploy-checklist).
604
+
605
+ #### `canopycms-cdk`: `CanopyCmsService.attachTo`, and editor response headers — **behaviour change if you frame the CMS**
606
+
607
+ **What changed.** (int.94) `cmsService.attachTo(distribution, { viewerRequestFunction?, behaviorOverrides? })`
608
+ adds the editor's behaviors (`/edit`, `/edit/*`, `/api/canopycms/*`) to a distribution you already
609
+ own: OAC, the Lambda's timeout as the origin-read timeout, no caching, `x-forwarded-host`, and a
610
+ response headers policy. `CanopyCmsDistribution` sends the same headers: `frame-ancestors 'self'`,
611
+ `X-Frame-Options: SAMEORIGIN`, `nosniff`, HSTS and `X-Robots-Tag: noindex`. Unless your app sends
612
+ its own framing headers, no other origin can frame the editor or any page the CMS domain serves. Every such page is
613
+ marked `noindex`. The editor's default preview is same-origin and unaffected. See
614
+ [Serving the editor from a distribution you already own](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
615
+
616
+ **To adopt.** If you wired the Function URL into your own distribution by hand, delete those
617
+ behaviors and call `attachTo`, passing your CMS build's `assetPrefix` as `editorAssetPrefix` and a
618
+ path `editor.previewPrefix` as `previewPrefix`; synth fails while a hand-wired `/edit*`, `/api/*` or
619
+ asset-prefix behavior still sits ahead of it. Take the editor routes out of any HTTP Basic-auth
620
+ gate, and read the synth warning if your distribution has custom error responses.
621
+
622
+ **Now deletable.** Hand-wired `/edit*`, API, preview and asset-prefix behaviors, the origin, OAC and
623
+ `x-forwarded-host` function made for them, and any response-headers policy added only to stop
624
+ framing.
625
+
626
+ #### `unauthenticatedStatus`: answer signed-out API calls with 419
627
+
628
+ **To adopt.** (int.102) Set `unauthenticatedStatus: 419` if your pages sit behind HTTP Basic auth on
629
+ the editor's origin; the editor still detects sign-out. Otherwise nothing.
630
+
631
+ #### Auth plugins look users up in batches
632
+
633
+ **What changed.** (int.111) `AuthPlugin` gains an optional `getUsersMetadata(userIds)`, and the
634
+ editor batches its user-badge lookups.
635
+
636
+ **To adopt.** Nothing. A custom auth plugin may implement it; otherwise the server falls back to
637
+ bounded single lookups.
638
+
639
+ #### The CMS image builds a prod editor, and a mode mismatch blocks the editor — **behaviour change: a hand-built image can fail its build**
640
+
641
+ **What changed.** (int.111) `Dockerfile.cms` defaults `NEXT_PUBLIC_CANOPY_MODE` to `prod`, not
642
+ `dev`, and fails its build on any value but `prod` or `dev`. An editor built for the other mode than its
643
+ server runs now gets a blocking screen naming that variable, not a sign-in that never succeeds.
644
+
645
+ **To adopt.** A hand-built image running a dev-mode server passes
646
+ `--build-arg NEXT_PUBLIC_CANOPY_MODE=dev`.
647
+
648
+ #### Prod detects an unset `defaultBaseBranch` instead of assuming `main` — **behaviour change: startup can fail**
649
+
650
+ **What changed.** (int.110) Unset, prod reads the base branch from the HEAD of the workspace's `remote.git`,
651
+ which the worker points at the branch it uses. Before the worker creates `remote.git`, requests
652
+ answer the not-ready 503. A HEAD naming no branch, or a network remote, fails service creation with
653
+ an error naming `defaultBaseBranch`. `CanopyCmsService` stamps `CANOPYCMS_BASE_BRANCH` only when
654
+ `baseBranch` is set; unset, the worker uses GitHub's default branch. A stack that relied on the
655
+ implicit `'main'` behaves as before when its repository's default branch is `main`.
656
+
657
+ **To adopt.** If your base branch is not the repository's default, set `defaultBaseBranch`. Prod
658
+ does not follow a later change of GitHub's default branch, so set it before a cutover.
659
+
660
+ **Now deletable.** A `defaultBaseBranch: 'main'` in a repository whose default branch is `main`.
661
+
662
+ #### The prod workspace defaults to `/mnt/efs` — **behaviour change if you run prod without `CANOPYCMS_WORKSPACE_ROOT`**
663
+
664
+ **What changed.** (int.100) With `CANOPYCMS_WORKSPACE_ROOT` unset, prod mode keeps its branches,
665
+ settings, task queue and auth cache under `/mnt/efs` instead of `/mnt/efs/workspace`
666
+ (`DEFAULT_PROD_WORKSPACE`). `CanopyCmsService` sets the variable to `/mnt/efs` in the Lambda and
667
+ the worker, so CDK deployments are unaffected.
668
+
669
+ **To adopt.** If you run prod without the CDK and without `CANOPYCMS_WORKSPACE_ROOT`, set it to
670
+ `/mnt/efs/workspace` to keep your existing workspace, or move that directory's contents up to
671
+ `/mnt/efs`.
672
+
673
+ #### System health shows which build is running
674
+
675
+ **What changed.** (int.94) System health gains a Build section (canopycms version, source revision,
676
+ worker version, media-storage state) and warns when API and worker versions differ.
677
+
678
+ **To adopt.** Optional; without it the revision reads "not set". Copy the template changes
679
+ (`init-deploy aws` overwrites whole files): `Dockerfile.cms` (runner-stage `ARG`/`ENV`/`LABEL` for
680
+ `CANOPY_SOURCE_SHA`), `infrastructure/lib/cms-stack.ts` (`sourceRevision` prop, passed in
681
+ `buildArgs`), `infrastructure/bin/app.ts`, and `.github/workflows/deploy-cms.yml`
682
+ (`CANOPY_SOURCE_SHA: ${{ github.sha }}`).
683
+
684
+ **Now deletable.** Hand-rolled version or commit stamping, or a build-info endpoint.
685
+
686
+ #### The AWS example workflow gains the template's triggers and dependency checks
687
+
688
+ **What changed.** (int.111) Like the one `init-deploy aws` writes, `examples/aws-deployment/deploy-cms.yml`
689
+ now deploys on `next.config.*`, `middleware.ts` and `public/**` changes, and checks `canopycms` and
690
+ `aws-cdk` are installed.
691
+
692
+ **To adopt.** If you copied it by hand, add those `paths:` and packages.
693
+
694
+ #### `GET /admin/branch-health` scans for duplicate content IDs only on request — **behaviour change**
695
+
696
+ **What changed.** (int.111) The duplicate-ID scan runs only with `?duplicates=1`, under a 20 s
697
+ budget. Each healthy entry then carries `duplicateIdScan` (`none`, `found` or `unknown`), replacing
698
+ `duplicateContentIds`, and `duplicateIdScan.truncated` says whether the budget cut the scan short.
699
+
700
+ **To adopt.** Scripts reading `duplicateContentIds`: pass `duplicates=1`, read `duplicateIdScan`.
701
+
702
+ #### `canopycms-cdk`: `AssetSupport` serves images from S3 only — **breaking**
703
+
704
+ **What changed.** (int.100) `/assets/*` and `/assets/t/*` come from the bucket alone, with an
705
+ optional `replicaBucket` for 5xx failover. An unmaterialized URL is a 403; no transform Lambda or
706
+ `assets/t/` expiry exists, and `transformFunction`/`transformLogGroup`/`transformFunctionUrl` are
707
+ `undefined`. `transformRole`, `transformReservedConcurrency`, `transformOutputRetention`,
708
+ `transformLogGroupName` and `transformLogRetention` throw: drop them, or set
709
+ `lazyPublicTransforms: true`.
710
+
711
+ **To adopt.** Until your pipeline runs both steps in the next entry, set
712
+ `lazyPublicTransforms: true`. Only the Lambda's writes expire, tagged `canopy-transform=lazy`; older
713
+ ones never do. On a bucket you pass in, also pass `transformOutputRetention` and filter your expiry
714
+ rule on that tag; cross-account, grant the transform role `s3:PutObjectTagging`. Then remove the
715
+ opt-in and those props, and pass `replicaBucket` if you replicate `assets/`.
716
+
717
+ **Now deletable.** Your `assets/t/` expiry rule, before leaving lazy mode.
718
+
719
+ #### `collect-asset-refs` and `materialize-assets` store a build's images before release
720
+
721
+ **What changed.** (int.100) `canopycms collect-asset-refs <outDir>` writes the image keys a static
722
+ build references to `<outDir>/canopy-asset-refs.json`. `canopycms materialize-assets --refs <file>`
723
+ transforms and stores whichever of them the configured store lacks. (int.104) `collect-asset-refs`
724
+ reads a page's inline RSC payload whole, so a URL Next splits across inline scripts is never
725
+ truncated into a wrong key (`logo.sv`); a script naming `self.__next_f` that is not Next's own push
726
+ fails, naming the file and offset.
727
+
728
+ **To adopt.** Run `collect-asset-refs` after the static build, before any step that lists its files.
729
+ Run `materialize-assets` before the release is served, with the S3 grants the README lists. Every
730
+ `/assets/t/` URL a page can request must appear as text in the build output.
731
+
732
+ **Now deletable.** A pre-release warm-up step that requests image URLs to get them transformed.
733
+
734
+ #### The editor and its live preview load images through the signed-in asset route
735
+
736
+ **What changed.** (int.100) The editor loads asset-store images from
737
+ `GET /api/canopycms/assets/raw/…` under your `basePath`, which transforms on demand and, on S3,
738
+ answers with a short-lived redirect to a presigned S3 read. `CanopyClientConfig.assetBaseUrl` is
739
+ gone. Inside a same-origin live preview, `assetUrl` puts `/assets/t/…` URLs behind that route
740
+ instead of your `baseUrl`.
741
+
742
+ **To adopt.** Usually nothing. The `<img>` requests authenticate with the editor's session cookie,
743
+ so the editor and its preview must share an origin with `/api/canopycms`. A Content-Security-Policy
744
+ on those pages must allow `img-src` from your bucket's S3 endpoint, and a bucket policy denying
745
+ requests that bypass CloudFront blocks the presigned reads. An off-origin preview gets no route
746
+ prefix, so under `AssetSupport`'s S3-only default it shows only stored derivatives; serve it
747
+ same-origin or set `lazyPublicTransforms: true`.
748
+
749
+ **Now deletable.** Code passing `assetBaseUrl` into the editor.
750
+
751
+ #### `assetUrl` applies an image value's crop — **behaviour change for cropped images**
752
+
753
+ **What changed.** (int.100) `assetUrl`/`assetSrcSet` apply an `image` value's `crop`; `opts.crop`
754
+ overrides.
755
+
756
+ **To adopt.** Scale `<img>` `width`/`height` by `crop.w`/`crop.h`.
757
+
758
+ **Now deletable.** Copying `value.crop` into `opts.crop`.
759
+
760
+ #### `media.publicBaseUrl` is removed, and image limits are wider — **breaking (config)**
761
+
762
+ **What changed.** (int.100) Nothing read `media.publicBaseUrl`, and the strict `media` schema fails a
763
+ config that sets it. Uploads go up to 24 MP (6000×4000), up from 16.7 MP. Widths are any integer up
764
+ to 8192 everywhere but the lazy Lambda, whose allowlist gains 32, 48, 64, 96 and 128 and which has
765
+ 2048 MB. An `orig` over 10 MiB is accepted.
766
+
767
+ **To adopt.** Delete `publicBaseUrl`. Pass `baseUrl` to `assetUrl`/`assetSrcSet` to prefix public
768
+ asset URLs.
769
+
770
+ **Now deletable.** Anything that only fed `publicBaseUrl`, and rounding widths to a multiple of 160.
771
+
772
+ #### AI content can withhold or redirect a link to an entry with no page
773
+
774
+ <!-- changeset: ai-reference-link-hook -->
775
+
776
+ _First in int.115._
777
+
778
+ **What changed.** The AI export linked every exported reference target to its page URL, even
779
+ when the site gives that entry no page. A new `AIContentConfig.referenceLink(target, { body,
780
+ defaultUrl })` decides where links to an exported entry point. The decision covers both reference
781
+ fields and body `entry:` links, so the two kinds of link agree. Return a URL to link there
782
+ instead. Return `false` when the target has no page: its title then renders as plain text, with
783
+ no page link and no markdown-version link, and an inline `entry:` link becomes its text. Return
784
+ `undefined` to keep the default. `body` is the target's stored MD/MDX body, and none of it reaches the entries that link to it. The callback runs once per target per run, may be async, and never sees a missing target or
785
+ one the exclusions leave out. A throw or any other return is logged and treated as `false`.
786
+
787
+ **To adopt.** If your site's routes, sitemap or bylines decide that some entries have no page,
788
+ apply the same rule here, for example `referenceLink: (target, { body }) =>
789
+ !body?.trim() ? false : undefined` in `defineAIContentConfig` from `canopycms/ai`. See
790
+ [AI Content Configuration](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#ai-content-configuration).
791
+
792
+ **Now deletable.** A `fieldTransforms` entry that re-renders a reference field only to drop or
793
+ change its link.
794
+
795
+ #### `toPlainText` takes an entry body and keeps prose after a leading `---` — **behaviour change**
796
+
797
+ <!-- changeset: to-plain-text-body-only -->
798
+
799
+ _First in int.115._
800
+
801
+ **What changed.** `toPlainText` from `canopycms/ai` no longer parses front matter. It used to,
802
+ so a body that opened with a `---` thematic break was read as the start of a front-matter block
803
+ and everything after it was dropped: `toPlainText('---\n\nA bio.\n')` returned `''`, so a
804
+ "has prose" check judged the entry empty and a search index got no text for it. A body whose
805
+ opening `---` was followed by lines that are not valid YAML made it throw. It now returns
806
+ `'A bio.'`, and never parses YAML. Front matter in the input is read as prose.
807
+
808
+ **To adopt.** Pass the value of the entry's body field, such as `entry.data.body` when the schema
809
+ names that field `body`. The content store has already split it from its front matter. A caller
810
+ holding a whole `.md`/`.mdx` file splits it first:
811
+
812
+ ```ts
813
+ import matter from 'gray-matter'
814
+ import { toPlainText } from 'canopycms/ai'
815
+
816
+ const text = toPlainText(matter(file, {}).content)
817
+ ```
818
+
819
+ #### People without a name show "Name unavailable"
820
+
821
+ <!-- changeset: people-without-a-name-show-name-unavailable -->
822
+
823
+ _First in int.115._
824
+
825
+ **What changed.** The Clerk plugin no longer uses the Clerk user id as a user's name: a user with no full name or username has an empty name, and the editor shows "Name unavailable" wherever that person appears. Core and the editor also treat a blank name, or one equal to the user id, as no name: `/users/:userId`, `/users/batch` and the `users` lists on comments and branches return an empty `name` for that person. The users snapshot the worker writes follows the same rule.
826
+
827
+ #### Branch access lists stay with admins and reviewers — **behaviour change**
828
+
829
+ <!-- changeset: branch-access-list-withheld-from-non-privileged -->
830
+
831
+ _First in int.115._
832
+
833
+ **What changed.** Every branch response (the listing, create, the workflow actions, status and
834
+ access updates) sends a caller who is not an admin or reviewer only the `access.allowedUsers` and
835
+ `access.allowedGroups` entries naming them or their groups, and counts the rest in a new
836
+ `withheldAccess` field. A branch card's access list shows the people the viewer can name, then
837
+ "and N others", instead of a row of "Name unavailable" avatars.
838
+
839
+ #### A branch lockdown outranks the branch's access list — **security fix**
840
+
841
+ <!-- changeset: branch-lockdown-outranks-branch-acl -->
842
+
843
+ _First in int.115._
844
+
845
+ **What changed.** A branch whose `access.managerOrAdminAllowed` is `true` admits only admins and
846
+ reviewers, whatever its `allowedUsers` and `allowedGroups` list. An access list used to switch the
847
+ lockdown off, so a listed editor, or a creator who listed themselves, could read, edit, comment
848
+ on, submit and withdraw a locked branch. The branch list hides a locked branch from editors, and
849
+ the editor's Submit and Withdraw controls follow the same rule.
850
+
851
+ **To adopt.** Nothing, unless a site relied on an access list admitting editors to a locked
852
+ branch: those editors lose access. To give it back, set `managerOrAdminAllowed` to `false` in
853
+ that branch's `branch.json`, and the access list decides again.
854
+
855
+ #### The base-branch conflict notice stays until the branch merges — **behaviour change**
856
+
857
+ <!-- changeset: conflict-notice-persists-until-merge -->
858
+
859
+ _First in int.115._
860
+
861
+ **What changed.** When the CMS brings a branch up to date and keeps the branch's version of an
862
+ entry the base branch also changed, the "updated on the base branch" notice and the branch's
863
+ conflict count now stay until the branch merges, so a reviewer still sees which entries replaced a
864
+ base change. They used to clear on the next sync, about five minutes later. README's new [Drafts
865
+ and Unpublishing](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#drafts-and-unpublishing) section covers long-lived branches and
866
+ unpublishing.
867
+
868
+ #### The editor calls the content drawer "All content"
869
+
870
+ <!-- changeset: all-content-label -->
871
+
872
+ _First in int.115._
873
+
874
+ **What changed.** The first breadcrumb segment and the file menu item that opens the content drawer say "All content" instead of "All entries", since the drawer holds collections as well as entries.
875
+
876
+ **To adopt.** Nothing, unless your browser tests find either by the text "All entries".
877
+
878
+ #### The Branches drawer shows one action per branch, and comments reply in place
879
+
880
+ <!-- changeset: branches-drawer-and-comment-replies -->
881
+
882
+ _First in int.115._
883
+
884
+ **What changed.** The drawer lists the open branch first, with one action per card and the rest
885
+ in a "…" menu, in the editor's vocabulary. A thread's Reply opens a box under it.
886
+
887
+ **To adopt.** Nothing, unless your browser tests click the old drawer buttons or labels. Every
888
+ form has a "Field comments" button, so tests that find buttons by `/comment/i` may match more than
889
+ one; and the editor listens for Ctrl+Alt+M (⌘⌥M on macOS) on the whole document, taking it while a
890
+ form is open.
891
+
892
+ #### An object's Clear offers Undo, and refusals say why
893
+
894
+ <!-- changeset: clear-undo-and-clearer-refusals -->
895
+
896
+ _First in int.115._
897
+
898
+ **What changed.** An optional object's Clear raises the same Undo toast as a list item's Remove,
899
+ and Undo returns focus to the object. A Remove toast says when the item's open comment threads moved
900
+ to "Comment on this entry". A collection your path permissions refuse new entries in says "You can't
901
+ add entries here." in place of its hidden menu. The group and permission managers show a version
902
+ conflict in yellow; the API marks it with the new error code `SETTINGS_CHANGED`.
903
+
904
+ `sanitizeHref` returns its fallback for `undefined` or `null`, which a live preview's draft can
905
+ pass for a link field it doesn't hold yet, instead of throwing.
906
+
907
+ #### The collection dialog says what saves at once
908
+
909
+ <!-- changeset: collection-dialog-says-what-saves -->
910
+
911
+ _First in int.115._
912
+
913
+ **What changed.** Editing a collection, entry-type changes still save to the branch at once, and
914
+ the dialog now says so; name, label and slug save from "Save collection details". The dismiss
915
+ button reads "Discard changes", "Close" or "Cancel" to match what it does. Entry-type results show
916
+ inline instead of as toasts.
917
+
918
+ **To adopt.** Nothing, unless browser tests click the old buttons or wait for those toasts.
919
+
920
+ #### Comments and branches name their people for every editor — **behaviour change on two APIs**
921
+
922
+ <!-- changeset: comments-and-branches-name-their-people -->
923
+
924
+ _First in int.115._
925
+
926
+ **What changed.** `GET /:branch/comments` adds `users`: the id, name and avatar URL, never the
927
+ email, of everyone who wrote or resolved a thread the requester may read. `GET /branches` adds the
928
+ same for each listed branch's creator, but not its access list. Every editor now sees those names.
929
+ Your auth plugin answers through the `/users/batch` cache; a failed lookup leaves the names out.
930
+
931
+ **To adopt.** Nothing, unless you call either API yourself: read `data.users`.
932
+
933
+ #### A date picker, a form header, and a toolbar that wraps
934
+
935
+ <!-- changeset: date-picker-form-header-wrapping-toolbar -->
936
+
937
+ _First in int.115._
938
+
939
+ **What changed.** A `datetime` field is a Mantine `DateTimePicker`, still stored as ISO UTC. The
940
+ form has a sticky header with the entry's title and type, an "Outline" of its fields and rows to
941
+ jump to, and Expand or Collapse all lists. The markdown toolbar wraps in a narrow form. canopycms
942
+ now depends on `@mantine/dates` and `dayjs`, and loads `@mantine/dates/styles.css` with its other
943
+ editor styles.
944
+
945
+ **To adopt.** Nothing. Browser tests that typed into a `datetime-local` input pick a date instead.
946
+
947
+ #### Editor colours reach 4.5:1 in light and dark mode
948
+
949
+ <!-- changeset: editor-colours-reach-contrast-in-both-schemes -->
950
+
951
+ _First in int.115._
952
+
953
+ **What changed.** The editor theme draws each colour's text, light and subtle variants at 4.5:1,
954
+ uses primary shade 7 in light mode, fills primary Buttons and Badges at 4.5:1 under white text in
955
+ both schemes (`--canopy-primary-fill`), and sets placeholders,
956
+ dark-mode dimmed text, input borders (3:1), destructive and error text, and editor panels from
957
+ scheme-aware variables, so `colorScheme="dark"` leaves no light surface behind. Every Tooltip
958
+ opens on keyboard focus. Date-time fields follow the browser's 12- or 24-hour clock and read a
959
+ date-only value as local midnight; empty header menus show a focusable item saying why.
960
+
961
+ **To adopt.** Nothing, unless visual snapshots or browser tests assert the old colours, or click
962
+ the navigator's "Delete…" or "Delete collection…", now "Delete entry" and "Delete collection". A
963
+ `themeOverride` setting `primaryShade` or `components.Tooltip.defaultProps.events` replaces these
964
+ defaults.
965
+
966
+ **Now deletable.** Local overrides that darkened light buttons, badges, placeholders or input
967
+ borders for contrast.
968
+
969
+ #### Editor contrast, and blocked buttons stay focusable
970
+
971
+ <!-- changeset: editor-contrast-and-focusable-blocked-buttons -->
972
+
973
+ _First in int.115._
974
+
975
+ **What changed.** Dimmed editor text is `gray.7`, or `dark.1` in dark mode. Filled red buttons
976
+ are `red.8`; filled orange, `orange.6` with black text. A blocked Save, Submit, Withdraw, Purge or
977
+ menu item is `aria-disabled`, not `disabled`, so its reason shows on focus. The header's entry
978
+ menu says "Reload entry" and "Discard changes".
979
+
980
+ **To adopt.** A `themeOverride` setting `components.Button.vars` replaces the editor's filled red
981
+ and orange Button resolver, and one setting `components.Alert.vars` replaces the resolver that
982
+ gives a light yellow Alert's title, icon and close button the text colour.
983
+ Browser tests check `aria-disabled`; Playwright's `toBeDisabled` reads it. Tests that click the
984
+ old "Reload File" or "Discard File Draft" use the new labels.
985
+
986
+ **Now deletable.** Local overrides that darkened the same text or buttons.
987
+
988
+ #### Editor focus rings and checked controls reach 3:1 for any brand
989
+
990
+ <!-- changeset: editor-focus-ring-and-control-contrast -->
991
+
992
+ _First in int.115._
993
+
994
+ **What changed.** Focus rings, focused borders, checked controls and the active tab draw in
995
+ `--canopy-focus-ring`, the first step of your primary colour at 3:1 on the editor's surfaces, so
996
+ a dark brand such as `#1e3a8a` is lightened in dark mode and a checked control's tick turns black
997
+ where white would not show. Unchecked Checkbox, Radio and Switch borders take
998
+ `--canopy-input-border`. Selected options and dates sit on `--canopy-primary-fill`. Badges are
999
+ sentence case, coloured badges use their light variant, dark-mode dimmed text and input
1000
+ descriptions are italic, the navigator's entry badge shows the entry type's label, count badges show
1001
+ their tint instead of the primary colour, and Markdown popups have a border in dark mode.
1002
+
1003
+ **To adopt.** Nothing, unless visual snapshots or browser tests assert the old colours or badge
1004
+ text, such as the System Health panel's "corrupt metadata", now "Corrupt metadata". A
1005
+ `themeOverride` that sets `primaryShade` moves where the ring starts.
1006
+
1007
+ #### Editor requests time out instead of hanging
1008
+
1009
+ <!-- changeset: editor-requests-time-out -->
1010
+
1011
+ _First in int.115._
1012
+
1013
+ **What changed.** Every editor API request has a deadline: 75 s, 90 s for branch create, and 180
1014
+ s for submit, admin branch scans and repairs, and the proxied upload. Past it the editor says so.
1015
+ A save keeps your changes. A write with no answer, or a gateway's error page, may still have gone
1016
+ through, so the editor says that rather than inviting a blind retry, and reloads what it can. The
1017
+ content and branch-list load failures offer Retry. A comments panel or popover held open by a
1018
+ write in flight says why.
1019
+
1020
+ **To adopt.** Nothing, unless browser tests hold an API response for longer than a deadline to
1021
+ observe a busy state; keep such holds under it.
1022
+
1023
+ #### Field editing: descriptions, list-card titles, and public/ images, continued
1024
+
1025
+ <!-- changeset: list-card-titles-and-block-descriptions -->
1026
+
1027
+ _First in int.115._
1028
+
1029
+ Continues [Field editing: descriptions, list-card titles, and public/ images](#field-editing-descriptions-list-card-titles-and-public-images).
1030
+
1031
+ **What changed.** A list card with no `itemTitleField`, or whose `itemTitleField` value is blank,
1032
+ takes its title from its first non-blank `string` field, then "Item N" (or its `itemLabel`, as in
1033
+ "Team member N"). A block template's `description` now renders; see
1034
+ [List items and blocks collapse](#list-items-and-blocks-collapse-and-reorder-from-one-menu).
1035
+
1036
+ #### List items and blocks collapse, and reorder from one menu
1037
+
1038
+ <!-- changeset: list-items-and-blocks-collapse -->
1039
+
1040
+ _First in int.115._
1041
+
1042
+ **What changed.** Object-list items and block sections are collapsible rows with a drag handle
1043
+ and a "…" menu: Move up, Move down, Duplicate, Remove. Blocks are added from "Add section" or the
1044
+ "+" above any block; both menus show each template's `description`, as does an open block. A
1045
+ `list: true` object takes an optional `itemLabel` ("team member") naming its items.
1046
+
1047
+ **To adopt.** Optionally set `itemLabel`. Browser tests that clicked a row's "Remove", "Move block
1048
+ up" or the "Add block..." select use the row's `Actions for <title>` menu (`Actions for <title>, <secondary>` when the row shows
1049
+ a second value) or "Add section".
1050
+
1051
+ **Now deletable.** Template labels that repeated the template's description.
1052
+
1053
+ #### "Add entry" follows path permissions; admin panels and labels tidied
1054
+
1055
+ <!-- changeset: navigator-add-follows-path-access-and-ux-batch2-rest -->
1056
+
1057
+ _First in int.115._
1058
+
1059
+ **What changed.** The entries listing (`GET /:branch/entries`) gains `collectionAccess`, one
1060
+ `canAddEntry` per listed collection, and the navigator offers "Add entry…" only where it is true.
1061
+ The group and permission managers always name people, never show a raw id, confirm before their
1062
+ save bar's "Discard changes", and keep Delete off the built-in groups. A read-only toggle can be
1063
+ focused and announces its state. The sync-failed badge's reason opens on focus and touch. Editor
1064
+ labels such as "All entries", "Manage permissions" and "Create group" are now in sentence case.
1065
+ Discarding an entry's changes toasts only for a read-only branch's hidden draft, where the form shows
1066
+ no change.
1067
+
1068
+ #### Quick open, a navigator filter, title-first new entries, and media search
1069
+
1070
+ <!-- changeset: quick-open-navigator-filter-media-search -->
1071
+
1072
+ _First in int.115._
1073
+
1074
+ **What changed.** Ctrl+P (⌘P on macOS) opens a quick-open list of entries from anywhere in the
1075
+ editor, even inside a text field, unless another dialog is open (the Content drawer gives way to
1076
+ it), so the browser's print shortcut never prints while the editor
1077
+ is open. The navigator filters by title or slug. "Add entry" asks for the title first, when the
1078
+ entry type has a title field, and derives the slug from it until the slug is edited. The media
1079
+ library searches, sorts and filters all its files, in a grid or a list.
1080
+
1081
+ **To adopt.** Nothing, unless browser tests add an entry by typing only a slug: fill in "Title"
1082
+ too when the entry type has one.
1083
+
1084
+ #### A save never brings back a deleted or moved entry — **behaviour change**
1085
+
1086
+ <!-- changeset: save-never-recreates-deleted-entry -->
1087
+
1088
+ _First in int.115._
1089
+
1090
+ **What changed.** A save of an entry that was deleted or moved since it was opened is now refused
1091
+ with a 409 (code `ENTRY_GONE`) instead of quietly re-creating it, which undid the other editor's
1092
+ delete or left a second copy beside a renamed entry. For a renamed entry the editor opens it under
1093
+ its new name with the changes still in the form, ready to save again. For a deleted one it keeps
1094
+ the changes and offers "Save as new entry", a deliberate create. This also covers an editor's own
1095
+ delete or rename that got no answer. Creating an entry is unchanged.
1096
+
1097
+ #### Submit names unsaved entries; unanswered writes recover more
1098
+
1099
+ <!-- changeset: submit-warns-unsaved-and-unanswered-writes-recover -->
1100
+
1101
+ _First in int.115._
1102
+
1103
+ **What changed.** Submitting a branch with unsaved edits names the entries the submit leaves
1104
+ out, and its confirm reads "Submit anyway". After a write the server never answered, the editor
1105
+ re-reads what it touched: a comment the reload shows counts as posted, otherwise it is a yellow
1106
+ warning; a rename the re-list shows under its new name keeps that entry selected, otherwise the
1107
+ rename dialog warns in yellow; and a collection create or details save fills its dialog in again
1108
+ on the next open, unless a fresh read then shows it landed (a create only once a read rules it
1109
+ out). The save-outcome toasts name their
1110
+ entry. A lookup sent as a POST that times out says to try again. Removing a staged entry type
1111
+ from a new collection goes at once, with Undo in the dialog.
1112
+
1113
+ #### Unlabelled fields and block templates show their names as words
1114
+
1115
+ <!-- changeset: unlabelled-fields-read-as-words -->
1116
+
1117
+ _First in int.115._
1118
+
1119
+ **What changed.** A field, group or block template with no `label` shows its name in sentence case,
1120
+ `metaText` as "Meta text", in its form label, the outline, comment and validation breadcrumbs, the
1121
+ block picker and the Remove toast.
1122
+
1123
+ **To adopt.** Nothing, unless your browser tests find such a field by its raw name. Give it a
1124
+ `label` to choose the words yourself.
1125
+
1126
+ #### Stable releases follow the changes they carry, and ship their changelog
1127
+
1128
+ <!-- changeset: stable-releases-from-changesets -->
1129
+
1130
+ _First in int.115._
1131
+
1132
+ **What changed.** A stable version is now chosen by what it carries: a minor when an entry asks
1133
+ adopters to act, a patch otherwise. The first one is `0.1.0`. Each stable build ships this
1134
+ changelog as `node_modules/canopycms/CHANGELOG.md`, with its links pinned to the release commit,
1135
+ as int builds already do.
1136
+
1137
+ **To adopt.** Nothing. With `0.1.0` and later, a `^0.1.0` range takes patches without action.
1138
+
1139
+ #### List items and blocks carry a `_key`, and field comments follow it — **behaviour change**
1140
+
1141
+ <!-- changeset: list-items-and-blocks-carry-keys -->
1142
+
1143
+ _First in int.115._
1144
+
1145
+ **What changed.** Every object-list item and block now has a `_key`, a short string that names it
1146
+ among its list's items. A move or an edit keeps it; Add, insert and Duplicate get new ones. The
1147
+ editor writes keys into the file on the next save, so the first save of an entry adds a `_key`
1148
+ line to each of its list items and blocks. Until then, reads give an item without one a key made
1149
+ from its content. A comment on a field inside a list item or block now follows that item. A
1150
+ comment whose item was removed stays on the entry, under "Comment on this entry", marked "On a
1151
+ removed item".
1152
+
1153
+ Reads return `_key`, and so does the data a `validateEntry` hook is given: `TypeFromEntrySchema`
1154
+ types it as an optional `_key?: string` on list items and blocks. A block keeps it beside
1155
+ `template`, never inside `value`. The unknown-field warnings never report it.
1156
+
1157
+ **To adopt.** Rename any schema field named `_key`: the config now refuses it. If your code,
1158
+ a `validateEntry` hook included, checks content against a strict schema, allow `_key` on list
1159
+ items and blocks. Don't spread a list item onto a DOM element (`<li {...item}>`), or it renders
1160
+ a `_key` attribute. `_key` also suits React's `key` when rendering a list.
1161
+
1162
+ #### `stampOnPublish` dates a new entry at its first submit
1163
+
1164
+ <!-- changeset: datetime-stamp-on-publish -->
1165
+
1166
+ _First in int.115._
1167
+
1168
+ **What changed.** A top-level `datetime` field can set `stampOnPublish: true`. When Canopy first
1169
+ submits an entry the base branch does not have, it fills that field, if empty, with the submit's
1170
+ time, in the commit it pushes. A set value is never overwritten, and an entry already on the base
1171
+ branch, even renamed or moved, or merged without a Canopy submit, is never stamped. The field's description
1172
+ states the rule, the submit toast counts the entries dated, and after a submit that succeeds the
1173
+ editor carries an unsaved draft of a stamped entry onto its stamped version.
1174
+ The flag is refused off a top-level `datetime`, on `list: true`, and with `required: true`.
1175
+
1176
+ **To adopt.** Add `stampOnPublish: true` to the date field your site sorts or labels by, such as
1177
+ `{ name: 'publishedAt', type: 'datetime', stampOnPublish: true }`. See [Stamping a publication date](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#stamping-a-publication-date).
1178
+
1179
+ **Now deletable.** A build step or hook that writes a missing publication date into new entries.
1180
+
1181
+ #### A left activity rail and a docked content panel
1182
+
1183
+ <!-- changeset: editor-activity-rail -->
1184
+
1185
+ _First in 0.1.0._
1186
+
1187
+ **What changed.** A rail down the editor's left edge opens the content panel, quick open, the
1188
+ Branches and Comments drawers and the media library, with Settings and the account control at its
1189
+ foot. Comments open from the rail only, with the unresolved count on the button, and the media
1190
+ library has its own button instead of a Settings menu item. In a wide side-by-side window the
1191
+ content panel docks beside the preview and stays open as you move between entries. Its width and
1192
+ whether it is open survive a reload. Narrower than 1024px, or stacked, it lays over the form
1193
+ only, so the preview stays in view. Below 1024px the editor also starts stacked until you choose
1194
+ a layout. Notifications moved to the bottom-right corner, opposite the rail.
1195
+
1196
+ #### `editor.subtitle` removed; the tab names the open entry — **breaking (`editor.subtitle` removed)**
1197
+
1198
+ <!-- changeset: editor-subtitle-removed-tab-title -->
1199
+
1200
+ _First in 0.1.0._
1201
+
1202
+ **What changed.** `editor.subtitle` is no longer a config option, and `Editor` no longer takes
1203
+ `subtitle` or `siteSubtitle`: nothing has shown them since the top bar replaced the old header.
1204
+ While the editor is mounted, the browser tab reads "Entry label · Site title", or the site title
1205
+ with no entry open, and your page's own title comes back when it unmounts, unless a page navigated to has set its own.
1206
+
1207
+ **To adopt.** Delete `editor.subtitle` from your config; it was no longer shown.
1208
+
1209
+ #### The editor takes over the window — **breaking (`EditorPanes` export removed)**
1210
+
1211
+ <!-- changeset: editor-takes-over-the-window -->
1212
+
1213
+ _First in 0.1.0._
1214
+
1215
+ **What changed.** The editor renders as one full-window layer above your page, so a header or
1216
+ footer your root layout renders no longer pushes it down or makes the page scroll. While it is
1217
+ mounted, its sign-in and loading screens included, your layout's elements are hidden and out of
1218
+ the Tab order, and the page does not scroll; unmount restores them. The footer is gone: the version and an "About CanopyCMS" link are
1219
+ in the Settings menu. The two read-only banners sit in their own row under the header. The
1220
+ preview/form divider is now keyboard-resizable within the same 15–85% the split was always
1221
+ stored at, and `react-split-pane` is no longer a dependency.
1222
+ `EditorPanes`, `EditorPanesProps`, the `PaneLayout` type and the `SPLIT_PERCENT_*` and `DEFAULT_*_SPLIT_PERCENT` constants are
1223
+ no longer exported from `canopycms/client`. See
1224
+ [Your root layout](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#editor-customization).
1225
+
1226
+ **To adopt.** Nothing, unless you import `EditorPanes` or those constants: render `CanopyEditor`
1227
+ instead, which lays out its own panes. Hiding your site chrome on the editor route is now
1228
+ optional, but keeping analytics and global fetches out of it is still worthwhile.
1229
+
1230
+ **Now deletable.** A wrapper that drops the site header and footer on the editor route only to
1231
+ stop them shifting the editor.
1232
+
1233
+ #### The editor's top bar, with Save all — **behaviour change**
1234
+
1235
+ <!-- changeset: editor-top-bar-and-save-all -->
1236
+
1237
+ _First in 0.1.0._
1238
+
1239
+ **What changed.** The editor's header is a 48px top bar. Its breadcrumb's crumbs navigate: "All
1240
+ content" opens the content panel, and a collection crumb opens it on that collection. The open
1241
+ entry's crumb holds the entry menu, whose "Reload entry" is now "Refresh from server". A
1242
+ "Read-only" chip says why the entry or branch takes no edits. The branch pill shows the branch,
1243
+ its status and how many entries are unsaved, and its menu lists them; its "Reload all entries" is
1244
+ now "Refresh branch" and re-reads the entry list too. Beside Save, a caret offers "Save all", which
1245
+ saves every unsaved entry on the branch and names any it could not save, with the reason, keeping
1246
+ their changes. The site title and `editor.subtitle` no longer appear in the header; the rail's
1247
+ monogram carries the site title. Below 62em the breadcrumb takes its own row and the actions wrap, so no control is clipped. A content save refused because the
1248
+ branch is mid-rebase now carries `code: 'BRANCH_SYNCING'`.
1249
+
1250
+ **To adopt.** Nothing, unless your browser tests find the header by its old text: "Reload entry",
1251
+ "Reload all entries", "Recently modified", "No entry selected", or the file menu's "All content"
1252
+ item, which the breadcrumb's "All content" replaces.
1253
+
1254
+ #### Rename and delete from the entry menu; Save says "Saved" itself — **behaviour change**
1255
+
1256
+ <!-- changeset: entry-menu-rename-delete-inline-saved -->
1257
+
1258
+ _First in 0.1.0._
1259
+
1260
+ **What changed.** The open entry's menu, on its name in the top bar, offers "Rename…" and
1261
+ "Delete entry", through the same dialogs as the content panel's. On a locked branch both stay in
1262
+ the menu, blocked, with the reason; for an entry a path rule refuses they are hidden, as in the
1263
+ content panel. A Save reads "Saving…" on the button, then "Saved" for two seconds, announced to
1264
+ screen readers, instead of a green "Saved" toast; on a phone the button shows a spinner, then a
1265
+ check. Failures still show their toast or alert, and Save all its result. Save is the top bar's
1266
+ one filled button; "Submit for review" and "Withdraw from review" are tinted. From either menu,
1267
+ deleting the open entry leaves no entry open instead of opening the first, and renaming it keeps
1268
+ it open under its new name. The delete dialog names the entry.
1269
+
1270
+ **To adopt.** Nothing, unless your browser tests wait for a "Saved" toast after Save: wait for the
1271
+ save request's response, or for the Save button to read "Saved".
1272
+
1273
+ #### The preview has a toolbar with device widths, reload and the saved version
1274
+
1275
+ <!-- changeset: preview-toolbar -->
1276
+
1277
+ _First in 0.1.0._
1278
+
1279
+ **What changed.** A toolbar above the editor's preview shows the page's path on your site
1280
+ (without `editor.previewPrefix` or `?branch=`), which copies on click. Its width selector
1281
+ renders the page at Fit, Desktop (1280 px), Tablet (768 px) or Mobile (390 px): the iframe takes
1282
+ exactly that CSS width, so your site's own breakpoints apply, scaled down when the pane is
1283
+ narrower. The choice persists per browser. Reload reloads the preview, the same action as the
1284
+ "Live updates off" Retry. "Open saved version" opens the page's `?branch=` URL in a new tab,
1285
+ which shows the branch's last saved state, without unsaved edits. A preview error now sits
1286
+ between the toolbar and the page instead of over it, naming the field by its labels. A custom
1287
+ `renderPreview` gets no toolbar. `PreviewFrame` takes two new optional props, `iframeStyle` and
1288
+ `onReload`.
1289
+
1290
+ #### The preview toolbar holds the highlight toggle and maximizes the preview; the layout toggle moves to the rail
1291
+
1292
+ <!-- changeset: preview-toolbar-controls -->
1293
+
1294
+ _First in 0.1.0._
1295
+
1296
+ **What changed.** The highlight toggle moves from the strip at the editor's right edge into the
1297
+ preview toolbar, and the layout toggle into the rail, above Settings, so it works with any
1298
+ preview or none; the strip is gone. The layout button shows the current layout and switches to
1299
+ the other; while the preview is maximized it says to restore the preview first. Layout, preview
1300
+ width and highlights persist per browser. Maximize lays the preview over the preview and form
1301
+ area, without reloading it, and drafts keep reaching it. Restore brings the form back, and so
1302
+ does Esc unless focus is inside the page. Anything that needs the form restores it too: a click
1303
+ on a highlighted element, which scrolls to its field, opening the content panel over the form,
1304
+ or a save refused for field errors. Dragging the divider between preview and form shows the
1305
+ preview's width over it, and resizing the panes, by drag or key, switches the width to Fit.
1306
+ Admins with highlights on see an "N unmapped" chip when the page marks fields that don't exist
1307
+ in the entry. It explains what that means and points to the browser console, which names the
1308
+ nearest field of each. Editors see no such diagnostics. A custom `renderPreview`, and an entry
1309
+ with no preview page, get no toolbar.
1310
+
1311
+ #### Linear-time mount paths and push refusals, and a fixed message for non-Error gateway failures — **security fix**
1312
+
1313
+ <!-- changeset: codeql-release-alerts -->
1314
+
1315
+ _First in 0.1.0._
1316
+
1317
+ **What changed.** The AI export trims slashes from `AIContentConfig.mountPath`, and the worker
1318
+ and its GitHub gateway recognise GitHub's refusal of a workflow push, in time linear in their
1319
+ length; a long run of slashes or a repeated phrase took seconds before. When the gateway fails with
1320
+ a thrown value that is not an `Error`, the worker gets a fixed message and the gateway's log gets
1321
+ the value, scrubbed and capped, since it could hold a stack.
1322
+
1323
+ ### 0.0.67
1324
+
1325
+ #### Clerk sign-in works behind CloudFront OAC
1326
+
1327
+ **What changed.** `@clerk/nextjs`'s provider makes every `setActive` (sign-in, account or org
1328
+ switch) wait on a Server Action, whose POST 403s behind `CanopyCmsDistribution`'s OAC, so the wait
1329
+ never ended. `canopycms-auth-clerk` skips it while the editor is mounted, and exports
1330
+ `useSkipClerkSetActiveAction()` for Clerk components elsewhere in the CMS build.
1331
+
1332
+ **To adopt.** Nothing for the editor, if your edit page uses `useClerkAuthConfig()` or
1333
+ `ClerkSignIn`. If your CMS build renders `<SignIn>`, `UserButton` or `OrganizationSwitcher` on your
1334
+ own pages, mount the hook as in
1335
+ [Clerk components on your own pages](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/packages/canopycms-auth-clerk/README.md#clerk-components-on-your-own-pages).
1336
+ Your own Server Actions in the CMS build still 403 behind OAC.
1337
+
1338
+ **Now deletable.** Any local patch of `window.__internal_onBeforeSetActive` (or 6.x's
1339
+ `__unstable__onBeforeSetActive`), and any edge function added only to make Clerk sign-in finish.
1340
+
1341
+ #### The editor handles signed-out users itself, so `clerkMiddleware` is optional
1342
+
1343
+ **What changed.** The editor treats a 401 from the CMS API as signed out and shows the auth
1344
+ provider's sign-in screen, over the open editor when a session ends mid-edit, so unsaved edits
1345
+ survive signing back in. `useClerkAuthConfig()` and `useDevAuthConfig()` supply that screen. The
1346
+ API verifies tokens from `CLERK_JWT_KEY` alone. `init --auth clerk` writes the same passthrough
1347
+ `middleware.ts` as dev auth, with `clerkMiddleware` as a commented opt-in.
1348
+
1349
+ **To adopt.** Nothing, if your edit page passes one of those hooks into `config.client(...)`. A
1350
+ custom auth provider adds `editor.SignInComponent`, which receives `EditorSignInProps`. `init` keeps
1351
+ an existing `middleware.ts` unless you confirm or pass `--force`; either replaces an active
1352
+ `clerkMiddleware`, so re-apply the commented snippet if you want the edge check.
1353
+
1354
+ **Now deletable.**
1355
+
1356
+ - A signed-out gate around the editor page, such as `<SignedOut>` wrappers or a `useAuth()` check
1357
+ rendering `<RedirectToSignIn />`, and reload-on-401 or "session expired" handling.
1358
+ - A `clerkMiddleware` kept only to send signed-out visitors to sign-in, or written by an earlier
1359
+ `init` if you never chose the edge check, and with it `CLERK_SECRET_KEY` in the deployed CMS runtime.
1360
+
1361
+ #### A worker credential can be one field of a JSON secret
1362
+
1363
+ **What changed.** The EC2 worker can read one field of a Secrets Manager secret whose value is a
1364
+ JSON document. Unset, the secret's whole string is the credential, byte for byte; a secret that
1365
+ parses as a JSON object with no field configured logs a warning. With a field set, every miss fails
1366
+ fast naming the ARN, the field and the keys present, never a value.
1367
+
1368
+ | `CanopyCmsService` prop | Worker env var | Names a field in |
1369
+ | ------------------------------- | ------------------------------------------ | ------------------------- |
1370
+ | `githubTokenSecretJsonField` | `CANOPYCMS_GITHUB_TOKEN_SECRET_JSON_FIELD` | `githubTokenSecretArn` |
1371
+ | `clerkSecretKeySecretJsonField` | `CLERK_SECRET_KEY_SECRET_JSON_FIELD` | `clerkSecretKeySecretArn` |
1372
+
1373
+ **To adopt.** Nothing, unless one of those secrets holds a JSON document: then set the matching
1374
+ prop. The scaffolded stack fills them from the repository _variables_
1375
+ `CANOPY_GITHUB_TOKEN_SECRET_JSON_FIELD` and `CLERK_SECRET_KEY_SECRET_JSON_FIELD`
1376
+ ([why the prefix](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#repository-secrets-and-variables)); a stack scaffolded earlier
1377
+ adds the props to `infrastructure/bin/app.ts` and `infrastructure/lib/cms-stack.ts` and the two `env`
1378
+ lines to `.github/workflows/deploy-cms.yml`, or re-runs the generator and diffs. `cdk synth` refuses a `…JsonField` prop without its `…SecretArn`, and an ARN carrying the ECS `:KEY::` suffix
1379
+ (also in `secretsArns`). Don't use CDK's `secretValueFromJson`: it resolves the plaintext into the deployed
1380
+ resource's configuration, readable by anyone who can describe it. Only `CLERK_SECRET_KEY` goes through Secrets Manager; `CLERK_JWT_KEY` and the publishable
1381
+ key are public material ([Security Model](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#security-model)).
1382
+
1383
+ **Now deletable.** A wrapper that fetches the secret, parses it and re-exports one field into the
1384
+ worker's environment, such as a `jq` step in user data or a wrapper entrypoint around
1385
+ `canopy-worker`.
1386
+
1387
+ #### The worker can authenticate to GitHub as an App
1388
+
1389
+ **What changed.** The worker can act as a GitHub App installation instead of using a token.
1390
+ `githubToken` is not deprecated and stays the documented default; `canopycms` neither depends on
1391
+ nor imports `@octokit/auth-app`. Register one App per site, not one shared across repositories:
1392
+ anyone holding an App's key can mint a token for any of its installations
1393
+ ([why](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/ARCHITECTURE.md#why-one-github-app-per-site-not-one-shared-across-an-organisation)).
1394
+
1395
+ **To adopt with `canopycms-cdk`** (existing stacks need no edit). `CanopyCmsServiceProps` takes
1396
+ `githubAppId`, `githubAppInstallationId`, `githubAppPrivateKeySecretArn` and
1397
+ `githubAppPrivateKeySecretJsonField`, and the worker entrypoint builds the credential:
1398
+
1399
+ 1. Register the App under your organisation, install it on the content repository with
1400
+ **Contents: read & write** and **Pull requests: read & write**, and store its PEM private key in
1401
+ Secrets Manager. `canopycms init-github-app create -- <command>` does this from a manifest: it
1402
+ pipes the key to your command's standard input (or `--key-out <path>` writes a `0600` file), and
1403
+ `verify` checks an existing installation's permissions: run it, since an App missing a
1404
+ permission only shows up later, as `sync-failed`. `create` needs an interactive terminal and
1405
+ never edits an existing JSON secret (create one and point the JSON-field prop at it)
1406
+ ([details](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#register-it-with-canopycms-init-github-app)).
1407
+ 2. Set the three props and **remove `githubTokenSecretArn`** (with its JSON field). A partial set
1408
+ of the three is refused at synth, and so is an App alongside a token.
1409
+ 3. In the generated workflow, store them as `CANOPY_GITHUB_APP_ID`,
1410
+ `CANOPY_GITHUB_APP_INSTALLATION_ID` and `CANOPY_GITHUB_APP_PRIVATE_KEY_SECRET_ARN`: GitHub refuses
1411
+ an Actions secret or variable whose name starts with `GITHUB_`. The workflow maps each onto the
1412
+ unprefixed variable the CDK app reads. A workflow, `app.ts` or `cms-stack.ts` scaffolded before
1413
+ 0.0.67 has none of this wiring: re-run `init-deploy aws` and diff.
1414
+
1415
+ The private key is ARN-only; passing the key itself is refused at synth. Its ARN joins the worker's
1416
+ IAM policy automatically, so you need not add it to `secretsArns`. See
1417
+ [Authenticating as a GitHub App](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#authenticating-as-a-github-app).
1418
+
1419
+ ```bash
1420
+ canopycms init-github-app create -- \
1421
+ aws secretsmanager create-secret --name canopycms/github-app-key --secret-string file:///dev/stdin
1422
+ ```
1423
+
1424
+ **To adopt from your own `CmsWorker` entrypoint.** Supply `githubAppAuth` _instead of_
1425
+ `githubToken`; both is rejected.
1426
+
1427
+ ```ts
1428
+ import { createAppAuth } from '@octokit/auth-app' // YOUR dependency, not canopycms's
1429
+ import { CmsWorker, normalizeGitHubAppPrivateKey } from 'canopycms/worker/cms-worker'
1430
+
1431
+ // ONE instance: it holds the installation-token cache, so sharing it keeps the REST
1432
+ // and git halves on the same hourly token.
1433
+ const appAuth = createAppAuth({
1434
+ appId,
1435
+ installationId,
1436
+ privateKey: normalizeGitHubAppPrivateKey(rawPrivateKey),
1437
+ })
1438
+
1439
+ new CmsWorker({
1440
+ ...rest,
1441
+ githubAppAuth: {
1442
+ mintInstallationToken: async () => (await appAuth({ type: 'installation' })).token,
1443
+ // A closure, not `authStrategy: createAppAuth` — that would have Octokit build a
1444
+ // SECOND instance with its own separate cache.
1445
+ octokitAuth: { authStrategy: () => appAuth, auth: {} },
1446
+ },
1447
+ })
1448
+ ```
1449
+
1450
+ `normalizeGitHubAppPrivateKey` repairs `\n` escapes and base64-wrapped PEMs, converts PKCS#1 to
1451
+ PKCS#8, and throws where the key is configured. The worker's git remote and REST client target
1452
+ github.com. `GitHubService` stays static-token-only.
1453
+
1454
+ **Now deletable.** A hand-written entrypoint that existed only to get App auth onto a CDK
1455
+ deployment; user-data steps that fetched the PEM into the worker's environment; your own PEM
1456
+ conversion; code that mints a token at boot and holds it (installation tokens last about an hour);
1457
+ a wrapper that catches and re-throws a mint failure, which drops the HTTP status the task
1458
+ classifier reads, so a bad key burns every publish's retry budget; and a runbook step that
1459
+ downloads the `.pem` from the App's settings page.
1460
+
1461
+ #### A rotated secret reaches the running worker, without an instance replacement
1462
+
1463
+ **What changed.** The worker re-reads a Secrets Manager secret (the GitHub token or the Clerk secret
1464
+ key) when the operation using it fails. Timings, costs and the store-before-revoke order are in
1465
+ [Rotating a secret](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#rotating-a-secret).
1466
+
1467
+ **To adopt.** Nothing for credentials from `*_SECRET_ARN`, which is every scaffolded deployment. A
1468
+ GitHub App private key is still read only at boot: store the new key, replace the instance, and
1469
+ only then delete the old key on GitHub. A plain env var (`CANOPYCMS_GITHUB_TOKEN`,
1470
+ `CLERK_SECRET_KEY`) is never re-read.
1471
+
1472
+ A custom `CmsWorker` entrypoint can pass `refreshGitHubToken?: () => Promise<string | undefined>`,
1473
+ returning the new token or `undefined` for nothing to do. Core calls it after a failed git sync or
1474
+ task, at most once per `refreshGitHubTokenMinIntervalMs` (default `60000`; `0` disables it), and
1475
+ abandons a call still unsettled after `taskTimeoutMs`. Clerk's cache refreshes through
1476
+ `refreshAuthCache`. `packages/canopycms-cdk/worker/credential-refresh.ts` is the worked example.
1477
+
1478
+ **Now deletable.** Automation that runs `cdk deploy` or an instance refresh after rotating a secret
1479
+ only to pick it up, unless it is for a GitHub App private key.
1480
+
1481
+ #### `assetUploadBehavior()` builds the upload route from a bucket alone
1482
+
1483
+ **What changed.** `canopycms-cdk` exports a free function taking the same options as
1484
+ `AssetSupportProps.uploadBehavior` plus the `bucket`:
1485
+
1486
+ ```ts
1487
+ import { assetUploadBehavior } from 'canopycms-cdk'
1488
+
1489
+ const uploads = new cloudfront.Distribution(this, 'AssetUploads', {
1490
+ defaultBehavior: assetUploadBehavior(this, { bucket: assetBucket }),
1491
+ })
1492
+ // media.uploadUrl = `https://${uploads.distributionDomainName}/`
1493
+ ```
1494
+
1495
+ **To adopt.** Optional. Don't move an existing deployment from `AssetSupport.uploadBehavior()` to
1496
+ the function: its three CloudFront resources would get new logical IDs and be replaced.
1497
+
1498
+ **Now deletable.** An `AssetSupport` instantiated only to reach `uploadBehavior()`. Cross-account,
1499
+ also check the bucket policy for its grant.
1500
+
1501
+ #### The CMS image builds without git, `CanopyCmsService` defaults to arm64, and the CDK app is type-checked — **breaking (deploy), for a stack that sets `platform` without `architecture`**
1502
+
1503
+ **What changed.** Mostly for stacks from an earlier `canopycms init-deploy aws` or a hand-copied
1504
+ `Dockerfile.cms.template`:
1505
+
1506
+ 1. `next build` reads content from the build context's working tree, never git, a branch clone or
1507
+ `.canopy-dev`. The generated `Dockerfile.cms` builder stage installs no git, commits no snapshot
1508
+ repository, and sets `ENV CANOPY_BUILD_MODE=true`; the runner stage still installs git.
1509
+ 2. The pnpm Dockerfile copies `pnpm-workspace.yaml` before installing; pnpm 11 keeps its
1510
+ `allowBuilds` decisions there.
1511
+ 3. `init-deploy aws` keeps `infrastructure/` out of the app's `tsconfig.json` and `.dockerignore`,
1512
+ and the generated workflow type-checks it with `tsc --noEmit -p infrastructure` (tsx, which
1513
+ `cdk.json` runs, drops a misspelled prop silently).
1514
+ 4. `CanopyCmsService` defaults to `Architecture.ARM_64` and passes it to the function, from which
1515
+ CDK derives a `fromImageAsset` image's platform; the workflow runs on `ubuntu-24.04-arm`.
1516
+ 5. `withCanopy()` adds sharp's libvips to Next's file tracing (not for a static export) and, on
1517
+ Next 16 and later, sets `turbopack: {}` when your config has neither `turbopack` nor your own
1518
+ `webpack` and it can read your Next version. A webpack build still fails its image transforms
1519
+ ([Dual Build Support](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#dual-build-support)).
1520
+
1521
+ **Breaking:** a stack setting `platform` on `fromImageAsset` without `architecture` gets an arm64
1522
+ function while `platform` still decides the image, so `Platform.LINUX_AMD64` builds an image the
1523
+ function cannot run. A stack setting neither moves to arm64 with a matching image, built under QEMU
1524
+ on an x86 host ([Where the image is built](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#where-the-image-is-built)).
1525
+
1526
+ **To adopt.**
1527
+
1528
+ 1. Delete `platform` from `fromImageAsset` with its `Platform` import; set `architecture` on
1529
+ `CanopyCmsService` only if you want x86_64.
1530
+ 2. Re-run `canopycms init-deploy aws`. It asks before replacing each existing file
1531
+ (`--non-interactive` skips them, `--force` replaces everything, including an edited stack), and
1532
+ always adds `infrastructure/tsconfig.json`, and the `exclude` entry unless `tsconfig.json` is
1533
+ missing, isn't plain JSON, or inherits `exclude` through `extends` (it then tells you to add it). In files you keep, bring
1534
+ across: the workflow's "Type-check the CDK app" step before "Configure AWS credentials", with
1535
+ `tsconfig.json` in `on.push.paths` (`examples/aws-deployment/deploy-cms.yml` has both);
1536
+ `runs-on: ubuntu-24.04-arm`; an `infrastructure` line in `.dockerignore`; and, with pnpm,
1537
+ `pnpm-workspace.yam[l]` in the Dockerfile's first `COPY`.
1538
+ 3. A hand-copied `Dockerfile.cms` builds as before; when convenient, delete the builder's git
1539
+ install and snapshot commit and add `ENV CANOPY_BUILD_MODE=true` before the build command.
1540
+
1541
+ **Now deletable.** The builder's git install and snapshot commit, and any step creating or checking
1542
+ out the base branch so the build could find it; CI steps attaching a detached HEAD for the same
1543
+ reason; `platform` on `fromImageAsset`; and, with `withCanopy()`, a hand-written
1544
+ `outputFileTracingIncludes` entry for libvips and a `turbopack: {}` added only for Next 16's error
1545
+ (unless under Yarn PnP, where `withCanopy()` cannot read your Next version).
1546
+
1547
+ ### 0.0.66
1548
+
1549
+ #### `media.uploadUrl` routes presigned uploads through your own CDN (#44)
1550
+
1551
+ **What changed.** `media.uploadUrl` (s3 adapter; an absolute http(s) URL or a site-relative path)
1552
+ replaces the `url` that `beginUpload()` returns, leaving the presign's `fields` untouched, so
1553
+ uploads can be same-origin. See [Media Configuration](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#media-configuration) for the
1554
+ CloudFront behavior it expects.
1555
+
1556
+ **To adopt.** Optional. Set it from an environment variable: a site-relative value works only where
1557
+ that path routes to the bucket, so it 404s under `next dev`.
1558
+
1559
+ **Now deletable.** The bucket CORS rule naming your editor's origin, once uploads are same-origin.
1560
+ `AssetSupport`'s `editorOrigins` is then unused; standalone mode still needs either it or
1561
+ `uploadBehavior`.
1562
+
1563
+ #### `media` config rejects unknown keys
1564
+
1565
+ **What changed.** Each branch of `mediaSchema` is `.strict()`, so a misspelled key under `media`
1566
+ fails validation, naming it.
1567
+
1568
+ **To adopt.** Fix or remove the key.
1569
+
1570
+ #### `AssetSupport` and `CanopyCmsService` take an execution role, so its ARN is derivable without a construct reference (#42)
1571
+
1572
+ **What changed.** `CanopyCmsServiceProps.lambdaRole?: iam.Role` (and
1573
+ `AssetSupportProps.transformRole`, which since [S3-only assets](#canopycms-cdk-assetsupport-serves-images-from-s3-only--breaking)
1574
+ needs `lazyPublicTransforms: true`) let you pass a deterministically named role, so a cross-account
1575
+ bucket's stack computes `arn:aws:iam::<account>:role/<name>` from literals instead of a
1576
+ `Fn::GetStackOutput` reference only the CDK CLI resolves. The constructs re-attach
1577
+ `AWSLambdaBasicExecutionRole` (and the VPC policy) that `lambda.Function` drops from a role you
1578
+ pass. See [Cross-account asset bucket](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#cross-account-asset-bucket).
1579
+
1580
+ **To adopt.** Optional:
1581
+
1582
+ ```ts
1583
+ const roleName = `canopy-cms-${tier}` // derive it however you name things
1584
+ const role = new iam.Role(this, 'CmsRole', {
1585
+ roleName,
1586
+ assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
1587
+ })
1588
+ new CanopyCmsService(this, 'Cms', { /* ... */ lambdaRole: role })
1589
+
1590
+ // In the bucket's stack, in the other account - no reference, just literals:
1591
+ const principal = new iam.ArnPrincipal(`arn:aws:iam::${tierAccount}:role/${roleName}`)
1592
+ ```
1593
+
1594
+ A named role needs `CAPABILITY_NAMED_IAM` and cannot be replaced in place without a rename. The type
1595
+ is `iam.Role`, not `IRole`: `addManagedPolicy` does nothing on an imported role, so create the role
1596
+ in the compute's stack rather than passing `Role.fromRoleArn`.
1597
+
1598
+ **Now deletable.** A `Fn::GetStackOutput`-producing cross-stack reference, a `CfnOutput`-plus-manual
1599
+ wiring step, or an asset grant scoped to the whole compute account because the role could not be
1600
+ named.
1601
+
1602
+ #### `AssetSupport.attachTo()` and `CanopyCmsDistribution`'s `assetSupport` prop order the asset behaviors for you (#41)
1603
+
1604
+ **What changed.** CloudFront takes the first matching behavior, so `/assets/*` listed before
1605
+ `/assets/t/*` (as alphabetizing does) sends every derivative to the wrong behavior.
1606
+ `AssetSupport.attachTo(distribution, overrides?)` adds `/assets/t/*` then `/assets/*`, merging
1607
+ `overrides` (`Partial<cloudfront.AddBehaviorOptions>`) into both. `CanopyCmsDistribution` takes
1608
+ `assetSupport` and `assetBehaviorOverrides` and calls it for you. Its synth checks refuse
1609
+ `/assets/*` before `/assets/t/*`, the literal `assets`/`assetsTransform` keys spread from
1610
+ `assetBehaviors()`, `assetSupport` while `additionalBehaviors` still lists an asset pattern, and
1611
+ `assetBehaviorOverrides` without `assetSupport`. `assetBehaviors()` is unchanged, and a bespoke
1612
+ `cloudfront.Distribution` calls `attachTo()` directly.
1613
+
1614
+ **To adopt.** Optional, but don't do half of it. Replace a hand-written
1615
+
1616
+ ```typescript
1617
+ additionalBehaviors: {
1618
+ '/assets/t/*': assetSupport.assetBehaviors().assetsTransform,
1619
+ '/assets/*': assetSupport.assetBehaviors().assets,
1620
+ },
1621
+ ```
1622
+
1623
+ with the prop, carrying any per-behavior options (such as a tier's basic-auth viewer-request
1624
+ function, without which `/assets/*` is anonymously readable) as overrides:
1625
+
1626
+ ```ts
1627
+ new CanopyCmsDistribution(this, 'Dist', {
1628
+ ...yourExistingDistributionProps,
1629
+ assetSupport,
1630
+ assetBehaviorOverrides: {
1631
+ functionAssociations: [{ function: tierAuthFn, eventType: FunctionEventType.VIEWER_REQUEST }],
1632
+ },
1633
+ })
1634
+ // on a bespoke distribution, pass the same object as attachTo's second argument
1635
+ ```
1636
+
1637
+ **Now deletable.** The hand-ordered `additionalBehaviors` block, and comments reminding you of the
1638
+ order. Keep `assetBehaviors()` only if the two behaviors must differ, with your own assertion on
1639
+ the synthesized `CacheBehaviors` order.
1640
+
1641
+ #### `canopycms-auth-clerk` supports Clerk Core 3 (`@clerk/nextjs` 7.x, `@clerk/backend` 3.x)
1642
+
1643
+ **What changed.** Peer ranges are `@clerk/nextjs: ^6.0.0 || ^7.0.0` and
1644
+ `@clerk/backend: ^2.0.0 || ^3.0.0`. Networkless `verifyToken` PEM verification still works.
1645
+
1646
+ **To adopt.** Optional. Core 3 needs `<ClerkProvider>` inside `<body>`, not around `<html>`
1647
+ (`apps/example1/app/layout.tsx`); a dual-build editor layout is already inside `<body>`. If you
1648
+ render `AccountComponent` yourself with `afterSignOutUrl`/`signOutUrl`, move them to
1649
+ `ClerkProvider`'s `afterSignOutUrl` or a `SignOutButton`. If you keep `clerkMiddleware`, it needs a non-empty `secretKey`.
1650
+ `CLERK_ENCRYPTION_KEY` does not apply to
1651
+ the scaffold's commented `clerkMiddleware` snippet, which passes only `jwtKey`. Node must be >= 22.12.0 for every CanopyCMS
1652
+ package regardless.
1653
+
1654
+ ```tsx
1655
+ <html lang="en">
1656
+ <body>
1657
+ <ClerkProvider>{children}</ClerkProvider>
1658
+ </body>
1659
+ </html>
1660
+ ```
1661
+
1662
+ #### `CanopyCmsService` gains `settingsBranch`, and the generated stack derives both branches from `canopycms.config.ts` (#39)
1663
+
1664
+ **What changed.** `settingsBranch` sets the worker's `CANOPYCMS_SETTINGS_BRANCH`; it and
1665
+ `baseBranch` are checked against git's branch-name rules at synth. The generated
1666
+ `infrastructure/lib/cms-stack.ts` imports `canopycms.config.ts` and passes
1667
+ `baseBranch: canopyConfig.server.defaultBaseBranch` and
1668
+ `settingsBranch: canopyConfig.server.settingsBranch`.
1669
+
1670
+ **To adopt.** Regenerate `cms-stack.ts` or copy the import and two lines; your own stack sets both
1671
+ props to match `canopycms.config.ts`. **Do this now if your default branch is not `main` or you set
1672
+ `settingsBranch`**: the worker otherwise exits at start or syncs a different settings branch than
1673
+ the Lambda writes. From int.110, prod detects an unset base branch from the repository's
1674
+ default branch ([Prod detects an unset `defaultBaseBranch`](#prod-detects-an-unset-defaultbasebranch-instead-of-assuming-main--behaviour-change-startup-can-fail)).
1675
+
1676
+ **Now deletable.** Runbook steps keeping `CANOPYCMS_BASE_BRANCH` or a settings-branch override in
1677
+ sync with `canopycms.config.ts` by hand.
1678
+
1679
+ #### `CLERK_JWT_KEY` is a repository **variable**, not a secret (#37)
1680
+
1681
+ **What changed.** The generated `deploy-cms.yml` reads `${{ vars.CLERK_JWT_KEY }}`: it is Clerk's
1682
+ public JWKS PEM, used only to verify signatures.
1683
+
1684
+ **To adopt.** If you regenerate or copy the workflow, move `CLERK_JWT_KEY` from repository secrets
1685
+ to variables (Settings → Secrets and variables → Actions → Variables); `bin/app.ts` refuses the
1686
+ deploy at synth while it is unset. Check that `bin/app.ts` passes `CLERK_SECRET_KEY` as
1687
+ `clerkSecretKeySecretArn` (read by the worker), never in the Lambda's `environment`, unless you keep
1688
+ `clerkMiddleware` ([Security Model](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/docs/deploying-to-aws.md#security-model)).
1689
+
1690
+ #### `readByUrlPath` answers only where `listEntries` publishes — **breaking (routing)**
1691
+
1692
+ **What changed.** For every entry, `readByUrlPath(item.urlPath)` reaches it and no other URL does.
1693
+ These now return `null`: `/<collection>/<entryTypeName>` (which reached the collection's index
1694
+ entry) and `/<collection>/<entryTypeName>/<slug>`; and an entry whose type token on disk its
1695
+ collection does not declare (a type renamed without renaming files, or a collection declaring no
1696
+ entry types); such entries were already missing from listings, static params and the sitemap.
1697
+ Those entries stay editable, renameable and deletable. `read({ entryPath: 'content/home' })` still
1698
+ addresses a singleton, defaulting the slug to the entry type's name. A legacy untyped file
1699
+ (`overview.json`) is still readable by URL but invisible to listings; rename it into the
1700
+ `{type}.{slug}.{id}.{ext}` grammar.
1701
+
1702
+ **To adopt.** On a catch-all route, request `/<collection>/<entryTypeName>` and
1703
+ `/<collection>/<entryTypeName>/<some-slug>` for a few type names and confirm a 404 (under
1704
+ `next dev` or `output: 'standalone'` they were served). If you renamed an entry type without
1705
+ renaming its files, rename them or declare the type. A build does not flag them; they are the files missing from
1706
+ `listEntries()`.
1707
+
1708
+ **Now deletable.** Per-route `entryType` gates that only reject a URL that should not have resolved
1709
+ (keep a branch that dispatches between templates), catch-all filters dropping entry-type names, and
1710
+ tests asserting a specific phantom URL returns `null`.
1711
+
1712
+ #### Static exports are reproducible: `CANOPY_BUILD_ID` pins the build id, and the AI manifest stops baking a wall clock — **breaking (type-level)**
1713
+
1714
+ **What changed.** `withCanopy(..., { staticBuild: true })` uses `CANOPY_BUILD_ID` as Next's build id
1715
+ (your own `generateBuildId` still wins; non-static builds ignore it). `generate-ai-content` records
1716
+ `buildId` from it in `public/ai/manifest.json`, and pins `generated` to `SOURCE_DATE_EPOCH`, or
1717
+ omits it when a build id is set without one. `AIManifest.generated` is `string | undefined`.
1718
+
1719
+ **To adopt.** Guard reads of `AIManifest.generated`. To make an export reproducible, export
1720
+ `CANOPY_BUILD_ID` for both `next build` and `generate-ai-content`, and `SOURCE_DATE_EPOCH` to keep
1721
+ the manifest's timestamp. The id is 1-255 characters of `[A-Za-z0-9._-]`, not `.` or `..`; an
1722
+ unusable value is ignored with a warning. Derive it from a tree hash, not a commit SHA or date,
1723
+ which a rebase changes for an identical tree.
1724
+
1725
+ **Now deletable.** A `generateBuildId: () => process.env.<YOUR_VAR> || null` line, **only if** you
1726
+ pass `{ staticBuild: true }` (on other builds it un-pins the id). Written with `??`, such a line
1727
+ ships an empty build id from an empty variable. Also a post-build step rewriting the manifest's
1728
+ `generated`.
1729
+
1730
+ ### 0.0.65
1731
+
1732
+ #### `canopycms-cdk`: CloudFront waits as long as the CMS Lambda, and the worker boots from AL2023's Node 22
1733
+
1734
+ **What changed.**
1735
+
1736
+ - `CanopyCmsDistribution` takes `originReadTimeout`, defaulting to 60 seconds, the CMS Lambda's
1737
+ default `timeout` (`cmsService.timeout` exposes the actual one); synth refuses more than 60
1738
+ seconds. Without a `certificate`, it refuses a stack region other than `us-east-1`, where
1739
+ CloudFront needs its certificate, when the region is known at synth.
1740
+ - The worker installs Node 22 with `dnf` and runs `/usr/bin/node-22`; a failed boot step shuts the
1741
+ instance down so the group replaces it. Its IAM policy covers `githubTokenSecretArn` and
1742
+ `clerkSecretKeySecretArn` as well as `secretsArns`. The bot token never stays in
1743
+ `remote.git`'s config on EFS, and one an earlier worker left there is scrubbed.
1744
+ - An `/assets/t/` URL whose slug is not the asset's own answers 404; `assetUrl` keeps the stored
1745
+ `src`'s slug, so the URLs it builds are unaffected.
1746
+ - `canopycms worker run-once` exits 1 on an unknown `CANOPY_AUTH_MODE`.
1747
+
1748
+ **To adopt.** If you override `CanopyCmsService`'s `timeout`, pass
1749
+ `originReadTimeout: cmsService.timeout` (the generated stack does). A hand-installed worker runs
1750
+ `dnf install -y nodejs22` and sets `ExecStart=/usr/bin/node-22 index.js`. The generated
1751
+ workflow reads the bot-token ARN from the secret `CANOPY_GITHUB_TOKEN_SECRET_ARN` (GitHub refuses
1752
+ `GITHUB_`-prefixed names): store it under that name. In a kept `Dockerfile.cms`, change both
1753
+ `node:20-slim` stages to `node:22-slim`; in `deploy-cms.yml`, set `node-version: 22`.
1754
+
1755
+ #### `canopycms init` scaffold fixes
1756
+
1757
+ **What changed.** `init` writes `middleware.ts` in the app directory's parent (`src/` for
1758
+ `--app-dir src/app`), leaves an existing `next.config.js`/`.mjs` alone and prints the wiring,
1759
+ creates `.gitignore` with `.canopy-dev/` when absent, and the generated workflow deploys on
1760
+ `next.config.*`, `middleware.ts` and `public/**` changes.
1761
+
1762
+ **To adopt.** Check what an earlier `init` left: a `middleware.ts` at the root of a `src/app`
1763
+ project, which Next never loads (move it into `src/`); a `next.config.ts` beside a `.js`/`.mjs`
1764
+ config, which Next ignores (wrap the loaded one in `withCanopy` and delete it); `.canopy-dev/`
1765
+ missing from `.gitignore`; and those three paths missing from `deploy-cms.yml`'s `on.push.paths`.
1766
+
1767
+ ### 0.0.64
1768
+
1769
+ #### `basePath` deployments are supported, and `assetUrl`'s `baseUrl` is now safe for path prefixes (#24)
1770
+
1771
+ **What changed.** `assetUrl()`/`assetSrcSet()`'s `baseUrl` accepts a same-origin path prefix
1772
+ (`'/preview-123'`): an absolute `src` is returned untouched, and a prefix without a leading slash is
1773
+ normalized. A top-level `basePath` config key makes the editor's API calls and preview pane work
1774
+ under a Next.js `basePath`.
1775
+
1776
+ **To adopt.** Under a `basePath`, state it in your Canopy config too (CanopyCMS cannot read
1777
+ `next.config`):
1778
+
1779
+ ```typescript
1780
+ // canopycms.config.ts
1781
+ basePath: process.env.NEXT_PUBLIC_BASE_PATH,
1782
+ ```
1783
+
1784
+ Then pass `baseUrl` to `assetUrl` only if Next serves `/assets` (local adapter, `next dev`, S3 with
1785
+ no distribution): `assetUrl(image, { width: 960, baseUrl: BASE_PATH })`. Assets on CloudFront via
1786
+ `AssetSupport` stay at the distribution root; pass no `baseUrl` (mount table under "Where `/assets`
1787
+ is mounted" in [Media Configuration](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#media-configuration)). Two traps:
1788
+
1789
+ - Don't pass a deployment `basePath` to `contentStaticParams({ basePath })`; that option filters by
1790
+ a nested route prefix and emits zero params, building green.
1791
+ - Body images bypass `assetUrl()`; under a `basePath` they need the README's `img` override, which
1792
+ roots a page-relative src onto the base, so make those root-relative first.
1793
+
1794
+ **Now deletable.** A prefixing wrapper re-exporting `assetUrl`/`assetSrcSet`, and a local "strip
1795
+ trailing slashes" helper (`stripTrailingSlashes` is in `canopycms/server`).
1796
+
1797
+ #### `select` fields infer their own options — **breaking (type-level)**
1798
+
1799
+ **What changed.** `TypeFromEntrySchema` infers a `select` as the literal union of its options'
1800
+ values (`'draft' | 'published'`), not `string | number`. Options must be literals at the type level
1801
+ (`defineEntrySchema` or `as const`); an array typed `SelectOption[]`, or no options, falls back to
1802
+ `string`. `''` is not in the union, though the validator accepts it for any field not marked
1803
+ `required: true`: compare cleared
1804
+ values before they reach the typed surface, or add `''` to the options.
1805
+
1806
+ **To adopt.** Fix comparisons against strings that are not options (now "no overlap" errors), and
1807
+ assignments of a plain `string` into a select value. Rename a custom field type's own `options`
1808
+ property, which is now a reserved key. Grep for `typeof value === 'number'` branches, which now
1809
+ narrow to `never` silently.
1810
+
1811
+ **Now deletable.** Casts or allowlists re-narrowing a select value to the app's own union, and
1812
+ `typeof v === 'string'` guards that only stripped `number`.
1813
+
1814
+ #### Listings resolve references, and a resolved reference carries `urlPath` and optionally its body (#16) — **breaking (type-level)**
1815
+
1816
+ **What changed.**
1817
+
1818
+ - `listEntries()`, `buildContentTree()` and `collectRoutableEntries` take `resolveReferences`
1819
+ (default `false`; `read()`'s is `true`), resolving references at any depth, including in blocks,
1820
+ at one read per distinct target per call. Path ACLs are not applied to resolved targets, as with
1821
+ `read()`.
1822
+ - Every resolved reference carries `urlPath`, the same URL `listEntries` publishes.
1823
+ - `includeBody: true` on a reference field adds the target's body, under the target type's body
1824
+ field name.
1825
+ - `TypeFromEntrySchema` adds `ResolvedReferenceMeta` (`id`, `slug`, `collection`, `urlPath`), which
1826
+ are reserved: they win over target fields of those names; read the target directly for its own
1827
+ field.
1828
+ - A save collapses a resolved reference back to its id. A reference saved through the editor on an
1829
+ earlier version may hold an object instead of a 12-character id (replace it with its own `id`), or
1830
+ `null` where its target had been deleted (the id is only in git history).
1831
+
1832
+ For what a target the reader may not see, or a missing one, resolves to, see
1833
+ [References](#a-reference-resolves-to-its-target-a-restricted-stub-or-a-missing-stub--security-fix-breaking-types-and-build).
1834
+
1835
+ **To adopt.** Turn on `resolveReferences` for surfaces that read inside references (search indexes,
1836
+ feeds); leave it off for static params and sitemaps. Add `& ResolvedReferenceMeta` (from
1837
+ `canopycms`) where an exact-shape type stops compiling. Set `includeBody: true` on fields whose
1838
+ target prose you render, or leave the body field out of `resolvedSchema`.
1839
+
1840
+ ```ts
1841
+ const entries = await ctx.listEntries({ resolveReferences: true })
1842
+ ```
1843
+
1844
+ **Now deletable.** A second `read()` pass over a listing to resolve ids, a surface rebuilt on
1845
+ per-entry `read()` to dodge the gap, a contentId → URL index built by a second content pass,
1846
+ `resolveReferences: false` forced by that index, and a follow-up `read()` only for a target's body.
1847
+
1848
+ #### Editor saves keep comments, and report content keys the schema does not define (#29)
1849
+
1850
+ **What changed.** A save re-serialises onto the file's parsed document, so YAML and frontmatter
1851
+ comments survive ([int.101 extends this to formatting](#a-save-rewrites-only-what-changed)). Unknown
1852
+ content keys come back as `validationWarnings` on save ("Saved with warnings"), and a production
1853
+ build's `collectStaticPaths`/`collectRoutableEntries` print one warning listing them by path. Nothing
1854
+ is rejected or stripped.
1855
+
1856
+ **To adopt.** Expect the first build to list keys you no longer use: add each to the schema or
1857
+ delete it.
1858
+
1859
+ **Now deletable.** Conventions keeping notes out of content files, scripts diffing content keys
1860
+ against a schema, and `?? fallback`s kept because nobody knew whether a field was populated.
1861
+
1862
+ #### An `index` entry answers only at its collection's URL, and a contested URL fails the build — **breaking (routing)**
1863
+
1864
+ **What changed.** `readByUrlPath('/guides/index')` (any case) returns `null`; the index entry
1865
+ answers at `/guides`. A collection named `index` resolves at `/x/index` to its own index entry. A
1866
+ production build fails when two entries compute the same `urlPath`, listing each; creating or
1867
+ renaming an entry into one is refused with 409, renaming a collection into one with 400. An entry
1868
+ beside a same-named sibling collection with no index entry is not contested. A save of an entry
1869
+ already contested still succeeds. `read()`/`readByUrlPath()` `path` collapses an index slug and
1870
+ strips the content root from root-level entries.
1871
+ [0.0.66](#readbyurlpath-answers-only-where-listentries-publishes--breaking-routing) closes the
1872
+ remaining extra URLs.
1873
+
1874
+ **To adopt.** A `shape: 'single'` (`[slug]`) static-params route no longer emits the collection's
1875
+ index entry, so that page can silently stop building: move it to the collection's own route
1876
+ (`app/posts/page.tsx`). For a contested URL, rename or remove one of each pair. A hand-rolled
1877
+ `generateStaticParams` over `listEntries` doesn't fail; call `findDuplicateUrlPaths` there, over
1878
+ `listEntries()` (`collectRoutableEntries()` drops the `entryPath` naming the offenders):
1879
+
1880
+ ```ts
1881
+ import { findDuplicateUrlPaths } from 'canopycms/server'
1882
+
1883
+ const canopy = await getCanopyForBuild()
1884
+ const duplicates = findDuplicateUrlPaths(await canopy.listEntries())
1885
+ ```
1886
+
1887
+ **Now deletable.** Route guards rejecting a `.../index` URL, duplicate-URL integrity tests, and
1888
+ editor-side checks for a contested URL.
1889
+
1890
+ #### Sitemap `pathFor`, and modelling a page served at `/` as a root `index` entry
1891
+
1892
+ **What changed.** An entry with slug `index` answers at its collection's path (`/` at the root). For a URL that
1893
+ can't be modelled, `generateContentSitemap` takes `pathFor: (entry) => string | null`, keeping the
1894
+ entry's `noindex`, `lastModified` and `priority` handling; `null` keeps the structural path, and an
1895
+ empty string throws. `extraUrls` is for URLs with no entry behind them.
1896
+
1897
+ **To adopt.** To re-model a singleton served at its collection's path (such as home at `/`):
1898
+
1899
+ 1. `git mv home.home.<id>.json home.index.<id>.json`; type and id are unchanged.
1900
+ 2. Change `read({ entryPath: 'content/home' })` to `readByUrlPath('/')` (`'/<collection>'`
1901
+ elsewhere), or pass `slug: 'index'`.
1902
+ Otherwise the build is green with a 404 at `/`: check the emitted HTML.
1903
+ 3. Drop the sitemap workaround and check `sitemap.xml`; redirect the old URL if it was indexed.
1904
+
1905
+ ```ts
1906
+ pathFor: (entry) =>
1907
+ entry.entryType === 'article' ? entry.urlPath.replace(/^\/articles\//, '/blog/') : null,
1908
+ ```
1909
+
1910
+ **Now deletable.** An `exclude` plus `extraUrls` pair re-adding a page's real URL, and hand-derived
1911
+ `noindex`/`lastModified` beside an extra URL.
1912
+
1913
+ #### A slug that cannot round-trip through a URL fails the build, and the CMS refuses to create one — **breaking (build)**
1914
+
1915
+ **What changed.** A slug must be lowercase letters, numbers and hyphens, starting with a letter or
1916
+ number, and at most 64 characters. A production build fails listing every entry whose slug isn't (such as
1917
+ `post.getting.started.guide.<id>.md`, which built and then 404'd), and a create or rename to one is
1918
+ refused with 400. Existing entries stay readable and renameable.
1919
+
1920
+ **To adopt.** Build once; rename the slug segment of each listed file
1921
+ (`post.getting-started-guide.<id>.md`), in the editor or with `git mv`, and fix hand-written links.
1922
+ Slugify generated content the same way.
1923
+
1924
+ **Now deletable.** CI checks walking filenames for unservable slugs, and editor-side slug checks.
1925
+
1926
+ ### 0.0.63
1927
+
1928
+ #### `required: false` infers an optional property (#14) — **breaking (type-level)**
1929
+
1930
+ **What changed.** `TypeFromEntrySchema` emits an explicitly `required: false` field as `name?: T`,
1931
+ at every level, instead of `name: T | undefined`. A field omitting `required` is still required.
1932
+
1933
+ **To adopt.** Change hand-written interfaces declaring `subheading: string | undefined` to
1934
+ `subheading?: string` (or derive them from the schema). Audit `Required<…>` on schema-derived types.
1935
+ Under `exactOptionalPropertyTypes: true`, omit keys instead of assigning `undefined`, and use
1936
+ `skipLibCheck: true` (the Next.js default): with `skipLibCheck: false` the package does not compile.
1937
+
1938
+ **Now deletable.** `: undefined,` lines that only satisfied a `required: false` field.
1939
+
1940
+ #### Sitemap and SEO metadata helpers (#10, #10a)
1941
+
1942
+ **What changed.** `collectRoutableEntries`, `extractSeoFields`, `isNoindexEntry`, `resolveSeoUrl`,
1943
+ `withTrailingSlash` and `isAbsoluteUrl` from `canopycms/server`; `defineSeoFieldGroup()` from
1944
+ `canopycms`; `generateContentSitemap` and `entryToMetadata` from `canopycms-next`, bound on
1945
+ `createNextCanopyContext`'s result. See
1946
+ [Sitemap and SEO Metadata](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#sitemap-and-seo-metadata). Every routable entry type is in
1947
+ the sitemap unless `exclude`d or `noindex`; one `noindex` predicate drives both robots metadata and
1948
+ sitemap exclusion. `trailingSlash` is never inferred. `lastModified` defaults to `updatedAt`, a
1949
+ filesystem mtime. An empty SEO field counts as unset. `siteUrl` must be absolute, colliding URLs are
1950
+ deduped with a warning, and a schema-invalid or unparseable content file fails a production build,
1951
+ sitemap included.
1952
+
1953
+ **To adopt.**
1954
+
1955
+ ```ts
1956
+ // app/lib/canopy.ts — bind the sitemap helper once
1957
+ export const contentSitemap = async (options: GenerateContentSitemapOptions) => {
1958
+ const context = await canopyContextPromise
1959
+ return context.generateContentSitemap(options)
1960
+ }
1961
+
1962
+ // app/sitemap.ts
1963
+ export const dynamic = 'force-static' // required for output: 'export'
1964
+ export default () =>
1965
+ contentSitemap({
1966
+ siteUrl: SITE_URL,
1967
+ trailingSlash: true, // match your framework's routing config
1968
+ exclude: (entry) => entry.entryType === 'author', // types with no page of their own
1969
+ })
1970
+
1971
+ // app/posts/[slug]/page.tsx — without the type argument `result.data` is `unknown`
1972
+ const result = await readByUrlPath<PostContent>(`/posts/${slug}`)
1973
+ return entryToMetadata(result?.data, {
1974
+ path: `/posts/${slug}`,
1975
+ siteUrl: SITE_URL,
1976
+ fallbackTitle: result?.data.title,
1977
+ })
1978
+ ```
1979
+
1980
+ Exclude entry types with no page of their own. Pass `seo` once to `createNextCanopyContext`, not
1981
+ per call. With `defineSeoFieldGroup({ group: 'seo' })`, include the same `group` in
1982
+ that `seo` option. Add `defineSeoFieldGroup()` to schemas with SEO fields; map an existing group with
1983
+ `seo: { fields: { title: 'yourName' } }` rather than keeping both. Pass a `lastModified` callback for a
1984
+ real content date, or return `undefined` from it to omit `<lastmod>`. Write `app/robots.ts` yourself.
1985
+
1986
+ **Now deletable.** A sitemap over a hardcoded list of entry types, a hand-written `Metadata` mapper,
1987
+ a local `withTrailingSlash`/`absoluteUrl` pair, and a content walk only for sitemap dates.
1988
+
1989
+ #### The build guard ignores files that were never entry-shaped
1990
+
1991
+ **What changed.** The build fails only on a file that could have been an entry: four or more
1992
+ dot-separated segments, or three whose first names an entry type in that collection. A
1993
+ `README.md` or a `5NVkkrB1MJUv.profile.json` sibling artifact builds clean; dot- and
1994
+ underscore-prefixed files are skipped.
1995
+
1996
+ **To adopt.** Move back, or rename back, a sibling artifact you moved or renamed to dodge the guard;
1997
+ `readSibling` reads it beside its entry.
1998
+
1999
+ #### `canopycms init` scaffolds `defaultBranchAccess: 'deny'` and public read
2000
+
2001
+ **What changed.** New scaffolds write `defaultBranchAccess: 'deny'` and
2002
+ `defaultPathAccess: { read: 'allow' }`
2003
+ ([Public read on server deployments](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#public-read-on-server-deployments)).
2004
+
2005
+ **To adopt.** An older scaffold's `defaultBranchAccess: 'allow'` is wider than recommended;
2006
+ consider `'deny'`. If you deleted `defaultPathAccess: { read: 'allow' }`, anonymous visitors get no content until you
2007
+ restore it: `readByUrlPath` pages 404 and `read()` throws; non-admin signed-in users without a path rule get 403 from
2008
+ the API.
2009
+
2010
+ #### Read and listing helpers replace hand-rolled parsing (#1, #2, #3, #4, #17)
2011
+
2012
+ **What changed.**
2013
+
2014
+ | API | From | Replaces |
2015
+ | ---------------------------------------------------------- | -------------------- | ---------------------------------------------------------- |
2016
+ | `parseTypedFilename(name, entryTypes?)` | `canopycms/server` | splitting `{type}.{slug}.{id}.{ext}` filenames |
2017
+ | `defaultBuildPath(path, root, kind)` | `canopycms/server` | a copy of `buildContentTree`'s default `buildPath` |
2018
+ | `meta.entryType`, `meta.entryId` on `read`/`readByUrlPath` | — | parsing type or id out of `meta.physicalPath` |
2019
+ | `updatedAt` on `listEntries` items | — | an `fs.stat` walk of the content root |
2020
+ | `createBuildCanopy(config, options)` | `canopycms/server` | a script's own services + context + admin-user boot |
2021
+ | `resolveEntryTitle(data, options)` | `canopycms(/server)` | a `title ?? name ?? humanize(slug)` chain |
2022
+ | `toPlainText(markdown)` | `canopycms/ai` | a Markdown/MDX stripper that drops custom components' text |
2023
+
2024
+ `parseTypedFilename` returns `null` for an id that isn't 12-character Base58 (no `0 O I l`).
2025
+ `meta.entryType` comes from the filename: for a legacy `{slug}.{ext}` file it is the collection's
2026
+ default type and `entryId` is `undefined`, and it may name a type the schema no longer declares.
2027
+ `updatedAt` is a filesystem mtime, reset by a fresh clone. `createBuildCanopy` bypasses all ACLs:
2028
+ scripts only, never request handling. `buildPath` still replaces the default rather than composing
2029
+ with it. Composing search documents is left to you; these are the shared pieces. To find a filename
2030
+ parser, look for `.split('.')` or `lastIndexOf('.')` on content filenames, often in link-check or
2031
+ slug-collision tests.
2032
+
2033
+ **To adopt.**
2034
+
2035
+ ```ts
2036
+ import { createBuildCanopy, resolveEntryTitle } from 'canopycms/server'
2037
+ import { toPlainText } from 'canopycms/ai'
2038
+
2039
+ const canopy = await createBuildCanopy(config.server, { entrySchemaRegistry })
2040
+ for (const entry of await canopy.listEntries()) {
2041
+ const title = resolveEntryTitle(entry.data, { schema: entry.schema })
2042
+ const body = typeof entry.data.body === 'string' ? toPlainText(entry.data.body) : ''
2043
+ }
2044
+ ```
2045
+
2046
+ Dispatch on `result.meta.entryType` with a default case.
2047
+
2048
+ **Now deletable.** Each "Replaces" column above, plus route-level guard-and-delegate blocks that
2049
+ re-checked an entry's type (a `switch` on `meta.entryType` can merge them into one catch-all).
2050
+
2051
+ #### `BlockComponentRegistry`: exhaustive block → component types (#13)
2052
+
2053
+ **What changed.** `BlockComponentRegistry<Blocks>` (types only, from `canopycms`; `BlockValueOf<Blocks, 'hero'>`
2054
+ gives one template's value type) requires exactly
2055
+ one component per block template name when written as an object literal. See
2056
+ [Block Component Registries](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#block-component-registries) and
2057
+ `apps/example1/app/components/PostView.tsx`.
2058
+
2059
+ ```ts
2060
+ import type { BlockComponentRegistry } from 'canopycms'
2061
+
2062
+ type Blocks = Page['blocks'][number]
2063
+
2064
+ const blockRegistry: BlockComponentRegistry<Blocks> = {
2065
+ hero: ({ data }) => <HeroSection headline={data.headline} />,
2066
+ cta: ({ data }) => <CtaSection title={data.title} />,
2067
+ // Missing a key here, or a key that doesn't match a template name, is a compile error.
2068
+ }
2069
+ ```
2070
+
2071
+ **Now deletable.** A `switch (block.template)` with a silent `default`, and tests matching handled
2072
+ templates against the schema.
2073
+
2074
+ #### Reusable field fragments, plus `defineFieldFragment()` (#15)
2075
+
2076
+ **What changed.** Spreading a `const` field array into several schemas, and nesting
2077
+ `defineInlineFieldGroup()` inside a block template, are documented
2078
+ ([Reusable Field Fragments](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#reusable-field-fragments)); `defineFieldFragment()` is an
2079
+ identity helper for them.
2080
+
2081
+ ```ts
2082
+ const ctaFields = defineFieldFragment([
2083
+ { name: 'ctaLabel', type: 'string' },
2084
+ { name: 'ctaHref', type: 'string' },
2085
+ ])
2086
+ const heroSchema = defineEntrySchema([{ name: 'headline', type: 'string' }, ...ctaFields])
2087
+ ```
2088
+
2089
+ **Now deletable.** A field cluster spelled out by hand in several schemas; override a differing
2090
+ field by composing from the same `const` field object.
2091
+
2092
+ #### Shared/referenced blocks are documented (#16)
2093
+
2094
+ A block template holding a `reference` field is a shared block; `read()`/`readByUrlPath()` resolve
2095
+ it, and listings do with
2096
+ [`resolveReferences`](#listings-resolve-references-and-a-resolved-reference-carries-urlpath-and-optionally-its-body-16--breaking-type-level)
2097
+ (0.0.64). See [Shared / Referenced Blocks](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#shared--referenced-blocks).
2098
+
2099
+ **Now deletable.** A second `read()` in page code to unwrap a shared block's reference.
2100
+
2101
+ #### `checkPathAccess` removed from `CanopyServices`
2102
+
2103
+ **What changed.** It was bound with an empty rule set, so it always returned the default decision.
2104
+
2105
+ **To adopt.** Replace `context.services.checkPathAccess` with
2106
+ `context.services.createContentAccessChecker(...)`, which resolves the real rules.
2107
+
2108
+ ### 0.0.62 and earlier
2109
+
2110
+ Not retro-documented, apart from these:
2111
+
2112
+ - **`rich-text` was removed** (breaking). Change a `type: 'rich-text'` field to `type: 'markdown'`.
2113
+ - **Content IDs are 12-character Base58** without `0 O I l`. An entry with a hand-rolled ID
2114
+ containing one never loads: it is skipped silently in the editor and `next dev`, and fails a
2115
+ production build. Use `generateId()` from `canopycms/server`.
2116
+
2117
+ #### The registry is keyed by entry-type name (0.0.42)
2118
+
2119
+ **What changed.** `createEntrySchemaRegistry` keys are the entry-type names that `.collection.json` files name in `entry.schema`, and `EntryTypesFromRegistry` derives the typed entry-type map from them. Schema-variable keys (`{ postSchema }`) still work but derive nothing; [README's registry convention](https://github.com/safeinsights/canopycms/blob/39a755c116a52b6d16e0b09e9dde03c67a081754/README.md#convention-why-key-the-registry-by-entry-type-name) says when to keep them.
2120
+
2121
+ **To adopt.**
2122
+
2123
+ 1. Rename the keys in `schemas.ts`: `{ postSchema, authorSchema }` becomes `{ post: postSchema, author: authorSchema }`.
2124
+ 2. Rename every `entry.schema` string in `content/**/.collection.json` to match (`"schema": "postSchema"` becomes `"schema": "post"`), then confirm nothing is left with `grep -r 'Schema"' content/`.
2125
+ 3. Add `export type EntryTypes = EntryTypesFromRegistry<typeof entrySchemaRegistry>` and derive the per-schema aliases from it (`type PostContent = EntryTypes['post']`).
2126
+ 4. Pass `EntryTypes` as the second generic wherever you call `buildContentTree`, so `meta.indexEntry.data` narrows on `meta.indexEntry.entryType`.
2127
+ 5. Run `pnpm typecheck`. A `.collection.json` still naming an old key fails `next build` with `Schema reference "postSchema" ... not found in registry. Available schemas: ...`; in the editor, that entry type shows as unavailable.
2128
+
2129
+ Content files, frontmatter and `.canopy-meta/` caches are untouched; in dev, editing a `.collection.json` invalidates the schema cache.
2130
+
2131
+ **Now deletable.** A hand-written interface of `TypeFromEntrySchema<typeof xSchema>` members that existed only to type `buildContentTree`'s second generic.