canopycms 0.1.0-int.115 → 0.2.0-int.119

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 (203) hide show
  1. package/CHANGELOG.md +370 -231
  2. package/README.md +0 -1
  3. package/dist/ai/references.js +3 -1
  4. package/dist/api/assets.d.ts +27 -7
  5. package/dist/api/assets.js +99 -38
  6. package/dist/api/branch-merge.d.ts +1 -1
  7. package/dist/api/branch.js +1 -1
  8. package/dist/api/client.d.ts +5 -3
  9. package/dist/api/client.js +7 -3
  10. package/dist/api/content.js +1 -1
  11. package/dist/api/entries-constants.d.ts +1 -1
  12. package/dist/api/entries-constants.js +1 -1
  13. package/dist/api/entries.d.ts +2 -2
  14. package/dist/api/index.d.ts +1 -1
  15. package/dist/api/permissions.d.ts +2 -2
  16. package/dist/api/types.d.ts +3 -2
  17. package/dist/assets/asset-prefixes.d.ts +4 -1
  18. package/dist/assets/asset-prefixes.js +3 -1
  19. package/dist/assets/finalize.js +14 -1
  20. package/dist/assets/keys.d.ts +4 -1
  21. package/dist/assets/keys.js +4 -1
  22. package/dist/assets/store-local.d.ts +18 -9
  23. package/dist/assets/store-local.js +58 -26
  24. package/dist/assets/store-s3.d.ts +19 -4
  25. package/dist/assets/store-s3.js +56 -13
  26. package/dist/assets/types.d.ts +26 -2
  27. package/dist/cli/cli.js +128 -48
  28. package/dist/cli/generate-ai-content.js +7 -5
  29. package/dist/cli/init.js +4 -4
  30. package/dist/client.d.ts +0 -1
  31. package/dist/client.js +0 -1
  32. package/dist/config/schemas/config.d.ts +0 -5
  33. package/dist/config/schemas/config.js +0 -1
  34. package/dist/config/types.d.ts +0 -1
  35. package/dist/content-index-generation.d.ts +1 -1
  36. package/dist/content-index-generation.js +1 -1
  37. package/dist/content-listing.js +1 -1
  38. package/dist/content-reader.js +1 -1
  39. package/dist/content-store.d.ts +1 -1
  40. package/dist/content-store.js +1 -1
  41. package/dist/dev-content-watcher.d.ts +1 -1
  42. package/dist/dev-content-watcher.js +1 -1
  43. package/dist/editor/BranchManager.d.ts +2 -0
  44. package/dist/editor/BranchManager.js +6 -4
  45. package/dist/editor/CanopyEditor.d.ts +1 -1
  46. package/dist/editor/CanopyEditor.js +1 -2
  47. package/dist/editor/CommentsPanel.js +69 -34
  48. package/dist/editor/Editor.d.ts +3 -2
  49. package/dist/editor/Editor.js +361 -191
  50. package/dist/editor/EditorAuthGate.js +8 -3
  51. package/dist/editor/EntryNavigator.d.ts +18 -1
  52. package/dist/editor/EntryNavigator.js +82 -18
  53. package/dist/editor/FormHeader.d.ts +2 -1
  54. package/dist/editor/FormHeader.js +10 -6
  55. package/dist/editor/FormRenderer.d.ts +7 -2
  56. package/dist/editor/FormRenderer.js +51 -16
  57. package/dist/editor/PreviewFrame.d.ts +15 -2
  58. package/dist/editor/PreviewFrame.js +22 -5
  59. package/dist/editor/PreviewPane.d.ts +61 -0
  60. package/dist/editor/PreviewPane.js +194 -0
  61. package/dist/editor/PreviewToolbarTools.d.ts +30 -0
  62. package/dist/editor/PreviewToolbarTools.js +81 -0
  63. package/dist/editor/branch-status.d.ts +6 -0
  64. package/dist/editor/branch-status.js +10 -0
  65. package/dist/editor/comments/BranchComments.d.ts +2 -4
  66. package/dist/editor/comments/BranchComments.js +7 -11
  67. package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
  68. package/dist/editor/comments/ThreadCarousel.js +4 -49
  69. package/dist/editor/comments/ThreadReplyControls.js +5 -2
  70. package/dist/editor/comments/form-comments.js +2 -1
  71. package/dist/editor/components/BranchLockBanner.d.ts +24 -0
  72. package/dist/editor/components/BranchLockBanner.js +43 -0
  73. package/dist/editor/components/BranchPill.d.ts +33 -0
  74. package/dist/editor/components/BranchPill.js +31 -0
  75. package/dist/editor/components/BranchesDrawer.js +3 -3
  76. package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
  77. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  78. package/dist/editor/components/EditorHeader.d.ts +47 -109
  79. package/dist/editor/components/EditorHeader.js +128 -148
  80. package/dist/editor/components/EditorRail.d.ts +48 -0
  81. package/dist/editor/components/EditorRail.js +64 -0
  82. package/dist/editor/components/EntryBreadcrumb.d.ts +32 -0
  83. package/dist/editor/components/EntryBreadcrumb.js +53 -0
  84. package/dist/editor/components/QuickOpenPalette.d.ts +8 -2
  85. package/dist/editor/components/QuickOpenPalette.js +8 -2
  86. package/dist/editor/components/RenameEntryModal.d.ts +8 -1
  87. package/dist/editor/components/RenameEntryModal.js +21 -5
  88. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  89. package/dist/editor/components/SaveAllResult.js +86 -0
  90. package/dist/editor/components/SaveSplitButton.d.ts +31 -0
  91. package/dist/editor/components/SaveSplitButton.js +121 -0
  92. package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
  93. package/dist/editor/components/StagedChangesDrawer.js +5 -4
  94. package/dist/editor/components/UndoToastMessage.d.ts +6 -5
  95. package/dist/editor/components/UndoToastMessage.js +6 -4
  96. package/dist/editor/components/UnsavedMark.d.ts +9 -0
  97. package/dist/editor/components/UnsavedMark.js +20 -0
  98. package/dist/editor/components/UserBadge.js +13 -11
  99. package/dist/editor/components/index.d.ts +2 -2
  100. package/dist/editor/components/index.js +2 -2
  101. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  102. package/dist/editor/context/EditorStateContext.js +4 -4
  103. package/dist/editor/copy.d.ts +7 -0
  104. package/dist/editor/copy.js +7 -0
  105. package/dist/editor/editor-utils.d.ts +52 -7
  106. package/dist/editor/editor-utils.js +122 -30
  107. package/dist/editor/entry-search.d.ts +4 -2
  108. package/dist/editor/entry-search.js +2 -3
  109. package/dist/editor/fields/FieldLabel.d.ts +1 -1
  110. package/dist/editor/fields/MarkdownField.js +3 -0
  111. package/dist/editor/fields/ObjectField.d.ts +13 -2
  112. package/dist/editor/fields/ObjectField.js +38 -9
  113. package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
  114. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
  115. package/dist/editor/group-manager/index.js +1 -1
  116. package/dist/editor/hooks/save-all.d.ts +68 -0
  117. package/dist/editor/hooks/save-all.js +61 -0
  118. package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
  119. package/dist/editor/hooks/useCommentSystem.js +3 -0
  120. package/dist/editor/hooks/useDraftManager.d.ts +24 -1
  121. package/dist/editor/hooks/useDraftManager.js +297 -16
  122. package/dist/editor/hooks/useEditorLayout.d.ts +19 -11
  123. package/dist/editor/hooks/useEditorLayout.js +18 -25
  124. package/dist/editor/hooks/useEntryManager.d.ts +6 -0
  125. package/dist/editor/hooks/useEntryManager.js +37 -12
  126. package/dist/editor/media/AssetCard.d.ts +6 -3
  127. package/dist/editor/media/AssetCard.js +20 -7
  128. package/dist/editor/media/AssetListRow.d.ts +9 -4
  129. package/dist/editor/media/AssetListRow.js +14 -7
  130. package/dist/editor/media/MediaLibrary.d.ts +1 -1
  131. package/dist/editor/media/MediaLibrary.js +5 -4
  132. package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
  133. package/dist/editor/media/MediaLibraryBody.js +65 -37
  134. package/dist/editor/media/useAssetLibrary.d.ts +6 -3
  135. package/dist/editor/media/useAssetLibrary.js +17 -10
  136. package/dist/editor/pane-header.d.ts +6 -0
  137. package/dist/editor/pane-header.js +6 -0
  138. package/dist/editor/permission-manager/index.js +1 -1
  139. package/dist/editor/preview-bridge.d.ts +16 -0
  140. package/dist/editor/preview-bridge.js +12 -3
  141. package/dist/editor/preview-path.d.ts +22 -3
  142. package/dist/editor/preview-path.js +92 -5
  143. package/dist/editor/preview-scale.d.ts +27 -0
  144. package/dist/editor/preview-scale.js +34 -0
  145. package/dist/editor/shell/ActivityRail.d.ts +35 -0
  146. package/dist/editor/shell/ActivityRail.js +91 -0
  147. package/dist/editor/shell/EditorShell.d.ts +32 -0
  148. package/dist/editor/shell/EditorShell.js +47 -0
  149. package/dist/editor/shell/OverlayHeader.d.ts +19 -0
  150. package/dist/editor/shell/OverlayHeader.js +15 -0
  151. package/dist/editor/shell/PaneLayout.d.ts +33 -0
  152. package/dist/editor/shell/PaneLayout.js +171 -0
  153. package/dist/editor/shell/SidePanelHost.d.ts +37 -0
  154. package/dist/editor/shell/SidePanelHost.js +221 -0
  155. package/dist/editor/shell/TopBar.d.ts +12 -0
  156. package/dist/editor/shell/TopBar.js +15 -0
  157. package/dist/editor/shell/host-chrome.d.ts +16 -0
  158. package/dist/editor/shell/host-chrome.js +69 -0
  159. package/dist/editor/theme.js +20 -3
  160. package/dist/editor/undo-removal.d.ts +19 -10
  161. package/dist/editor/undo-removal.js +44 -16
  162. package/dist/git-manager.d.ts +1 -1
  163. package/dist/git-manager.js +2 -2
  164. package/dist/operating-mode/types.d.ts +1 -1
  165. package/dist/resolve-canopy-user.d.ts +1 -1
  166. package/dist/resolve-canopy-user.js +1 -1
  167. package/dist/schema/resolver.d.ts +1 -1
  168. package/dist/schema/resolver.js +1 -1
  169. package/dist/schema/schema-store.d.ts +1 -1
  170. package/dist/schema/schema-store.js +2 -2
  171. package/dist/url-collision.js +3 -3
  172. package/dist/url-exclusivity-fixtures.d.ts +1 -1
  173. package/dist/url-exclusivity-fixtures.js +1 -1
  174. package/dist/utils/content-write-lock.d.ts +1 -1
  175. package/dist/utils/content-write-lock.js +1 -1
  176. package/dist/utils/debug.d.ts +1 -1
  177. package/dist/utils/debug.js +5 -3
  178. package/dist/utils/git.d.ts +4 -0
  179. package/dist/utils/git.js +27 -5
  180. package/dist/validation/block-structural-keys.d.ts +1 -1
  181. package/dist/validation/block-structural-keys.js +1 -1
  182. package/dist/version.d.ts +1 -1
  183. package/dist/version.js +1 -1
  184. package/dist/worker/cms-worker.d.ts +1 -1
  185. package/dist/worker/cms-worker.js +2 -2
  186. package/dist/worker/github-auth.d.ts +1 -1
  187. package/dist/worker/github-auth.js +1 -1
  188. package/dist/worker/github-gateway-wire.d.ts +2 -1
  189. package/dist/worker/github-gateway-wire.js +7 -3
  190. package/dist/worker/github-gateway.js +1 -1
  191. package/dist/worker/github-mirror.d.ts +2 -2
  192. package/dist/worker/github-mirror.js +2 -2
  193. package/dist/worker/history-rewrite.d.ts +1 -1
  194. package/dist/worker/history-rewrite.js +1 -1
  195. package/dist/worker/self-checks.d.ts +1 -1
  196. package/dist/worker/shared-repo-git.js +1 -1
  197. package/package.json +5 -4
  198. package/dist/editor/EditorPanes.d.ts +0 -20
  199. package/dist/editor/EditorPanes.js +0 -124
  200. package/dist/editor/components/EditorFooter.d.ts +0 -11
  201. package/dist/editor/components/EditorFooter.js +0 -9
  202. package/dist/editor/components/EditorSidebar.d.ts +0 -70
  203. package/dist/editor/components/EditorSidebar.js +0 -50
package/CHANGELOG.md CHANGED
@@ -5,38 +5,11 @@ packages release together, at one version.
5
5
 
6
6
  ## This build
7
7
 
