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