8
- Version `0.1.0-int.115`, built from [`f8b50b3`](https://github.com/safeinsights/canopycms/tree/f8b50b30170954bf52fb670afe6796d2cb8fa275).
9
-
10
- ### New since int.114
11
-
12
- - [AI content can withhold or redirect a link to an entry with no page](#ai-content-can-withhold-or-redirect-a-link-to-an-entry-with-no-page)
13
- - [toPlainText takes an entry body and keeps prose after a leading --- — behaviour change](#toplaintext-takes-an-entry-body-and-keeps-prose-after-a-leading------behaviour-change)
14
- - [People without a name show "Name unavailable"](#people-without-a-name-show-name-unavailable)
15
- - [Branch access lists stay with admins and reviewers — behaviour change](#branch-access-lists-stay-with-admins-and-reviewers--behaviour-change)
16
- - [A branch lockdown outranks the branch's access list — security fix](#a-branch-lockdown-outranks-the-branchs-access-list--security-fix)
17
- - [The base-branch conflict notice stays until the branch merges — behaviour change](#the-base-branch-conflict-notice-stays-until-the-branch-merges--behaviour-change)
18
- - [The editor calls the content drawer "All content"](#the-editor-calls-the-content-drawer-all-content)
19
- - [The Branches drawer shows one action per branch, and comments reply in place](#the-branches-drawer-shows-one-action-per-branch-and-comments-reply-in-place)
20
- - [An object's Clear offers Undo, and refusals say why](#an-objects-clear-offers-undo-and-refusals-say-why)
21
- - [The collection dialog says what saves at once](#the-collection-dialog-says-what-saves-at-once)
22
- - [Comments and branches name their people for every editor — behaviour change on two APIs](#comments-and-branches-name-their-people-for-every-editor--behaviour-change-on-two-apis)
23
- - [A date picker, a form header, and a toolbar that wraps](#a-date-picker-a-form-header-and-a-toolbar-that-wraps)
24
- - [Editor colours reach 4.5:1 in light and dark mode](#editor-colours-reach-451-in-light-and-dark-mode)
25
- - [Editor contrast, and blocked buttons stay focusable](#editor-contrast-and-blocked-buttons-stay-focusable)
26
- - [Editor focus rings and checked controls reach 3:1 for any brand](#editor-focus-rings-and-checked-controls-reach-31-for-any-brand)
27
- - [Editor requests time out instead of hanging](#editor-requests-time-out-instead-of-hanging)
28
- - [Field editing: descriptions, list-card titles, and public/ images, continued](#field-editing-descriptions-list-card-titles-and-public-images-continued)
29
- - [List items and blocks collapse, and reorder from one menu](#list-items-and-blocks-collapse-and-reorder-from-one-menu)
30
- - ["Add entry" follows path permissions; admin panels and labels tidied](#add-entry-follows-path-permissions-admin-panels-and-labels-tidied)
31
- - [Quick open, a navigator filter, title-first new entries, and media search](#quick-open-a-navigator-filter-title-first-new-entries-and-media-search)
32
- - [A save never brings back a deleted or moved entry — behaviour change](#a-save-never-brings-back-a-deleted-or-moved-entry--behaviour-change)
33
- - [Submit names unsaved entries; unanswered writes recover more](#submit-names-unsaved-entries-unanswered-writes-recover-more)
34
- - [Unlabelled fields and block templates show their names as words](#unlabelled-fields-and-block-templates-show-their-names-as-words)
35
- - [Stable releases follow the changes they carry, and ship their changelog](#stable-releases-follow-the-changes-they-carry-and-ship-their-changelog)
36
- - [List items and blocks carry a \_key, and field comments follow it — behaviour change](#list-items-and-blocks-carry-a-_key-and-field-comments-follow-it--behaviour-change)
37
- - [stampOnPublish dates a new entry at its first submit](#stamponpublish-dates-a-new-entry-at-its-first-submit)
38
-
39
- 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. Legacy entries above the changeset entries carry hand-written int labels.
8
+ Version `0.2.0-int.119`, built from [`35953fb`](https://github.com/safeinsights/canopycms/tree/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a).
9
+
10
+ No earlier int is in this build's history, so read every Unreleased entry.
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.
40
13
 
41
14
  ## How to use this document
42
15
 
@@ -51,7 +24,7 @@ Each entry has up to three parts:
51
24
  `node_modules/canopycms/CHANGELOG.md`, assembled for that build: its **This build** section lists
52
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
53
26
  came from. The copy in the repository holds released history and the entries from before changesets;
54
- every entry written since is a file in [`.changeset/`](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/.changeset/README.md) until a stable release
27
+ every entry written since is a file in [`.changeset/`](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/.changeset/README.md) until a stable release
55
28
  moves it here.
56
29
 
57
30
  **Find your starting point.** From a stable pin, start at the first version under **Released**
@@ -80,89 +53,119 @@ Nothing in this section is edited by hand: each entry is a `.changeset/` file, w
80
53
  assembles here and labels with the first int that carries it, and a stable release moves the
81
54
  entries under **Released**.
82
55
 
83
- <!-- legacy:start -->
84
-
85
- The entries below predate changesets and keep their hand-written labels.
86
-
87
- | int | Area | Change | Action |
88
- | --- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
89
- | 93 | Branches | [Path rules match logical paths](#path-permission-rules-now-take-effect-below-the-content-root--security-fix-breaking-for-rules-written-with-on-disk-names) | Review rules |
90
- | 93 | Branches | [`.canopy-meta/` must not be committed](#canopy-meta-must-not-be-committed-and-system-health-says-so) | If tracked |
91
- | 93 | Branches | [`autoCreateSettingsPR` removed](#settings-saves-only-push-the-settings-branch--breaking-config-autocreatesettingspr-is-removed) | Required |
92
- | 93 | Reading | [Listings take `branch`; reads report `slug`/`urlPath`](#content-reads-listings-take-a-branch-and-reads-report-slug-and-urlpath) (102) | Index pages |
93
- | 93 | Editing | [Field descriptions, list-card titles, `public/` images](#field-editing-descriptions-list-card-titles-and-public-images) (96, 103) | None |
94
- | 93 | Branches | [Commits name editors; submit refuses an empty branch](#commits-name-their-editors-and-submit-refuses-a-branch-with-nothing-to-submit) (108, 110) | Scripts |
95
- | 94 | Branches | [Reads never create a branch](#reads-never-create-a-requested-branch--security-fix-breaking-for-some-direct-createcontentreader-callers) | Direct callers |
96
- | 94 | Preview | [Preview URLs: one prefix, `trailingSlash`, own page](#preview-urls-take-one-prefix-follow-trailingslash-and-load-each-entrys-own-page--breaking-env) (98) | Required |
97
- | 94 | Preview | [`createPreviewPage`, `/preview` entries](#the-preview-route-and-preview-entries--breaking-imports) (95, 101, 103) | Required |
98
- | 94 | Refs | [Restricted and missing references](#a-reference-resolves-to-its-target-a-restricted-stub-or-a-missing-stub--security-fix-breaking-types-and-build) (106) | Required |
99
- | 94 | CDK | [`attachTo`, editor response headers](#canopycms-cdk-canopycmsserviceattachto-and-editor-response-headers--behaviour-change-if-you-frame-the-cms) | If hand-wired or framed |
100
- | 94 | Ops | [System health shows the build](#system-health-shows-which-build-is-running) | Optional |
101
- | 100 | Branches | [Crash-safe branch creation](#fast-crash-safe-branch-creation--behaviour-change-branch-clones-hold-only-the-content-root) | Upgrade together |
102
- | 100 | Ops | [Prod workspace defaults to `/mnt/efs`](#the-prod-workspace-defaults-to-mntefs--behaviour-change-if-you-run-prod-without-canopycms_workspace_root) | Non-CDK prod |
103
- | 100 | Assets | [`AssetSupport` serves from S3 only](#canopycms-cdk-assetsupport-serves-images-from-s3-only--breaking) | Required |
104
- | 100 | Assets | [`collect-asset-refs`, `materialize-assets`](#collect-asset-refs-and-materialize-assets-store-a-builds-images-before-release) (104) | With S3-only |
105
- | 100 | Assets | [Editor images via signed-in route](#the-editor-and-its-live-preview-load-images-through-the-signed-in-asset-route) | CSP, origin |
106
- | 100 | Assets | [`assetUrl` applies crop](#asseturl-applies-an-image-values-crop--behaviour-change-for-cropped-images) | Scale `<img>` |
107
- | 100 | Assets | [`media.publicBaseUrl` removed; wider limits](#mediapublicbaseurl-is-removed-and-image-limits-are-wider--breaking-config) | Delete key |
108
- | 101 | Editing | [Saves rewrite only what changed](#a-save-rewrites-only-what-changed) (103) | None |
109
- | 102 | Auth | [`unauthenticatedStatus: 419`](#unauthenticatedstatus-answer-signed-out-api-calls-with-419) | Basic-auth sites |
110
- | 105 | Editing | [MDXEditor 4.3](#the-markdown-editor-runs-mdxeditor-43) | Direct deps |
111
- | 106 | Refs | [AI content links references](#ai-content-links-a-reference-to-its-target--behaviour-change-for-ai-output-and-ai-config-callbacks) | AI config |
112
- | 106 | Refs | [Deleting a referenced entry asks](#deleting-a-referenced-entry-asks-first--behaviour-change-on-the-delete-api) | Scripts |
113
- | 107 | Schema | [Unknown schemas cost one entry type](#a-schema-the-running-code-lacks-costs-one-entry-type-and-holds-the-worker--behaviour-change) | Rare |
114
- | 108 | MDX | [MDX that runs code is refused](#mdx-content-that-runs-code-is-refused-at-save--breaking-behaviour) (109, 110) | Required |
115
- | 108 | Worker | [On-demand worker, drain; `spotMaxPrice` removed](#canopycms-cdk-the-worker-drains-before-replacement-and-runs-on-demand--breaking-props-spotmaxprice-is-removed-behaviour-and-cost-change) | If set |
116
- | 109 | Worker | [Hardened worker instance](#canopycms-cdk-the-worker-instance-is-hardened--an-existing-stack-upgrades-in-two-deploys-behaviour-and-cost-change) | Two deploys |
117
- | 109 | Worker | [Failed or stopped worker says why](#a-failed-or-stopped-worker-says-why--behaviour-change-on-the-not-ready-503-new-worker-apis) | Custom entrypoint |
118
- | 109 | Worker | [CI worker roll; worker-down alarm](#canopycms-cdk-ci-can-roll-the-worker-with-a-parameter-and-alarm-when-it-stops-syncing--new-opt-in) | Optional |
119
- | 109 | Preview | [Typed `fieldProps`](#preview-fieldprops-is-typed-with-server-safe-helpers--breaking-types-and-schemas) | Required |
120
- | 110 | Worker | [Worker needs a state directory](#canopycms-cdk-the-worker-needs-a-state-directory--deploy-the-template-before-the-bundle) | Template first |
121
- | 110 | Ops | [Prod detects an unset `defaultBaseBranch`](#prod-detects-an-unset-defaultbasebranch-instead-of-assuming-main--behaviour-change-startup-can-fail) | Base ≠ repo default |
122
- | 111 | CDK | [Example workflow's triggers and checks](#the-aws-example-workflow-gains-the-templates-triggers-and-dependency-checks) | If copied by hand |
123
- | 111 | Ops | [Duplicate-ID scan only on request](#get-adminbranch-health-scans-for-duplicate-content-ids-only-on-request--behaviour-change) | Admin-API scripts |
124
- | 111 | Auth | [CMS image builds a prod editor; mismatch blocks](#the-cms-image-builds-a-prod-editor-and-a-mode-mismatch-blocks-the-editor--behaviour-change-a-hand-built-image-can-fail-its-build) | Hand-built images |
125
- | 111 | Auth | [Auth plugins look users up in batches](#auth-plugins-look-users-up-in-batches) | Custom plugins |
126
- | 112 | Worker | [Bundle states the template it needs](#canopycms-cdk-a-worker-bundle-states-the-template-it-needs--template-first-for-the-gate-only) | Template first |
127
- | 112 | Worker | [Poisoned `remote.git` re-clones](#a-poisoned-remotegit-re-clones-itself) | None |
128
- | 112 | Worker | [Nano worker boots reliably](#canopycms-cdk-a-t4gnano-worker-boots-reliably--behaviour-change-a-deploy-replaces-the-worker) | Deploy |
129
- | 112 | Worker | [Worker never pushes base or default branch](#the-worker-never-pushes-the-base-or-default-branch--security-fix) (114) | Protect base on GitHub |
130
- | 112 | Worker | [Worker log is root-owned](#canopycms-cdk-the-workers-log-is-root-owned--security-fix) | Deploy; hand-installed units |
131
- | 113 | Worker | [Push logs say whether GitHub moved](#the-workers-push-logs-say-whether-github-moved) | None |
132
- | 113 | Editing | [Unsaved ACL changes show; closing asks](#unsaved-group-and-permission-changes-show-and-closing-asks-first) | None |
133
- | 113 | Worker | [Credential in a per-command git config](#the-worker-hands-git-its-github-credential-in-a-per-command-file--security-fix-dev-behaviour-change) | Dev: proxy or CA in env |
134
- | 114 | Worker | [GitHub objects cross as bundles](#the-worker-moves-objects-between-github-and-remotegit-as-bundles) | None |
135
- | 114 | AI | [`noindex` entries leave AI content](#ai-content-leaves-out-noindex-entries--behaviour-change-for-ai-output) | `exclude.noindex: false` |
136
- | 114 | Worker | [Worker follows a rebased or deleted branch](#the-worker-follows-a-rebased-or-deleted-github-branch) | None |
137
- | 114 | Worker | [Worker holds no secret (contract 2)](#canopycms-cdk-the-worker-holds-no-secret--security-fix-template-first-contract-2) | Template first |
138
- | 115 | AI | [referenceLink decides where AI-export entry links point](#ai-content-can-withhold-or-redirect-a-link-to-an-entry-with-no-page) | Optional |
139
- | 115 | AI | [toPlainText no longer strips front matter](#toplaintext-takes-an-entry-body-and-keeps-prose-after-a-leading------behaviour-change) | Whole-file callers split first |
140
- | 115 | Auth | [Clerk users without a name no longer show their id](#people-without-a-name-show-name-unavailable) | None |
141
- | 115 | Branches | [Non-privileged callers get only their own branch ACL entries](#branch-access-lists-stay-with-admins-and-reviewers--behaviour-change) | None |
142
- | 115 | Branches | [managerOrAdminAllowed now refuses editors an access list names](#a-branch-lockdown-outranks-the-branchs-access-list--security-fix) | Optional |
143
- | 115 | Branches | [Conflict notice persists until merge](#the-base-branch-conflict-notice-stays-until-the-branch-merges--behaviour-change) | None |
144
- | 115 | Editing | [Breadcrumb root and file menu say "All content"](#the-editor-calls-the-content-drawer-all-content) | Editor UI tests |
145
- | 115 | Editing | [Branches drawer and comment replies](#the-branches-drawer-shows-one-action-per-branch-and-comments-reply-in-place) | Editor UI tests |
146
- | 115 | Editing | [Clear offers Undo; refusals and conflicts explain themselves](#an-objects-clear-offers-undo-and-refusals-say-why) | None |
147
- | 115 | Editing | [Collection dialog says what saves at once](#the-collection-dialog-says-what-saves-at-once) | Editor UI tests |
148
- | 115 | Editing | [Comments and branches name their people](#comments-and-branches-name-their-people-for-every-editor--behaviour-change-on-two-apis) | Direct API callers |
149
- | 115 | Editing | [Date picker, form header, wrapping toolbar](#a-date-picker-a-form-header-and-a-toolbar-that-wraps) | Editor UI tests |
150
- | 115 | Editing | [Editor colours reach contrast in both schemes](#editor-colours-reach-451-in-light-and-dark-mode) | Visual snapshot tests |
151
- | 115 | Editing | [Editor contrast; blocked buttons stay focusable](#editor-contrast-and-blocked-buttons-stay-focusable) | Browser tests |
152
- | 115 | Editing | [Focus ring, control borders, light badges, sentence-case badges](#editor-focus-rings-and-checked-controls-reach-31-for-any-brand) | Visual snapshot tests |
153
- | 115 | Editing | [Editor requests time out](#editor-requests-time-out-instead-of-hanging) | Slow-API browser tests |
154
- | 115 | Editing | [Field descriptions, list-card titles, `public/` images, continued](#field-editing-descriptions-list-card-titles-and-public-images-continued) | None |
155
- | 115 | Editing | [List items and blocks collapse](#list-items-and-blocks-collapse-and-reorder-from-one-menu) | Editor UI tests |
156
- | 115 | Editing | [Navigator hides "Add entry" where path permissions refuse it](#add-entry-follows-path-permissions-admin-panels-and-labels-tidied) | None |
157
- | 115 | Editing | [Quick open, navigator filter, title-first entries, media search](#quick-open-a-navigator-filter-title-first-new-entries-and-media-search) | Editor UI tests |
158
- | 115 | Editing | [Saving a deleted or moved entry is refused](#a-save-never-brings-back-a-deleted-or-moved-entry--behaviour-change) | None |
159
- | 115 | Editing | [Submit warns about unsaved entries; unanswered writes keep input](#submit-names-unsaved-entries-unanswered-writes-recover-more) | None |
160
- | 115 | Editing | [Unlabelled field names are humanised](#unlabelled-fields-and-block-templates-show-their-names-as-words) | Editor UI tests |
161
- | 115 | Ops | [Stable versions follow changesets; CHANGELOG.md ships](#stable-releases-follow-the-changes-they-carry-and-ship-their-changelog) | None |
162
- | 115 | Reading | [List items and blocks gain a persisted `_key`](#list-items-and-blocks-carry-a-_key-and-field-comments-follow-it--behaviour-change) | Rename fields named _key |
163
- | 115 | Schema | [Date new entries at their first submit](#stamponpublish-dates-a-new-entry-at-its-first-submit) | Optional |
164
-
165
- ### Preview URLs take one prefix, follow `trailingSlash`, and load each entry's own page — **breaking (env)**
56
+ | int | Area | Change | Action |
57
+ | --- | --- | --- | --- |
58
+ | 119 | Assets | [Library removal hides the file; grant the asset-hidden/ prefix](#removing-a-file-from-the-media-library-hides-it-and-an-admin-can-restore-it--breaking-assetsdelete-assetstore--behaviour-change) | Upgrade both; grant asset-hidden |
59
+ | 119 | Editing | [Content panel docks when stacked; overlay slides; matched dividers](#stacked-layout-docks-the-content-panel-beside-the-form) | None |
60
+ | 119 | Editing | [Editor opens the home page; singletons named by type](#the-editor-opens-the-home-page-and-names-single-entry-types-by-their-type--behaviour-change) | None |
61
+ | 119 | Editing | [Drawers share the content panel's compact header](#compact-drawer-headers-jumpable-comment-headings-and-a-steadier-owner-chip) | None |
62
+ | 119 | Editing | [Object Clear in a "…" menu; Undo stays inline](#an-objects-clear-moves-into-its--menu-with-an-inline-undo) | Editor UI tests |
63
+ | 119 | Editing | [Top bar: one unsaved dot, no "Editing" label](#the-top-bar-shows-one-unsaved-mark-and-a-status-only-when-it-matters--behaviour-change) | None |
64
+ | 119 | Preview | [createPreviewPage takes a layout for the site header and footer](#previews-can-show-the-site-header-and-footer-and-the-preview-stays-put-between-entries) | Optional |
65
+
66
+ ### Removing a file from the media library hides it, and an admin can restore it — **breaking (assets.delete, AssetStore) / behaviour change**
67
+
68
+ <!-- changeset: ef-g-media -->
69
+
70
+ _First in int.119._
71
+
72
+ **What changed.** "Remove from library" no longer deletes the file's metadata, so every image
73
+ size and crop keeps working wherever the file is used, including `materialize-assets`. It writes
74
+ a marker under the new `asset-hidden/` prefix instead. Admins can turn on "Show removed files" in
75
+ the media library and restore a file, and uploading the same file again restores it too.
76
+
77
+ **To adopt.**
78
+
79
+ - Upgrade `canopycms` and `canopycms-cdk` together.
80
+ - `assets.delete` and `DELETE /assets` are removed (`DELETE /assets` answers 410); use
81
+ `assets.hide` / `POST /assets/hide` and `assets.restore` / `POST /assets/restore`.
82
+ - A custom `AssetStore` drops `deleteMeta` and must implement `hideAsset` and `unhideAsset`; a
83
+ custom `AssetPrefixes` must set `hidden`.
84
+ - `AssetSupport.grantUploadAccess()` and `CanopyCmsService`'s `assetBucket` grant `s3:PutObject`
85
+ and `s3:DeleteObject` on `asset-hidden/*` and no longer grant delete on `asset-meta/*`. If you
86
+ write the CMS role's bucket policy yourself, do the same (no `s3:GetObject` is needed), and if
87
+ it scopes `s3:ListBucket` by prefix, include `asset-hidden/`.
88
+ - Files removed before this release lost their metadata. Uploading the same bytes again recreates
89
+ it, recorded under the re-uploader's name.
90
+
91
+ ### Stacked layout docks the content panel beside the form
92
+
93
+ <!-- changeset: ef-a-shell -->
94
+
95
+ _First in int.119._
96
+
97
+ **What changed.** In a wide window the content panel docks beside the form row in the stacked layout too, keeping the preview full width; in a narrow window it slides in over the form, unless the user prefers reduced motion. Both pane dividers share one width, and a branch banner has a line beneath it.
98
+
99
+ ### The editor opens the home page and names single-entry types by their type — **behaviour change**
100
+
101
+ <!-- changeset: ef-b-identity -->
102
+
103
+ _First in int.119._
104
+
105
+ **What changed.** With no entry in the URL, the editor opens the entry routed at `/`, else the first with a preview. An entry type limited to one entry (`maxItems: 1`) with a label is named by that label, though entry links and search still use its title. Content tree rows show the type on a second line, quick open shows type and route, "Rename…" is "Change URL…" ("Change slug…" for an entry with no page), and the "All content" crumb, the form header's title and the rail's site button are gone.
106
+
107
+ ### Compact drawer headers, jumpable comment headings and a steadier owner chip
108
+
109
+ <!-- changeset: ef-c-drawers -->
110
+
111
+ _First in int.119._
112
+
113
+ **What changed.** The Branches, Comments, Media and staged-changes drawers take the content panel's compact header with no subtitle. In the Comments drawer, each thread's heading is the one control that jumps to it. The Branches drawer no longer offers "New branch comment" or "+ New", since new branch comments start in the Comments drawer. A person's chip keeps its avatar and a long name on one line.
114
+
115
+ ### An object's Clear moves into its "…" menu, with an inline Undo
116
+
117
+ <!-- changeset: ef-d-clear -->
118
+
119
+ _First in int.119._
120
+
121
+ **What changed.** An optional object's Clear is now an item in a "…" menu on its label row, named
122
+ for the object ("Clear "SEO""). After a clear, the label row offers "Undo clear" in the menu's
123
+ place, with focus on it, until it can no longer put the object back where it was (the object was
124
+ filled in again, or the item holding it moved or went), another entry or branch opens, or the entry
125
+ is saved or its draft discarded. It has no timer, and Clear no longer raises a toast; list item and block removals keep
126
+ theirs. Their Undo, and the inline one, now survive edits to other items in an enclosing list.
127
+
128
+ **To adopt.** A UI test that clicked an object's "Clear" button now opens the object's
129
+ "Actions for …" menu and chooses the "Clear" item, then finds "Undo clear" in the label row instead of a toast.
130
+
131
+ ### The top bar shows one unsaved mark and a status only when it matters — **behaviour change**
132
+
133
+ <!-- changeset: ef-f-topbar -->
134
+
135
+ _First in int.119._
136
+
137
+ **What changed.** The branch pill's unsaved count is the same dot as the entry's, with a number, in the brand colour the content tree also uses. "Unsaved · kept on this device" moved into Save's tooltip and description; the bar shows it only as a warning when drafts could not be stored. The pill drops the default "Editing" status and shows In review, Approved, Archived or Protected. The "Read-only" chip no longer repeats a branch-wide lock the banner already explains; it stays for an entry read-only for its own reason.
138
+
139
+ ### Previews can show the site header and footer, and the preview stays put between entries
140
+
141
+ <!-- changeset: ef-e-preview -->
142
+
143
+ _First in int.119._
144
+
145
+ **What changed.** `createPreviewPage` takes an optional `layout`, a server component that wraps every view and receives `{ children, chrome }`. Given one, the preview toolbar offers a "Show site header and footer" toggle, remembered per device, which adds `?chrome=1` to the preview's URL and sets `chrome`. Without the toggle on, `chrome` is `false`. A page that never connects with them, such as one whose layout throws, is shown without them, and the toggle says so. Switching entries keeps the preview pane and its toolbar in place: the last page stays, dimmed, until the next entry's data arrives.
146
+
147
+ **To adopt.** Pass the component your public pages use for their header and footer:
148
+
149
+ ```tsx
150
+ export default createPreviewPage({
151
+ layout: ({ children, chrome }) =>
152
+ chrome ? <SiteChrome>{children}</SiteChrome> : <main>{children}</main>,
153
+ views: { post: PostPreview },
154
+ })
155
+ ```
156
+
157
+ A preview page built on `useCanopyPreview` alone can pass `siteChrome: true` when it renders the header and footer for `?chrome=1`.
158
+
159
+ ---
160
+
161
+ ## Released
162
+
163
+ ### 0.1.0
164
+
165
+ <!-- changeset: legacy-unreleased -->
166
+ <!-- changeset: nice-boxes-battle -->
167
+
168
+ #### Preview URLs take one prefix, follow `trailingSlash`, and load each entry's own page — **breaking (env)**
166
169
 
167
170
  **What changed.**
168
171
 
@@ -182,7 +185,7 @@ served at `/` without an `index` slug needs `'content/<slug>': '/'`, or re-model
182
185
  **Now deletable.** `previewBase` keys that only added a shared prefix or restate a root entry's own
183
186
  URL.
184
187
 
185
- ### The preview route and `/preview` entries — **breaking (imports)**
188
+ #### The preview route and `/preview` entries — **breaking (imports)**
186
189
 
187
190
  **What changed.**
188
191
 
@@ -208,7 +211,7 @@ URL.
208
211
  2. For a branch preview: wrap views with `withCanopyPreview` in a `'use client'` module, add
209
212
  `app/preview/[[...path]]/page.server.tsx`, set `editor.previewPrefix: '/preview'`, and serve that
210
213
  route with `frame-ancestors 'self'`, not `X-Frame-Options: DENY` (see
211
- [Live Preview](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#live-preview)).
214
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#live-preview)).
212
215
  3. A test reading view content from the route's server HTML must load the page in a browser.
213
216
  Server output rendered during a preview request carries the signed-in asset prefix; keep it out
214
217
  of cross-request caches (`unstable_cache`, module memos).
@@ -216,14 +219,14 @@ URL.
216
219
  **Now deletable.** A hand-built branch-preview route, and CSS layer or specificity workarounds that
217
220
  kept site styles ahead of Mantine's in the preview.
218
221
 
219
- ### Preview `fieldProps` is typed, with server-safe helpers — **breaking (types and schemas)**
222
+ #### Preview `fieldProps` is typed, with server-safe helpers — **breaking (types and schemas)**
220
223
 
221
224
  **What changed.** (int.109) `fieldProps` checks each path against the view's content type, and
222
225
  `canopycms` exports `FieldProps`, `FieldAttrs`, `fieldAttrs` and `scopeFieldProps`, callable from
223
226
  server components. A string path marks the form's spelling (`'a.0.b'` marks `a[0].b`), and an empty
224
227
  path marks nothing. The editor counts and logs marks naming no field. `createEntrySchemaRegistry`
225
228
  refuses a field name that is empty, all digits or contains `.`, `[` or `]`. See
226
- [Live Preview](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#live-preview).
229
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#live-preview).
227
230
 
228
231
  **To adopt.** Fix each path that stops compiling; the error lists valid ones. Rewrite computed paths
229
232
  as literal segments, or type that component's prop as plain `FieldProps`. Pass `fieldProps` as
@@ -233,7 +236,7 @@ as literal segments, or type that component's prop as plain `FieldProps`. Pass `
233
236
  `fieldProps` defaults; schema-walking tests for misspelled mark paths, once components are typed
234
237
  (the editor flags another block template's field).
235
238
 
236
- ### A reference resolves to its target, a restricted stub, or a missing stub — **security fix; breaking (types and build)**
239
+ #### A reference resolves to its target, a restricted stub, or a missing stub — **security fix; breaking (types and build)**
237
240
 
238
241
  **What changed.**
239
242
 
@@ -271,7 +274,7 @@ optional if it was required.
271
274
  content-integrity test checking reference fields against entry ids, and top-level-only reference
272
275
  fields kept for the preview's sake.
273
276
 
274
- ### AI content links a reference to its target — **behaviour change for `/ai` output and AI config callbacks**
277
+ #### AI content links a reference to its target — **behaviour change for `/ai` output and AI config callbacks**
275
278
 
276
279
  **What changed.** (int.106) A `reference` field renders as a link to its target, not the stored id:
277
280
  a byline reading `**Author:** 5NVkkrB1MJUv` now links the name `Alice` to that entry's page, then to
@@ -293,7 +296,7 @@ matching: search your AI config for each reference field's name.
293
296
 
294
297
  **Now deletable.** A `fieldTransforms` entry that looked up a reference id to print its title.
295
298
 
296
- ### AI content leaves out `noindex` entries — **behaviour change for `/ai` output**
299
+ #### AI content leaves out `noindex` entries — **behaviour change for `/ai` output**
297
300
 
298
301
  **What changed.** (int.114) AI content leaves out entries marked `noindex`, both from
299
302
  `generate-ai-content` and from the `/ai` route, using the same predicate as the sitemap and robots
@@ -305,11 +308,11 @@ or a failed render) renders as its plain text, with no URL; any other form of th
305
308
  fields are nested or renamed, pass the AI config's `seo` the same object you pass
306
309
  `createNextCanopyContext({ seo })`, so the sitemap and AI content read the flag from the same
307
310
  place. A field that links to a hidden section by plain URL needs a field transform; the README
308
- shows [the recipe](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#ai-content-configuration).
311
+ shows [the recipe](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#ai-content-configuration).
309
312
 
310
313
  **Now deletable.** An `exclude.where` that re-read the `noindex` flag by hand.
311
314
 
312
- ### Deleting a referenced entry asks first — **behaviour change on the delete API**
315
+ #### Deleting a referenced entry asks first — **behaviour change on the delete API**
313
316
 
314
317
  **What changed.** (int.106) Deleting an entry that other entries reference (by field or `entry:`
315
318
  link) returns 409 with `data.referencedBy`: the referencing entries the user may read, and a
@@ -318,7 +321,7 @@ link) returns 409 with `data.referencedBy`: the referencing entries the user may
318
321
 
319
322
  **To adopt.** Only scripts calling the delete endpoint: pass the flag or handle the 409.
320
323
 
321
- ### `mdx` content that runs code is refused at save — **breaking (behaviour)**
324
+ #### `mdx` content that runs code is refused at save — **breaking (behaviour)**
322
325
 
323
326
  **What changed.** (int.108) An `mdx` field or body refuses `{…}` expressions other than comments and
324
327
  plain values, `import`/`export`, and tags, attributes and URL schemes outside a safe set; other
@@ -327,7 +330,7 @@ A `markdown` field with `renderAs: 'mdx'` is checked as MDX, and `mdxAllow` narr
327
330
  field or site-wide. New: field options `renderAs`, `mdxAllow`; config key `mdxAllow`; type
328
331
  `MdxAllowlist`. (int.110) A component prop allowance can be `'string'` (or
329
332
  `{ type: 'string', maxLength }`): a quoted value only, never bare or `{…}`. See
330
- [MDX content cannot run code](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#mdx-content-cannot-run-code).
333
+ [MDX content cannot run code](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#mdx-content-cannot-run-code).
331
334
 
332
335
  **To adopt.** Set `renderAs: 'mdx'` on each `markdown` field your site compiles as MDX, and
333
336
  `mdxAllow` to what your renderer takes. Set `executable: true` only on a field whose editors you
@@ -339,7 +342,7 @@ build lists it.
339
342
  `javascript:` links in MDX or markdown rendered as MDX, and the path-prefix matching behind its
340
343
  entry-type gate. Keep a rule checking that the body compiles.
341
344
 
342
- ### A save rewrites only what changed
345
+ #### A save rewrites only what changed
343
346
 
344
347
  **What changed.**
345
348
 
@@ -359,7 +362,7 @@ entry-type gate. Keep a rule checking that the body compiles.
359
362
  **Now deletable.** A formatter pass or `.prettierignore` entry over CMS-written content that only
360
363
  undoes the editor's re-folding, restyling or date rewriting.
361
364
 
362
- ### The markdown editor runs MDXEditor 4.3
365
+ #### The markdown editor runs MDXEditor 4.3
363
366
 
364
367
  **What changed.** (int.105) `@mdxeditor/editor` is `^4.3.2` (Lexical 0.48), up from `^3.52.4`.
365
368
  Two-paragraph list items, multi-block quotes and code fences in any language edit in rich text.
@@ -367,7 +370,7 @@ Two-paragraph list items, multi-block quotes and code fences in any language edi
367
370
  **To adopt.** Nothing, unless your app depends on `@mdxeditor/editor` or `lexical` directly: align
368
371
  those versions so one copy loads.
369
372
 
370
- ### Unsaved group and permission changes show, and closing asks first
373
+ #### Unsaved group and permission changes show, and closing asks first
371
374
 
372
375
  **What changed.** (int.113) Manage Groups and Manage Permissions still save as one batch, but now
373
376
  mark unsaved changes, keep the save bar in view, and ask before closing or leaving drops them. The
@@ -375,7 +378,7 @@ group dialog's buttons read "Add" and "Apply", since neither saves.
375
378
 
376
379
  **To adopt.** Nothing.
377
380
 
378
- ### Field editing: descriptions, list-card titles, and `public/` images
381
+ #### Field editing: descriptions, list-card titles, and `public/` images
379
382
 
380
383
  **What changed.**
381
384
 
@@ -393,7 +396,7 @@ image, upload a raster copy to the media library. For card titles:
393
396
 
394
397
  **Now deletable.** Editor hints worked into a field's `label` because `description` never showed.
395
398
 
396
- ### Content reads: listings take a `branch`, and reads report `slug` and `urlPath`
399
+ #### Content reads: listings take a `branch`, and reads report `slug` and `urlPath`
397
400
 
398
401
  **What changed.**
399
402
 
@@ -406,7 +409,7 @@ branch.
406
409
 
407
410
  **Now deletable.** Code parsing a slug or URL back out of `path` or `entry.path`.
408
411
 
409
- ### Path-permission rules now take effect below the content root — **security fix; breaking for rules written with on-disk names**
412
+ #### Path-permission rules now take effect below the content root — **security fix; breaking for rules written with on-disk names**
410
413
 
411
414
  **What changed.** (int.93) Rules match an entry's logical path (`content/blog/my-post`). Enforcement
412
415
  checked the id-suffixed on-disk path, so collection and entry rules matched nothing; only a glob
@@ -419,7 +422,7 @@ extension in logical form (`content/blog/**`, `content/about`).
419
422
 
420
423
  **Now deletable.** A content-root grant working around inert collection grants.
421
424
 
422
- ### Reads never create a requested branch — **security fix; breaking for some direct `createContentReader` callers**
425
+ #### Reads never create a requested branch — **security fix; breaking for some direct `createContentReader` callers**
423
426
 
424
427
  **What changed.** (int.94) A branch other than the active one must exist and be readable by the
425
428
  user, or `read()`/`readByUrlPath()` treat it as not found; no workspace is created per `?branch=`
@@ -430,7 +433,7 @@ passes `allowCreateBranch: true`, never with a request's branch.
430
433
 
431
434
  **Now deletable.** Checks that allow-list `?branch=` before it reaches `read()`.
432
435
 
433
- ### Fast, crash-safe branch creation — **behaviour change: branch clones hold only the content root**
436
+ #### Fast, crash-safe branch creation — **behaviour change: branch clones hold only the content root**
434
437
 
435
438
  **What changed.** (int.100) A branch is built under a staging name and appears only when complete.
436
439
  The worker repairs branch directories an interrupted create or delete left wedged, and owns
@@ -444,7 +447,7 @@ changed `contentRoot` reaches existing clones within a worker cycle of the API r
444
447
  **Now deletable.** Manual EFS cleanup of provisioning locks or half-made branch directories, and
445
448
  `remote.git` gc scripts.
446
449
 
447
- ### `.canopy-meta/` must not be committed, and System health says so
450
+ #### `.canopy-meta/` must not be committed, and System health says so
448
451
 
449
452
  **What changed.** (int.93) The schema cache lives in each branch clone's `.git/canopycms/`. Sync,
450
453
  editor submits and `canopycms sync` ignore `.canopy-meta/`, and System health shows the base
@@ -458,13 +461,13 @@ repaired or re-created.
458
461
  **Now deletable.** Any local step that resets or reformats `.canopy-meta/` files before a commit or
459
462
  a format check.
460
463
 
461
- ### Commits name their editors, and submit refuses a branch with nothing to submit
464
+ #### Commits name their editors, and submit refuses a branch with nothing to submit
462
465
 
463
466
  **What changed.**
464
467
 
465
468
  - (int.93) Submit commits gain an `Edited-by: Name (id)` trailer; PR bodies gain a section that
466
469
  re-submits replace, keeping human text. Options: `gitEditedByTrailers`, `gitCoAuthoredByTrailers`
467
- ([reference](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#definecanopyconfig-options)).
470
+ ([reference](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#definecanopyconfig-options)).
468
471
  - (int.110) Submit-commit trailers name every user who saved since the last submit commit, and
469
472
  the PR-body section every user who edited the branch, not only the submitter; settings commits
470
473
  (permissions, groups) carry the acting user's trailer.
@@ -473,7 +476,7 @@ a format check.
473
476
 
474
477
  **To adopt.** Scripts calling submit: save a change first, or handle the 400.
475
478
 
476
- ### Settings saves only push the settings branch — **breaking (config): `autoCreateSettingsPR` is removed**
479
+ #### Settings saves only push the settings branch — **breaking (config): `autoCreateSettingsPR` is removed**
477
480
 
478
481
  **What changed.** (int.93) The orphan settings branch can never get a PR, so groups and permissions
479
482
  saves only push the branch, and no failing PR task lands in System health.
@@ -482,20 +485,20 @@ saves only push the branch, and no failing PR task lands in System health.
482
485
 
483
486
  **Now deletable.** Filters that hid the failed settings-PR tasks.
484
487
 
485
- ### A schema the running code lacks costs one entry type, and holds the worker — **behaviour change**
488
+ #### A schema the running code lacks costs one entry type, and holds the worker — **behaviour change**
486
489
 
487
490
  **What changed.** (int.107) When synced content names an entry schema the running code lacks, only
488
491
  that entry type goes unavailable: the editor says so, and its entries answer 503 with
489
492
  `code: 'SCHEMA_UNAVAILABLE'`. Builds, static deploys and `generate-ai-content` still fail. The
490
493
  worker holds the base branch for up to 30 minutes while that lasts; see
491
- [New schemas wait for the editor deploy](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#new-schemas-wait-for-the-editor-deploy).
494
+ [New schemas wait for the editor deploy](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#new-schemas-wait-for-the-editor-deploy).
492
495
  Public types gain optional `unavailable` on `EntryTypeConfig` and `FlatSchemaItem`, optional
493
496
  `ApiResponse.code`, and a required `SchemaResolutionResult.issues`.
494
497
 
495
498
  **To adopt.** Code that builds a `SchemaResolutionResult` adds `issues`. A custom worker entrypoint
496
499
  can pass `schemaHoldMaxMs` to `CmsWorker`.
497
500
 
498
- ### `canopycms-cdk`: the worker drains before replacement and runs on-demand — **breaking (props): `spotMaxPrice` is removed; behaviour and cost change**
501
+ #### `canopycms-cdk`: the worker drains before replacement and runs on-demand — **breaking (props): `spotMaxPrice` is removed; behaviour and cost change**
499
502
 
500
503
  **What changed.** (int.108) The worker is one on-demand `t4g.nano` by default (about $3 a month,
501
504
  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
@@ -512,7 +515,7 @@ drain applies from the deploy after this one.
512
515
 
513
516
  **Now deletable.** Any override stripping `InstanceMarketOptions` from the worker's launch template.
514
517
 
515
- ### `canopycms-cdk`: the worker instance is hardened — **an existing stack upgrades in two deploys; behaviour and cost change**
518
+ #### `canopycms-cdk`: the worker instance is hardened — **an existing stack upgrades in two deploys; behaviour and cost change**
516
519
 
517
520
  **Upgrade an existing stack in two deploys:**
518
521
 
@@ -526,7 +529,7 @@ hang until the drain heartbeat lets termination continue. Its in-flight task is
526
529
  new stack needs no steps.
527
530
 
528
531
  **What changed.** (int.109) Detailed in
529
- [The worker instance](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#the-worker-instance): IMDSv2 with a hop limit of 1; the
532
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#the-worker-instance): IMDSv2 with a hop limit of 1; the
530
533
  bundle is one file whose sha256 user data checks, readable as that one object; a file-system policy
531
534
  refusing clients without TLS or IAM, replacing the worker's
532
535
  `AmazonElasticFileSystemClientReadWriteAccess`; an encrypted gp3 root volume; a boot-time upgrade to
@@ -542,7 +545,7 @@ key, grant the Auto Scaling service-linked role on it. Pass `workerMaxInstanceLi
542
545
  **Now deletable.** Any override adding `MetadataOptions`, an encrypted root volume or an EFS
543
546
  `FileSystemPolicy`, or narrowing the worker role's asset-bucket grant.
544
547
 
545
- ### `canopycms-cdk`: the worker needs a state directory — **deploy the template before the bundle**
548
+ #### `canopycms-cdk`: the worker needs a state directory — **deploy the template before the bundle**
546
549
 
547
550
  **What changed.** (int.110) The worker's git keeps the GitHub credential in a private mirror
548
551
  under the unit's `StateDirectory=`, and the worker does not start without it.
@@ -552,14 +555,14 @@ new template before CI rolls an int.110 or later bundle; a parameter-only change
552
555
  the new unit, so the new bundle exits at start. A hand-installed unit needs
553
556
  `StateDirectory=canopy-worker` under `[Service]`.
554
557
 
555
- ### A poisoned `remote.git` re-clones itself
558
+ #### A poisoned `remote.git` re-clones itself
556
559
 
557
560
  **What changed.** (int.112) A `remote.git` missing its base branch is re-cloned unless an unpushed ref would be
558
561
  lost; the refusal names those refs. A tampered config still refuses and needs an operator.
559
562
 
560
563
  **To adopt.** Nothing.
561
564
 
562
- ### A failed or stopped worker says why — **behaviour change on the not-ready 503; new worker APIs**
565
+ #### A failed or stopped worker says why — **behaviour change on the not-ready 503; new worker APIs**
563
566
 
564
567
  **What changed.** (int.109) While the latest start's recorded failure stands, the prod not-ready 503
565
568
  is `WORKER_FAILED`: no `Retry-After`, the failure named to admins, account ids masked.
@@ -571,7 +574,7 @@ failure and exits non-zero on `selfStopped`, with a code outside `RestartPrevent
571
574
 
572
575
  **Now deletable.** A watchdog that restarts an idle worker process.
573
576
 
574
- ### `canopycms-cdk`: CI can roll the worker with a parameter, and alarm when it stops syncing — **new, opt-in**
577
+ #### `canopycms-cdk`: CI can roll the worker with a parameter, and alarm when it stops syncing — **new, opt-in**
575
578
 
576
579
  **What changed.** (int.109)
577
580
 
@@ -580,28 +583,28 @@ failure and exits non-zero on `selfStopped`, with a code outside `RestartPrevent
580
583
  with its hash, as `worker/dist/index.js` and `index.js.sha256`. The default, `'asset'`, is
581
584
  unchanged.
582
585
  - `CanopyCmsService` alarms when the worker logs no git sync for 30 minutes. See
583
- [Worker-down alarm](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#worker-down-alarm).
586
+ [Worker-down alarm](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#worker-down-alarm).
584
587
 
585
588
  **To adopt.** Optional. For parameter-only change sets, see
586
- [Rolling the worker from CI](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#rolling-the-worker-from-ci). For the alarm, pass
589
+ [Rolling the worker from CI](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#rolling-the-worker-from-ci). For the alarm, pass
587
590
  `alarmTopic`.
588
591
 
589
592
  **Now deletable.** A manual `cdk deploy` after each canopycms bump whose only purpose is moving the
590
593
  worker, and a hand-built alarm on the worker log group.
591
594
 
592
- ### `canopycms-cdk`: a `t4g.nano` worker boots reliably — **behaviour change: a deploy replaces the worker**
595
+ #### `canopycms-cdk`: a `t4g.nano` worker boots reliably — **behaviour change: a deploy replaces the worker**
593
596
 
594
597
  **What changed.** (int.112) The boot's `dnf upgrade` could be OOM-killed on every retry. A 1 GiB
595
598
  swap file now goes on first; an upgrade that still fails starts the worker unpatched and notifies
596
599
  `alarmTopic` if set; a retried step names itself when it gives up. See
597
- [The worker instance](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#the-worker-instance).
600
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#the-worker-instance).
598
601
 
599
602
  **To adopt.** Deploy, which replaces the worker instance. The bundle needs none of this, so it is
600
603
  not Template first and the worker contract stays at 1.
601
604
 
602
605
  **Now deletable.** An instance type above `t4g.nano` chosen only to get the boot through.
603
606
 
604
- ### `canopycms-cdk`: the worker's log is root-owned — **security fix**
607
+ #### `canopycms-cdk`: the worker's log is root-owned — **security fix**
605
608
 
606
609
  **What changed.** (int.112) `/var/log/canopy-worker` and `worker.log` are root-owned, and the unit
607
610
  drops `LogsDirectory=`. systemd opens the log as root, following symlinks, before the sandbox
@@ -619,7 +622,7 @@ contract is unchanged. A hand-installed unit needs the same change:
619
622
 
620
623
  **Now deletable.** Nothing.
621
624
 
622
- ### The worker never pushes the base or default branch — **security fix**
625
+ #### The worker never pushes the base or default branch — **security fix**
623
626
 
624
627
  **What changed.** (int.112) The worker refuses to push, plainly or replacing history, to the base
625
628
  branch or GitHub's default branch, whatever a task asks, and fails the task permanently.
@@ -635,14 +638,14 @@ its bypass list. Set `CANOPYCMS_BASE_BRANCH` (the construct's `baseBranch`) expl
635
638
  it, the gateway protects only GitHub's default branch and your list; the worker still refuses the
636
639
  base it detected, warning when that base is neither.
637
640
 
638
- ### The worker's push logs say whether GitHub moved
641
+ #### The worker's push logs say whether GitHub moved
639
642
 
640
643
  **What changed.** (int.113) `Pushed … to GitHub` is logged only when the branch moved, saying
641
644
  where from; an unmoved branch logs at debug level. An unchanged settings branch is not pushed.
642
645
 
643
646
  **To adopt.** Nothing.
644
647
 
645
- ### The worker hands git its GitHub credential in a per-command file — **security fix; dev behaviour change**
648
+ #### The worker hands git its GitHub credential in a per-command file — **security fix; dev behaviour change**
646
649
 
647
650
  **What changed.** (int.113) The token leaves git URLs: each GitHub fetch and push reads it from a
648
651
  `0600` file named by `GIT_CONFIG_GLOBAL`, deleted afterwards. For those commands the file replaces
@@ -652,7 +655,7 @@ stops applying and no keychain credential is tried.
652
655
  **To adopt.** Nothing in production. In dev, move a global-config proxy or CA to `HTTPS_PROXY` or
653
656
  `GIT_SSL_CAINFO`.
654
657
 
655
- ### The worker moves objects between GitHub and `remote.git` as bundles
658
+ #### The worker moves objects between GitHub and `remote.git` as bundles
656
659
 
657
660
  **What changed.** (int.114) The worker moves objects between GitHub and
658
661
  `remote.git` as git bundles. A new instance's first sync fetches the whole repository from GitHub,
@@ -660,9 +663,9 @@ and operations may reuse GitHub fetches under 30 seconds old.
660
663
 
661
664
  **To adopt.** Nothing. Seeding `remote.git` stages a copy of the repository beside the gateway's
662
665
  mirror, so the root volume needs room for two
663
- ([details](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#the-worker-instance)).
666
+ ([details](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#the-worker-instance)).
664
667
 
665
- ### The worker follows a rebased or deleted GitHub branch
668
+ #### The worker follows a rebased or deleted GitHub branch
666
669
 
667
670
  **What changed.** (int.114) When a branch is rebased and force-pushed on GitHub, or deleted there, the
668
671
  worker now updates or deletes its own copy of it instead of warning about it on every sync. A rebased
@@ -671,7 +674,7 @@ health lists it with what to do.
671
674
 
672
675
  **To adopt.** Nothing.
673
676
 
674
- ### `canopycms-cdk`: a worker bundle states the template it needs — **template first for the gate only**
677
+ #### `canopycms-cdk`: a worker bundle states the template it needs — **template first for the gate only**
675
678
 
676
679
  **What changed.** (int.112) The worker unit carries a contract version
677
680
  (`Environment=CANOPYCMS_WORKER_CONTRACT=<n>`). Parameter mode outputs it as `WorkerContract`, and
@@ -682,13 +685,13 @@ changes the template too (the fallback bundle's key and hash), so only the contr
682
685
  bundle-only roll is safe.
683
686
 
684
687
  **To adopt.** Parameter mode: gate bundle-only rolls on the contract
685
- ([recipe](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#rolling-the-worker-from-ci)). The gate refuses a stack without the
688
+ ([recipe](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#rolling-the-worker-from-ci)). The gate refuses a stack without the
686
689
  `WorkerContract` output, so `cdk deploy` the template once, which adds it. A hand-installed unit
687
690
  copies its `Environment=CANOPYCMS_WORKER_CONTRACT=` line from `worker/canopy-worker.service`.
688
691
 
689
692
  **Now deletable.** Deciding from the template diff whether a bundle-only roll is safe.
690
693
 
691
- ### `canopycms-cdk`: the worker holds no secret — **security fix; template first (contract 2)**
694
+ #### `canopycms-cdk`: the worker holds no secret — **security fix; template first (contract 2)**
692
695
 
693
696
  **What changed.** (int.114) The worker runs with no network and no secret. `canopy-github`, a gateway
694
697
  service under its own user with no view of EFS, holds the GitHub credential and the Clerk key, and
@@ -699,10 +702,10 @@ units from the bundle's contract: 2 gets the split, an older bundle the single u
699
702
  template, then roll the bundle; on an older template it stops with `template too old for this
700
703
  bundle`. Inspect EFS only through `sudo /opt/canopy-worker/canopy-inspect <command>`, never from a root shell. A hand install
701
704
  takes `worker/`'s three units and `gateway.env.example`
702
- ([steps](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#installing-the-units-by-hand)). After the first deploy, run the
703
- [checklist](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#first-deploy-checklist).
705
+ ([steps](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#installing-the-units-by-hand)). After the first deploy, run the
706
+ [checklist](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#first-deploy-checklist).
704
707
 
705
- ### `canopycms-cdk`: `CanopyCmsService.attachTo`, and editor response headers — **behaviour change if you frame the CMS**
708
+ #### `canopycms-cdk`: `CanopyCmsService.attachTo`, and editor response headers — **behaviour change if you frame the CMS**
706
709
 
707
710
  **What changed.** (int.94) `cmsService.attachTo(distribution, { viewerRequestFunction?, behaviorOverrides? })`
708
711
  adds the editor's behaviors (`/edit`, `/edit/*`, `/api/canopycms/*`) to a distribution you already
@@ -711,7 +714,7 @@ response headers policy. `CanopyCmsDistribution` sends the same headers: `frame-
711
714
  `X-Frame-Options: SAMEORIGIN`, `nosniff`, HSTS and `X-Robots-Tag: noindex`. Unless your app sends
712
715
  its own framing headers, no other origin can frame the editor or any page the CMS domain serves. Every such page is
713
716
  marked `noindex`. The editor's default preview is same-origin and unaffected. See
714
- [Serving the editor from a distribution you already own](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
717
+ [Serving the editor from a distribution you already own](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
715
718
 
716
719
  **To adopt.** If you wired the Function URL into your own distribution by hand, delete those
717
720
  behaviors and call `attachTo`, passing your CMS build's `assetPrefix` as `editorAssetPrefix` and a
@@ -723,12 +726,12 @@ gate, and read the synth warning if your distribution has custom error responses
723
726
  `x-forwarded-host` function made for them, and any response-headers policy added only to stop
724
727
  framing.
725
728
 
726
- ### `unauthenticatedStatus`: answer signed-out API calls with 419
729
+ #### `unauthenticatedStatus`: answer signed-out API calls with 419
727
730
 
728
731
  **To adopt.** (int.102) Set `unauthenticatedStatus: 419` if your pages sit behind HTTP Basic auth on
729
732
  the editor's origin; the editor still detects sign-out. Otherwise nothing.
730
733
 
731
- ### Auth plugins look users up in batches
734
+ #### Auth plugins look users up in batches
732
735
 
733
736
  **What changed.** (int.111) `AuthPlugin` gains an optional `getUsersMetadata(userIds)`, and the
734
737
  editor batches its user-badge lookups.
@@ -736,7 +739,7 @@ editor batches its user-badge lookups.
736
739
  **To adopt.** Nothing. A custom auth plugin may implement it; otherwise the server falls back to
737
740
  bounded single lookups.
738
741
 
739
- ### The CMS image builds a prod editor, and a mode mismatch blocks the editor — **behaviour change: a hand-built image can fail its build**
742
+ #### The CMS image builds a prod editor, and a mode mismatch blocks the editor — **behaviour change: a hand-built image can fail its build**
740
743
 
741
744
  **What changed.** (int.111) `Dockerfile.cms` defaults `NEXT_PUBLIC_CANOPY_MODE` to `prod`, not
742
745
  `dev`, and fails its build on any value but `prod` or `dev`. An editor built for the other mode than its
@@ -745,7 +748,7 @@ server runs now gets a blocking screen naming that variable, not a sign-in that
745
748
  **To adopt.** A hand-built image running a dev-mode server passes
746
749
  `--build-arg NEXT_PUBLIC_CANOPY_MODE=dev`.
747
750
 
748
- ### Prod detects an unset `defaultBaseBranch` instead of assuming `main` — **behaviour change: startup can fail**
751
+ #### Prod detects an unset `defaultBaseBranch` instead of assuming `main` — **behaviour change: startup can fail**
749
752
 
750
753
  **What changed.** (int.110) Unset, prod reads the base branch from the HEAD of the workspace's `remote.git`,
751
754
  which the worker points at the branch it uses. Before the worker creates `remote.git`, requests
@@ -759,7 +762,7 @@ does not follow a later change of GitHub's default branch, so set it before a cu
759
762
 
760
763
  **Now deletable.** A `defaultBaseBranch: 'main'` in a repository whose default branch is `main`.
761
764
 
762
- ### The prod workspace defaults to `/mnt/efs` — **behaviour change if you run prod without `CANOPYCMS_WORKSPACE_ROOT`**
765
+ #### The prod workspace defaults to `/mnt/efs` — **behaviour change if you run prod without `CANOPYCMS_WORKSPACE_ROOT`**
763
766
 
764
767
  **What changed.** (int.100) With `CANOPYCMS_WORKSPACE_ROOT` unset, prod mode keeps its branches,
765
768
  settings, task queue and auth cache under `/mnt/efs` instead of `/mnt/efs/workspace`
@@ -770,7 +773,7 @@ the worker, so CDK deployments are unaffected.
770
773
  `/mnt/efs/workspace` to keep your existing workspace, or move that directory's contents up to
771
774
  `/mnt/efs`.
772
775
 
773
- ### System health shows which build is running
776
+ #### System health shows which build is running
774
777
 
775
778
  **What changed.** (int.94) System health gains a Build section (canopycms version, source revision,
776
779
  worker version, media-storage state) and warns when API and worker versions differ.
@@ -783,7 +786,7 @@ worker version, media-storage state) and warns when API and worker versions diff
783
786
 
784
787
  **Now deletable.** Hand-rolled version or commit stamping, or a build-info endpoint.
785
788
 
786
- ### The AWS example workflow gains the template's triggers and dependency checks
789
+ #### The AWS example workflow gains the template's triggers and dependency checks
787
790
 
788
791
  **What changed.** (int.111) Like the one `init-deploy aws` writes, `examples/aws-deployment/deploy-cms.yml`
789
792
  now deploys on `next.config.*`, `middleware.ts` and `public/**` changes, and checks `canopycms` and
@@ -791,7 +794,7 @@ now deploys on `next.config.*`, `middleware.ts` and `public/**` changes, and che
791
794
 
792
795
  **To adopt.** If you copied it by hand, add those `paths:` and packages.
793
796
 
794
- ### `GET /admin/branch-health` scans for duplicate content IDs only on request — **behaviour change**
797
+ #### `GET /admin/branch-health` scans for duplicate content IDs only on request — **behaviour change**
795
798
 
796
799
  **What changed.** (int.111) The duplicate-ID scan runs only with `?duplicates=1`, under a 20 s
797
800
  budget. Each healthy entry then carries `duplicateIdScan` (`none`, `found` or `unknown`), replacing
@@ -799,7 +802,7 @@ budget. Each healthy entry then carries `duplicateIdScan` (`none`, `found` or `u
799
802
 
800
803
  **To adopt.** Scripts reading `duplicateContentIds`: pass `duplicates=1`, read `duplicateIdScan`.
801
804
 
802
- ### `canopycms-cdk`: `AssetSupport` serves images from S3 only — **breaking**
805
+ #### `canopycms-cdk`: `AssetSupport` serves images from S3 only — **breaking**
803
806
 
804
807
  **What changed.** (int.100) `/assets/*` and `/assets/t/*` come from the bucket alone, with an
805
808
  optional `replicaBucket` for 5xx failover. An unmaterialized URL is a 403; no transform Lambda or
@@ -816,7 +819,7 @@ opt-in and those props, and pass `replicaBucket` if you replicate `assets/`.
816
819
 
817
820
  **Now deletable.** Your `assets/t/` expiry rule, before leaving lazy mode.
818
821
 
819
- ### `collect-asset-refs` and `materialize-assets` store a build's images before release
822
+ #### `collect-asset-refs` and `materialize-assets` store a build's images before release
820
823
 
821
824
  **What changed.** (int.100) `canopycms collect-asset-refs <outDir>` writes the image keys a static
822
825
  build references to `<outDir>/canopy-asset-refs.json`. `canopycms materialize-assets --refs <file>`
@@ -831,7 +834,7 @@ Run `materialize-assets` before the release is served, with the S3 grants the RE
831
834
 
832
835
  **Now deletable.** A pre-release warm-up step that requests image URLs to get them transformed.
833
836
 
834
- ### The editor and its live preview load images through the signed-in asset route
837
+ #### The editor and its live preview load images through the signed-in asset route
835
838
 
836
839
  **What changed.** (int.100) The editor loads asset-store images from
837
840
  `GET /api/canopycms/assets/raw/…` under your `basePath`, which transforms on demand and, on S3,
@@ -848,7 +851,7 @@ same-origin or set `lazyPublicTransforms: true`.
848
851
 
849
852
  **Now deletable.** Code passing `assetBaseUrl` into the editor.
850
853
 
851
- ### `assetUrl` applies an image value's crop — **behaviour change for cropped images**
854
+ #### `assetUrl` applies an image value's crop — **behaviour change for cropped images**
852
855
 
853
856
  **What changed.** (int.100) `assetUrl`/`assetSrcSet` apply an `image` value's `crop`; `opts.crop`
854
857
  overrides.
@@ -857,7 +860,7 @@ overrides.
857
860
 
858
861
  **Now deletable.** Copying `value.crop` into `opts.crop`.
859
862
 
860
- ### `media.publicBaseUrl` is removed, and image limits are wider — **breaking (config)**
863
+ #### `media.publicBaseUrl` is removed, and image limits are wider — **breaking (config)**
861
864
 
862
865
  **What changed.** (int.100) Nothing read `media.publicBaseUrl`, and the strict `media` schema fails a
863
866
  config that sets it. Uploads go up to 24 MP (6000×4000), up from 16.7 MP. Widths are any integer up
@@ -869,9 +872,7 @@ asset URLs.
869
872
 
870
873
  **Now deletable.** Anything that only fed `publicBaseUrl`, and rounding widths to a multiple of 160.
871
874
 
872
- <!-- legacy:end -->
873
-
874
- ### AI content can withhold or redirect a link to an entry with no page
875
+ #### AI content can withhold or redirect a link to an entry with no page
875
876
 
876
877
  <!-- changeset: ai-reference-link-hook -->
877
878
 
@@ -889,12 +890,12 @@ one the exclusions leave out. A throw or any other return is logged and treated
889
890
  **To adopt.** If your site's routes, sitemap or bylines decide that some entries have no page,
890
891
  apply the same rule here, for example `referenceLink: (target, { body }) =>
891
892
  !body?.trim() ? false : undefined` in `defineAIContentConfig` from `canopycms/ai`. See
892
- [AI Content Configuration](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#ai-content-configuration).
893
+ [AI Content Configuration](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#ai-content-configuration).
893
894
 
894
895
  **Now deletable.** A `fieldTransforms` entry that re-renders a reference field only to drop or
895
896
  change its link.
896
897
 
897
- ### `toPlainText` takes an entry body and keeps prose after a leading `---` — **behaviour change**
898
+ #### `toPlainText` takes an entry body and keeps prose after a leading `---` — **behaviour change**
898
899
 
899
900
  <!-- changeset: to-plain-text-body-only -->
900
901
 
@@ -918,7 +919,7 @@ import { toPlainText } from 'canopycms/ai'
918
919
  const text = toPlainText(matter(file, {}).content)
919
920
  ```
920
921
 
921
- ### People without a name show "Name unavailable"
922
+ #### People without a name show "Name unavailable"
922
923
 
923
924
  <!-- changeset: people-without-a-name-show-name-unavailable -->
924
925
 
@@ -926,7 +927,7 @@ _First in int.115._
926
927
 
927
928
  **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.
928
929
 
929
- ### Branch access lists stay with admins and reviewers — **behaviour change**
930
+ #### Branch access lists stay with admins and reviewers — **behaviour change**
930
931
 
931
932
  <!-- changeset: branch-access-list-withheld-from-non-privileged -->
932
933
 
@@ -938,7 +939,7 @@ access updates) sends a caller who is not an admin or reviewer only the `access.
938
939
  `withheldAccess` field. A branch card's access list shows the people the viewer can name, then
939
940
  "and N others", instead of a row of "Name unavailable" avatars.
940
941
 
941
- ### A branch lockdown outranks the branch's access list — **security fix**
942
+ #### A branch lockdown outranks the branch's access list — **security fix**
942
943
 
943
944
  <!-- changeset: branch-lockdown-outranks-branch-acl -->
944
945
 
@@ -954,7 +955,7 @@ the editor's Submit and Withdraw controls follow the same rule.
954
955
  branch: those editors lose access. To give it back, set `managerOrAdminAllowed` to `false` in
955
956
  that branch's `branch.json`, and the access list decides again.
956
957
 
957
- ### The base-branch conflict notice stays until the branch merges — **behaviour change**
958
+ #### The base-branch conflict notice stays until the branch merges — **behaviour change**
958
959
 
959
960
  <!-- changeset: conflict-notice-persists-until-merge -->
960
961
 
@@ -964,10 +965,10 @@ _First in int.115._
964
965
  entry the base branch also changed, the "updated on the base branch" notice and the branch's
965
966
  conflict count now stay until the branch merges, so a reviewer still sees which entries replaced a
966
967
  base change. They used to clear on the next sync, about five minutes later. README's new [Drafts
967
- and Unpublishing](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#drafts-and-unpublishing) section covers long-lived branches and
968
+ and Unpublishing](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#drafts-and-unpublishing) section covers long-lived branches and
968
969
  unpublishing.
969
970
 
970
- ### The editor calls the content drawer "All content"
971
+ #### The editor calls the content drawer "All content"
971
972
 
972
973
  <!-- changeset: all-content-label -->
973
974
 
@@ -977,7 +978,7 @@ _First in int.115._
977
978
 
978
979
  **To adopt.** Nothing, unless your browser tests find either by the text "All entries".
979
980
 
980
- ### The Branches drawer shows one action per branch, and comments reply in place
981
+ #### The Branches drawer shows one action per branch, and comments reply in place
981
982
 
982
983
  <!-- changeset: branches-drawer-and-comment-replies -->
983
984
 
@@ -991,7 +992,7 @@ form has a "Field comments" button, so tests that find buttons by `/comment/i` m
991
992
  one; and the editor listens for Ctrl+Alt+M (⌘⌥M on macOS) on the whole document, taking it while a
992
993
  form is open.
993
994
 
994
- ### An object's Clear offers Undo, and refusals say why
995
+ #### An object's Clear offers Undo, and refusals say why
995
996
 
996
997
  <!-- changeset: clear-undo-and-clearer-refusals -->
997
998
 
@@ -1006,7 +1007,7 @@ conflict in yellow; the API marks it with the new error code `SETTINGS_CHANGED`.
1006
1007
  `sanitizeHref` returns its fallback for `undefined` or `null`, which a live preview's draft can
1007
1008
  pass for a link field it doesn't hold yet, instead of throwing.
1008
1009
 
1009
- ### The collection dialog says what saves at once
1010
+ #### The collection dialog says what saves at once
1010
1011
 
1011
1012
  <!-- changeset: collection-dialog-says-what-saves -->
1012
1013
 
@@ -1019,7 +1020,7 @@ inline instead of as toasts.
1019
1020
 
1020
1021
  **To adopt.** Nothing, unless browser tests click the old buttons or wait for those toasts.
1021
1022
 
1022
- ### Comments and branches name their people for every editor — **behaviour change on two APIs**
1023
+ #### Comments and branches name their people for every editor — **behaviour change on two APIs**
1023
1024
 
1024
1025
  <!-- changeset: comments-and-branches-name-their-people -->
1025
1026
 
@@ -1032,7 +1033,7 @@ Your auth plugin answers through the `/users/batch` cache; a failed lookup leave
1032
1033
 
1033
1034
  **To adopt.** Nothing, unless you call either API yourself: read `data.users`.
1034
1035
 
1035
- ### A date picker, a form header, and a toolbar that wraps
1036
+ #### A date picker, a form header, and a toolbar that wraps
1036
1037
 
1037
1038
  <!-- changeset: date-picker-form-header-wrapping-toolbar -->
1038
1039
 
@@ -1046,7 +1047,7 @@ editor styles.
1046
1047
 
1047
1048
  **To adopt.** Nothing. Browser tests that typed into a `datetime-local` input pick a date instead.
1048
1049
 
1049
- ### Editor colours reach 4.5:1 in light and dark mode
1050
+ #### Editor colours reach 4.5:1 in light and dark mode
1050
1051
 
1051
1052
  <!-- changeset: editor-colours-reach-contrast-in-both-schemes -->
1052
1053
 
@@ -1068,7 +1069,7 @@ defaults.
1068
1069
  **Now deletable.** Local overrides that darkened light buttons, badges, placeholders or input
1069
1070
  borders for contrast.
1070
1071
 
1071
- ### Editor contrast, and blocked buttons stay focusable
1072
+ #### Editor contrast, and blocked buttons stay focusable
1072
1073
 
1073
1074
  <!-- changeset: editor-contrast-and-focusable-blocked-buttons -->
1074
1075
 
@@ -1087,7 +1088,7 @@ old "Reload File" or "Discard File Draft" use the new labels.
1087
1088
 
1088
1089
  **Now deletable.** Local overrides that darkened the same text or buttons.
1089
1090
 
1090
- ### Editor focus rings and checked controls reach 3:1 for any brand
1091
+ #### Editor focus rings and checked controls reach 3:1 for any brand
1091
1092
 
1092
1093
  <!-- changeset: editor-focus-ring-and-control-contrast -->
1093
1094
 
@@ -1106,7 +1107,7 @@ their tint instead of the primary colour, and Markdown popups have a border in d
1106
1107
  text, such as the System Health panel's "corrupt metadata", now "Corrupt metadata". A
1107
1108
  `themeOverride` that sets `primaryShade` moves where the ring starts.
1108
1109
 
1109
- ### Editor requests time out instead of hanging
1110
+ #### Editor requests time out instead of hanging
1110
1111
 
1111
1112
  <!-- changeset: editor-requests-time-out -->
1112
1113
 
@@ -1122,7 +1123,7 @@ write in flight says why.
1122
1123
  **To adopt.** Nothing, unless browser tests hold an API response for longer than a deadline to
1123
1124
  observe a busy state; keep such holds under it.
1124
1125
 
1125
- ### Field editing: descriptions, list-card titles, and public/ images, continued
1126
+ #### Field editing: descriptions, list-card titles, and public/ images, continued
1126
1127
 
1127
1128
  <!-- changeset: list-card-titles-and-block-descriptions -->
1128
1129
 
@@ -1135,7 +1136,7 @@ takes its title from its first non-blank `string` field, then "Item N" (or its `
1135
1136
  "Team member N"). A block template's `description` now renders; see
1136
1137
  [List items and blocks collapse](#list-items-and-blocks-collapse-and-reorder-from-one-menu).
1137
1138
 
1138
- ### List items and blocks collapse, and reorder from one menu
1139
+ #### List items and blocks collapse, and reorder from one menu
1139
1140
 
1140
1141
  <!-- changeset: list-items-and-blocks-collapse -->
1141
1142
 
@@ -1152,7 +1153,7 @@ a second value) or "Add section".
1152
1153
 
1153
1154
  **Now deletable.** Template labels that repeated the template's description.
1154
1155
 
1155
- ### "Add entry" follows path permissions; admin panels and labels tidied
1156
+ #### "Add entry" follows path permissions; admin panels and labels tidied
1156
1157
 
1157
1158
  <!-- changeset: navigator-add-follows-path-access-and-ux-batch2-rest -->
1158
1159
 
@@ -1167,7 +1168,7 @@ labels such as "All entries", "Manage permissions" and "Create group" are now in
1167
1168
  Discarding an entry's changes toasts only for a read-only branch's hidden draft, where the form shows
1168
1169
  no change.
1169
1170
 
1170
- ### Quick open, a navigator filter, title-first new entries, and media search
1171
+ #### Quick open, a navigator filter, title-first new entries, and media search
1171
1172
 
1172
1173
  <!-- changeset: quick-open-navigator-filter-media-search -->
1173
1174
 
@@ -1183,7 +1184,7 @@ library searches, sorts and filters all its files, in a grid or a list.
1183
1184
  **To adopt.** Nothing, unless browser tests add an entry by typing only a slug: fill in "Title"
1184
1185
  too when the entry type has one.
1185
1186
 
1186
- ### A save never brings back a deleted or moved entry — **behaviour change**
1187
+ #### A save never brings back a deleted or moved entry — **behaviour change**
1187
1188
 
1188
1189
  <!-- changeset: save-never-recreates-deleted-entry -->
1189
1190
 
@@ -1196,7 +1197,7 @@ its new name with the changes still in the form, ready to save again. For a dele
1196
1197
  the changes and offers "Save as new entry", a deliberate create. This also covers an editor's own
1197
1198
  delete or rename that got no answer. Creating an entry is unchanged.
1198
1199
 
1199
- ### Submit names unsaved entries; unanswered writes recover more
1200
+ #### Submit names unsaved entries; unanswered writes recover more
1200
1201
 
1201
1202
  <!-- changeset: submit-warns-unsaved-and-unanswered-writes-recover -->
1202
1203
 
@@ -1212,7 +1213,7 @@ out). The save-outcome toasts name their
1212
1213
  entry. A lookup sent as a POST that times out says to try again. Removing a staged entry type
1213
1214
  from a new collection goes at once, with Undo in the dialog.
1214
1215
 
1215
- ### Unlabelled fields and block templates show their names as words
1216
+ #### Unlabelled fields and block templates show their names as words
1216
1217
 
1217
1218
  <!-- changeset: unlabelled-fields-read-as-words -->
1218
1219
 
@@ -1225,7 +1226,7 @@ block picker and the Remove toast.
1225
1226
  **To adopt.** Nothing, unless your browser tests find such a field by its raw name. Give it a
1226
1227
  `label` to choose the words yourself.
1227
1228
 
1228
- ### Stable releases follow the changes they carry, and ship their changelog
1229
+ #### Stable releases follow the changes they carry, and ship their changelog
1229
1230
 
1230
1231
  <!-- changeset: stable-releases-from-changesets -->
1231
1232
 
@@ -1238,7 +1239,7 @@ as int builds already do.
1238
1239
 
1239
1240
  **To adopt.** Nothing. With `0.1.0` and later, a `^0.1.0` range takes patches without action.
1240
1241
 
1241
- ### List items and blocks carry a `_key`, and field comments follow it — **behaviour change**
1242
+ #### List items and blocks carry a `_key`, and field comments follow it — **behaviour change**
1242
1243
 
1243
1244
  <!-- changeset: list-items-and-blocks-carry-keys -->
1244
1245
 
@@ -1261,7 +1262,7 @@ a `validateEntry` hook included, checks content against a strict schema, allow `
1261
1262
  items and blocks. Don't spread a list item onto a DOM element (`<li {...item}>`), or it renders
1262
1263
  a `_key` attribute. `_key` also suits React's `key` when rendering a list.
1263
1264
 
1264
- ### `stampOnPublish` dates a new entry at its first submit
1265
+ #### `stampOnPublish` dates a new entry at its first submit
1265
1266
 
1266
1267
  <!-- changeset: datetime-stamp-on-publish -->
1267
1268
 
@@ -1276,13 +1277,151 @@ editor carries an unsaved draft of a stamped entry onto its stamped version.
1276
1277
  The flag is refused off a top-level `datetime`, on `list: true`, and with `required: true`.
1277
1278
 
1278
1279
  **To adopt.** Add `stampOnPublish: true` to the date field your site sorts or labels by, such as
1279
- `{ name: 'publishedAt', type: 'datetime', stampOnPublish: true }`. See [Stamping a publication date](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#stamping-a-publication-date).
1280
+ `{ name: 'publishedAt', type: 'datetime', stampOnPublish: true }`. See [Stamping a publication date](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#stamping-a-publication-date).
1280
1281
 
1281
1282
  **Now deletable.** A build step or hook that writes a missing publication date into new entries.
1282
1283
 
1283
- ---
1284
+ #### A left activity rail and a docked content panel
1284
1285
 
1285
- ## Released
1286
+ <!-- changeset: editor-activity-rail -->
1287
+
1288
+ _First in 0.1.0._
1289
+
1290
+ **What changed.** A rail down the editor's left edge opens the content panel, quick open, the
1291
+ Branches and Comments drawers and the media library, with Settings and the account control at its
1292
+ foot. Comments open from the rail only, with the unresolved count on the button, and the media
1293
+ library has its own button instead of a Settings menu item. In a wide side-by-side window the
1294
+ content panel docks beside the preview and stays open as you move between entries. Its width and
1295
+ whether it is open survive a reload. Narrower than 1024px, or stacked, it lays over the form
1296
+ only, so the preview stays in view. Below 1024px the editor also starts stacked until you choose
1297
+ a layout. Notifications moved to the bottom-right corner, opposite the rail.
1298
+
1299
+ #### `editor.subtitle` removed; the tab names the open entry — **breaking (`editor.subtitle` removed)**
1300
+
1301
+ <!-- changeset: editor-subtitle-removed-tab-title -->
1302
+
1303
+ _First in 0.1.0._
1304
+
1305
+ **What changed.** `editor.subtitle` is no longer a config option, and `Editor` no longer takes
1306
+ `subtitle` or `siteSubtitle`: nothing has shown them since the top bar replaced the old header.
1307
+ While the editor is mounted, the browser tab reads "Entry label · Site title", or the site title
1308
+ with no entry open, and your page's own title comes back when it unmounts, unless a page navigated to has set its own.
1309
+
1310
+ **To adopt.** Delete `editor.subtitle` from your config; it was no longer shown.
1311
+
1312
+ #### The editor takes over the window — **breaking (`EditorPanes` export removed)**
1313
+
1314
+ <!-- changeset: editor-takes-over-the-window -->
1315
+
1316
+ _First in 0.1.0._
1317
+
1318
+ **What changed.** The editor renders as one full-window layer above your page, so a header or
1319
+ footer your root layout renders no longer pushes it down or makes the page scroll. While it is
1320
+ mounted, its sign-in and loading screens included, your layout's elements are hidden and out of
1321
+ the Tab order, and the page does not scroll; unmount restores them. The footer is gone: the version and an "About CanopyCMS" link are
1322
+ in the Settings menu. The two read-only banners sit in their own row under the header. The
1323
+ preview/form divider is now keyboard-resizable within the same 15–85% the split was always
1324
+ stored at, and `react-split-pane` is no longer a dependency.
1325
+ `EditorPanes`, `EditorPanesProps`, the `PaneLayout` type and the `SPLIT_PERCENT_*` and `DEFAULT_*_SPLIT_PERCENT` constants are
1326
+ no longer exported from `canopycms/client`. See
1327
+ [Your root layout](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#editor-customization).
1328
+
1329
+ **To adopt.** Nothing, unless you import `EditorPanes` or those constants: render `CanopyEditor`
1330
+ instead, which lays out its own panes. Hiding your site chrome on the editor route is now
1331
+ optional, but keeping analytics and global fetches out of it is still worthwhile.
1332
+
1333
+ **Now deletable.** A wrapper that drops the site header and footer on the editor route only to
1334
+ stop them shifting the editor.
1335
+
1336
+ #### The editor's top bar, with Save all — **behaviour change**
1337
+
1338
+ <!-- changeset: editor-top-bar-and-save-all -->
1339
+
1340
+ _First in 0.1.0._
1341
+
1342
+ **What changed.** The editor's header is a 48px top bar. Its breadcrumb's crumbs navigate: "All
1343
+ content" opens the content panel, and a collection crumb opens it on that collection. The open
1344
+ entry's crumb holds the entry menu, whose "Reload entry" is now "Refresh from server". A
1345
+ "Read-only" chip says why the entry or branch takes no edits. The branch pill shows the branch,
1346
+ its status and how many entries are unsaved, and its menu lists them; its "Reload all entries" is
1347
+ now "Refresh branch" and re-reads the entry list too. Beside Save, a caret offers "Save all", which
1348
+ saves every unsaved entry on the branch and names any it could not save, with the reason, keeping
1349
+ their changes. The site title and `editor.subtitle` no longer appear in the header; the rail's
1350
+ 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
1351
+ branch is mid-rebase now carries `code: 'BRANCH_SYNCING'`.
1352
+
1353
+ **To adopt.** Nothing, unless your browser tests find the header by its old text: "Reload entry",
1354
+ "Reload all entries", "Recently modified", "No entry selected", or the file menu's "All content"
1355
+ item, which the breadcrumb's "All content" replaces.
1356
+
1357
+ #### Rename and delete from the entry menu; Save says "Saved" itself — **behaviour change**
1358
+
1359
+ <!-- changeset: entry-menu-rename-delete-inline-saved -->
1360
+
1361
+ _First in 0.1.0._
1362
+
1363
+ **What changed.** The open entry's menu, on its name in the top bar, offers "Rename…" and
1364
+ "Delete entry", through the same dialogs as the content panel's. On a locked branch both stay in
1365
+ the menu, blocked, with the reason; for an entry a path rule refuses they are hidden, as in the
1366
+ content panel. A Save reads "Saving…" on the button, then "Saved" for two seconds, announced to
1367
+ screen readers, instead of a green "Saved" toast; on a phone the button shows a spinner, then a
1368
+ check. Failures still show their toast or alert, and Save all its result. Save is the top bar's
1369
+ one filled button; "Submit for review" and "Withdraw from review" are tinted. From either menu,
1370
+ deleting the open entry leaves no entry open instead of opening the first, and renaming it keeps
1371
+ it open under its new name. The delete dialog names the entry.
1372
+
1373
+ **To adopt.** Nothing, unless your browser tests wait for a "Saved" toast after Save: wait for the
1374
+ save request's response, or for the Save button to read "Saved".
1375
+
1376
+ #### The preview has a toolbar with device widths, reload and the saved version
1377
+
1378
+ <!-- changeset: preview-toolbar -->
1379
+
1380
+ _First in 0.1.0._
1381
+
1382
+ **What changed.** A toolbar above the editor's preview shows the page's path on your site
1383
+ (without `editor.previewPrefix` or `?branch=`), which copies on click. Its width selector
1384
+ renders the page at Fit, Desktop (1280 px), Tablet (768 px) or Mobile (390 px): the iframe takes
1385
+ exactly that CSS width, so your site's own breakpoints apply, scaled down when the pane is
1386
+ narrower. The choice persists per browser. Reload reloads the preview, the same action as the
1387
+ "Live updates off" Retry. "Open saved version" opens the page's `?branch=` URL in a new tab,
1388
+ which shows the branch's last saved state, without unsaved edits. A preview error now sits
1389
+ between the toolbar and the page instead of over it, naming the field by its labels. A custom
1390
+ `renderPreview` gets no toolbar. `PreviewFrame` takes two new optional props, `iframeStyle` and
1391
+ `onReload`.
1392
+
1393
+ #### The preview toolbar holds the highlight toggle and maximizes the preview; the layout toggle moves to the rail
1394
+
1395
+ <!-- changeset: preview-toolbar-controls -->
1396
+
1397
+ _First in 0.1.0._
1398
+
1399
+ **What changed.** The highlight toggle moves from the strip at the editor's right edge into the
1400
+ preview toolbar, and the layout toggle into the rail, above Settings, so it works with any
1401
+ preview or none; the strip is gone. The layout button shows the current layout and switches to
1402
+ the other; while the preview is maximized it says to restore the preview first. Layout, preview
1403
+ width and highlights persist per browser. Maximize lays the preview over the preview and form
1404
+ area, without reloading it, and drafts keep reaching it. Restore brings the form back, and so
1405
+ does Esc unless focus is inside the page. Anything that needs the form restores it too: a click
1406
+ on a highlighted element, which scrolls to its field, opening the content panel over the form,
1407
+ or a save refused for field errors. Dragging the divider between preview and form shows the
1408
+ preview's width over it, and resizing the panes, by drag or key, switches the width to Fit.
1409
+ Admins with highlights on see an "N unmapped" chip when the page marks fields that don't exist
1410
+ in the entry. It explains what that means and points to the browser console, which names the
1411
+ nearest field of each. Editors see no such diagnostics. A custom `renderPreview`, and an entry
1412
+ with no preview page, get no toolbar.
1413
+
1414
+ #### Linear-time mount paths and push refusals, and a fixed message for non-Error gateway failures — **security fix**
1415
+
1416
+ <!-- changeset: codeql-release-alerts -->
1417
+
1418
+ _First in 0.1.0._
1419
+
1420
+ **What changed.** The AI export trims slashes from `AIContentConfig.mountPath`, and the worker
1421
+ and its GitHub gateway recognise GitHub's refusal of a workflow push, in time linear in their
1422
+ length; a long run of slashes or a repeated phrase took seconds before. When the gateway fails with
1423
+ a thrown value that is not an `Error`, the worker gets a fixed message and the gateway's log gets
1424
+ the value, scrubbed and capped, since it could hold a stack.
1286
1425
 
1287
1426
  ### 0.0.67
1288
1427
 
@@ -1296,7 +1435,7 @@ never ended. `canopycms-auth-clerk` skips it while the editor is mounted, and ex
1296
1435
  **To adopt.** Nothing for the editor, if your edit page uses `useClerkAuthConfig()` or
1297
1436
  `ClerkSignIn`. If your CMS build renders `<SignIn>`, `UserButton` or `OrganizationSwitcher` on your
1298
1437
  own pages, mount the hook as in
1299
- [Clerk components on your own pages](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/packages/canopycms-auth-clerk/README.md#clerk-components-on-your-own-pages).
1438
+ [Clerk components on your own pages](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/packages/canopycms-auth-clerk/README.md#clerk-components-on-your-own-pages).
1300
1439
  Your own Server Actions in the CMS build still 403 behind OAC.
1301
1440
 
1302
1441
  **Now deletable.** Any local patch of `window.__internal_onBeforeSetActive` (or 6.x's
@@ -1337,12 +1476,12 @@ fast naming the ARN, the field and the keys present, never a value.
1337
1476
  **To adopt.** Nothing, unless one of those secrets holds a JSON document: then set the matching
1338
1477
  prop. The scaffolded stack fills them from the repository _variables_
1339
1478
  `CANOPY_GITHUB_TOKEN_SECRET_JSON_FIELD` and `CLERK_SECRET_KEY_SECRET_JSON_FIELD`
1340
- ([why the prefix](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#repository-secrets-and-variables)); a stack scaffolded earlier
1479
+ ([why the prefix](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#repository-secrets-and-variables)); a stack scaffolded earlier
1341
1480
  adds the props to `infrastructure/bin/app.ts` and `infrastructure/lib/cms-stack.ts` and the two `env`
1342
1481
  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
1343
1482
  (also in `secretsArns`). Don't use CDK's `secretValueFromJson`: it resolves the plaintext into the deployed
1344
1483
  resource's configuration, readable by anyone who can describe it. Only `CLERK_SECRET_KEY` goes through Secrets Manager; `CLERK_JWT_KEY` and the publishable
1345
- key are public material ([Security Model](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#security-model)).
1484
+ key are public material ([Security Model](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#security-model)).
1346
1485
 
1347
1486
  **Now deletable.** A wrapper that fetches the secret, parses it and re-exports one field into the
1348
1487
  worker's environment, such as a `jq` step in user data or a wrapper entrypoint around
@@ -1354,7 +1493,7 @@ worker's environment, such as a `jq` step in user data or a wrapper entrypoint a
1354
1493
  `githubToken` is not deprecated and stays the documented default; `canopycms` neither depends on
1355
1494
  nor imports `@octokit/auth-app`. Register one App per site, not one shared across repositories:
1356
1495
  anyone holding an App's key can mint a token for any of its installations
1357
- ([why](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/ARCHITECTURE.md#why-one-github-app-per-site-not-one-shared-across-an-organisation)).
1496
+ ([why](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/ARCHITECTURE.md#why-one-github-app-per-site-not-one-shared-across-an-organisation)).
1358
1497
 
1359
1498
  **To adopt with `canopycms-cdk`** (existing stacks need no edit). `CanopyCmsServiceProps` takes
1360
1499
  `githubAppId`, `githubAppInstallationId`, `githubAppPrivateKeySecretArn` and
@@ -1367,7 +1506,7 @@ anyone holding an App's key can mint a token for any of its installations
1367
1506
  `verify` checks an existing installation's permissions: run it, since an App missing a
1368
1507
  permission only shows up later, as `sync-failed`. `create` needs an interactive terminal and
1369
1508
  never edits an existing JSON secret (create one and point the JSON-field prop at it)
1370
- ([details](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#register-it-with-canopycms-init-github-app)).
1509
+ ([details](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#register-it-with-canopycms-init-github-app)).
1371
1510
  2. Set the three props and **remove `githubTokenSecretArn`** (with its JSON field). A partial set
1372
1511
  of the three is refused at synth, and so is an App alongside a token.
1373
1512
  3. In the generated workflow, store them as `CANOPY_GITHUB_APP_ID`,
@@ -1378,7 +1517,7 @@ anyone holding an App's key can mint a token for any of its installations
1378
1517
 
1379
1518
  The private key is ARN-only; passing the key itself is refused at synth. Its ARN joins the worker's
1380
1519
  IAM policy automatically, so you need not add it to `secretsArns`. See
1381
- [Authenticating as a GitHub App](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#authenticating-as-a-github-app).
1520
+ [Authenticating as a GitHub App](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#authenticating-as-a-github-app).
1382
1521
 
1383
1522
  ```bash
1384
1523
  canopycms init-github-app create -- \
@@ -1426,7 +1565,7 @@ downloads the `.pem` from the App's settings page.
1426
1565
 
1427
1566
  **What changed.** The worker re-reads a Secrets Manager secret (the GitHub token or the Clerk secret
1428
1567
  key) when the operation using it fails. Timings, costs and the store-before-revoke order are in
1429
- [Rotating a secret](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#rotating-a-secret).
1568
+ [Rotating a secret](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#rotating-a-secret).
1430
1569
 
1431
1570
  **To adopt.** Nothing for credentials from `*_SECRET_ARN`, which is every scaffolded deployment. A
1432
1571
  GitHub App private key is still read only at boot: store the new key, replace the instance, and
@@ -1480,12 +1619,12 @@ also check the bucket policy for its grant.
1480
1619
  5. `withCanopy()` adds sharp's libvips to Next's file tracing (not for a static export) and, on
1481
1620
  Next 16 and later, sets `turbopack: {}` when your config has neither `turbopack` nor your own
1482
1621
  `webpack` and it can read your Next version. A webpack build still fails its image transforms
1483
- ([Dual Build Support](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#dual-build-support)).
1622
+ ([Dual Build Support](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#dual-build-support)).
1484
1623
 
1485
1624
  **Breaking:** a stack setting `platform` on `fromImageAsset` without `architecture` gets an arm64
1486
1625
  function while `platform` still decides the image, so `Platform.LINUX_AMD64` builds an image the
1487
1626
  function cannot run. A stack setting neither moves to arm64 with a matching image, built under QEMU
1488
- on an x86 host ([Where the image is built](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#where-the-image-is-built)).
1627
+ on an x86 host ([Where the image is built](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#where-the-image-is-built)).
1489
1628
 
1490
1629
  **To adopt.**
1491
1630
 
@@ -1514,7 +1653,7 @@ reason; `platform` on `fromImageAsset`; and, with `withCanopy()`, a hand-written
1514
1653
 
1515
1654
  **What changed.** `media.uploadUrl` (s3 adapter; an absolute http(s) URL or a site-relative path)
1516
1655
  replaces the `url` that `beginUpload()` returns, leaving the presign's `fields` untouched, so
1517
- uploads can be same-origin. See [Media Configuration](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#media-configuration) for the
1656
+ uploads can be same-origin. See [Media Configuration](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#media-configuration) for the
1518
1657
  CloudFront behavior it expects.
1519
1658
 
1520
1659
  **To adopt.** Optional. Set it from an environment variable: a site-relative value works only where
@@ -1539,7 +1678,7 @@ needs `lazyPublicTransforms: true`) let you pass a deterministically named role,
1539
1678
  bucket's stack computes `arn:aws:iam::<account>:role/<name>` from literals instead of a
1540
1679
  `Fn::GetStackOutput` reference only the CDK CLI resolves. The constructs re-attach
1541
1680
  `AWSLambdaBasicExecutionRole` (and the VPC policy) that `lambda.Function` drops from a role you
1542
- pass. See [Cross-account asset bucket](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#cross-account-asset-bucket).
1681
+ pass. See [Cross-account asset bucket](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#cross-account-asset-bucket).
1543
1682
 
1544
1683
  **To adopt.** Optional:
1545
1684
 
@@ -1649,7 +1788,7 @@ public JWKS PEM, used only to verify signatures.
1649
1788
  to variables (Settings → Secrets and variables → Actions → Variables); `bin/app.ts` refuses the
1650
1789
  deploy at synth while it is unset. Check that `bin/app.ts` passes `CLERK_SECRET_KEY` as
1651
1790
  `clerkSecretKeySecretArn` (read by the worker), never in the Lambda's `environment`, unless you keep
1652
- `clerkMiddleware` ([Security Model](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/docs/deploying-to-aws.md#security-model)).
1791
+ `clerkMiddleware` ([Security Model](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/docs/deploying-to-aws.md#security-model)).
1653
1792
 
1654
1793
  #### `readByUrlPath` answers only where `listEntries` publishes — **breaking (routing)**
1655
1794
 
@@ -1748,7 +1887,7 @@ basePath: process.env.NEXT_PUBLIC_BASE_PATH,
1748
1887
  Then pass `baseUrl` to `assetUrl` only if Next serves `/assets` (local adapter, `next dev`, S3 with
1749
1888
  no distribution): `assetUrl(image, { width: 960, baseUrl: BASE_PATH })`. Assets on CloudFront via
1750
1889
  `AssetSupport` stay at the distribution root; pass no `baseUrl` (mount table under "Where `/assets`
1751
- is mounted" in [Media Configuration](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#media-configuration)). Two traps:
1890
+ is mounted" in [Media Configuration](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#media-configuration)). Two traps:
1752
1891
 
1753
1892
  - Don't pass a deployment `basePath` to `contentStaticParams({ basePath })`; that option filters by
1754
1893
  a nested route prefix and emits zero params, building green.
@@ -1907,7 +2046,7 @@ Under `exactOptionalPropertyTypes: true`, omit keys instead of assigning `undefi
1907
2046
  `withTrailingSlash` and `isAbsoluteUrl` from `canopycms/server`; `defineSeoFieldGroup()` from
1908
2047
  `canopycms`; `generateContentSitemap` and `entryToMetadata` from `canopycms-next`, bound on
1909
2048
  `createNextCanopyContext`'s result. See
1910
- [Sitemap and SEO Metadata](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#sitemap-and-seo-metadata). Every routable entry type is in
2049
+ [Sitemap and SEO Metadata](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#sitemap-and-seo-metadata). Every routable entry type is in
1911
2050
  the sitemap unless `exclude`d or `noindex`; one `noindex` predicate drives both robots metadata and
1912
2051
  sitemap exclusion. `trailingSlash` is never inferred. `lastModified` defaults to `updatedAt`, a
1913
2052
  filesystem mtime. An empty SEO field counts as unset. `siteUrl` must be absolute, colliding URLs are
@@ -1964,7 +2103,7 @@ underscore-prefixed files are skipped.
1964
2103
 
1965
2104
  **What changed.** New scaffolds write `defaultBranchAccess: 'deny'` and
1966
2105
  `defaultPathAccess: { read: 'allow' }`
1967
- ([Public read on server deployments](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#public-read-on-server-deployments)).
2106
+ ([Public read on server deployments](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#public-read-on-server-deployments)).
1968
2107
 
1969
2108
  **To adopt.** An older scaffold's `defaultBranchAccess: 'allow'` is wider than recommended;
1970
2109
  consider `'deny'`. If you deleted `defaultPathAccess: { read: 'allow' }`, anonymous visitors get no content until you
@@ -2017,7 +2156,7 @@ re-checked an entry's type (a `switch` on `meta.entryType` can merge them into o
2017
2156
  **What changed.** `BlockComponentRegistry<Blocks>` (types only, from `canopycms`; `BlockValueOf<Blocks, 'hero'>`
2018
2157
  gives one template's value type) requires exactly
2019
2158
  one component per block template name when written as an object literal. See
2020
- [Block Component Registries](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#block-component-registries) and
2159
+ [Block Component Registries](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#block-component-registries) and
2021
2160
  `apps/example1/app/components/PostView.tsx`.
2022
2161
 
2023
2162
  ```ts
@@ -2039,7 +2178,7 @@ templates against the schema.
2039
2178
 
2040
2179
  **What changed.** Spreading a `const` field array into several schemas, and nesting
2041
2180
  `defineInlineFieldGroup()` inside a block template, are documented
2042
- ([Reusable Field Fragments](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#reusable-field-fragments)); `defineFieldFragment()` is an
2181
+ ([Reusable Field Fragments](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#reusable-field-fragments)); `defineFieldFragment()` is an
2043
2182
  identity helper for them.
2044
2183
 
2045
2184
  ```ts
@@ -2058,7 +2197,7 @@ field by composing from the same `const` field object.
2058
2197
  A block template holding a `reference` field is a shared block; `read()`/`readByUrlPath()` resolve
2059
2198
  it, and listings do with
2060
2199
  [`resolveReferences`](#listings-resolve-references-and-a-resolved-reference-carries-urlpath-and-optionally-its-body-16--breaking-type-level)
2061
- (0.0.64). See [Shared / Referenced Blocks](https://github.com/safeinsights/canopycms/blob/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#shared--referenced-blocks).
2200
+ (0.0.64). See [Shared / Referenced Blocks](https://github.com/safeinsights/canopycms/blob/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#shared--referenced-blocks).
2062
2201
 
2063
2202
  **Now deletable.** A second `read()` in page code to unwrap a shared block's reference.
2064
2203
 
@@ -2080,7 +2219,7 @@ Not retro-documented, apart from these:
2080
2219
 
2081
2220
  #### The registry is keyed by entry-type name (0.0.42)
2082
2221
 
2083
- **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/f8b50b30170954bf52fb670afe6796d2cb8fa275/README.md#convention-why-key-the-registry-by-entry-type-name) says when to keep them.
2222
+ **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/35953fbf6f9e0e0bb8181ed89c3ade8cd14db72a/README.md#convention-why-key-the-registry-by-entry-type-name) says when to keep them.
2084
2223
 
2085
2224
  **To adopt.**
2086
2225