From eca376c8a712c4f2c64765a12aa6f06825ae408c Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:14:00 +0100 Subject: [PATCH 01/40] feat: memoize the boundGetAsset function --- packages/decap-cms-core/src/actions/media.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/decap-cms-core/src/actions/media.ts b/packages/decap-cms-core/src/actions/media.ts index 8cf7abab6be3..079b3991ade4 100644 --- a/packages/decap-cms-core/src/actions/media.ts +++ b/packages/decap-cms-core/src/actions/media.ts @@ -1,4 +1,5 @@ import { isAbsolutePath } from 'decap-cms-lib-util'; +import memoize from 'lodash/memoize' import { createAssetProxy } from '../valueObjects/AssetProxy'; import { selectMediaFilePath } from '../reducers/entries'; @@ -80,18 +81,22 @@ const emptyAsset = createAssetProxy({ }), }); -export function boundGetAsset( +export const boundGetAsset = memoize(( dispatch: ThunkDispatch, collection: Collection, entry: EntryMap, -) { +) => { function bound(path: string, field: EntryField) { const asset = dispatch(getAsset({ collection, entry, path, field })); return asset; } return bound; -} +}, function resolveCacheKey(_dispatch, collection, entry) { + // Generate a unique cache key based on the collection name and entry slug + // The dispatch function is not included in the cache key since it is stable and does not change between calls + return `${collection?.get('name')}$$${entry?.get('slug')}`; +}); export function getAsset({ collection, entry, path, field }: GetAssetArgs) { return (dispatch: ThunkDispatch, getState: () => State) => { From 854af210c17e1f801a622c6a484541bb27605be9 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:15:05 +0100 Subject: [PATCH 02/40] feat: make parent ids stable --- packages/decap-cms-widget-object/src/ObjectControl.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index 3ecadfcd434a..fa1b273dd2b1 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { ClassNames } from '@emotion/react'; +import memoize from 'lodash/memoize'; import { List, Map } from 'immutable'; import { colors, lengths, ObjectWidgetTopBar } from 'decap-cms-ui-default'; import { stringTemplate } from 'decap-cms-lib-widgets'; @@ -93,6 +94,8 @@ export default class ObjectControl extends React.Component { }); }; + getStableParentIds = memoize((parentIds, forID) => [...parentIds, forID], JSON.stringify /* Fast enough for only ids */); + controlFor(field, key) { const { value, @@ -130,7 +133,7 @@ export default class ObjectControl extends React.Component { fieldsErrors={fieldsErrors} onValidate={onValidateObject} controlRef={this.processControlRef} - parentIds={[...parentIds, forID]} + parentIds={this.getStableParentIds(parentIds, forID)} isDisabled={isDuplicate} isHidden={isHidden} isFieldDuplicate={isFieldDuplicate} From 9f79c399dde4c3f6dbbc10efa5d1bc5e274b50f4 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:16:09 +0100 Subject: [PATCH 03/40] feat: make each child onChange callback stable and also made parentIds stable --- .../decap-cms-widget-list/src/ListControl.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 97f74e9da16c..4cc45390f822 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -6,6 +6,7 @@ import { css, ClassNames } from '@emotion/react'; import { List, Map, fromJS } from 'immutable'; import partial from 'lodash/partial'; import isEmpty from 'lodash/isEmpty'; +import memoize from 'lodash/memoize'; import uniqueId from 'lodash/uniqueId'; import { v4 as uuid } from 'uuid'; import DecapCmsWidgetObject from 'decap-cms-widget-object'; @@ -217,6 +218,7 @@ export default class ListControl extends React.Component { listCollapsed, itemsCollapsed, value: this.valueToString(value), + valueReference: value, keys, }; } @@ -256,10 +258,18 @@ export default class ListControl extends React.Component { uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`); /** + * Old comment: + * * Always update so that each nested widget has the option to update. This is * required because ControlHOC provides a default `shouldComponentUpdate` * which only updates if the value changes, but every widget must be allowed * to override this. + * + * New comment: + * + * Each Widget is wrapped with EditorControl which already tries to update every time. + * Is there a specific reason we need to always rerender the list? + * This seems overkill. */ shouldComponentUpdate() { return true; @@ -419,7 +429,7 @@ export default class ListControl extends React.Component { */ getObjectValue = idx => this.props.value.get(idx) || Map(); - handleChangeFor(index) { + handleChangeFor = memoize((index) => { return (f, newValue, newMetadata) => { const { value, metadata, onChange, field } = this.props; const collectionName = field.get('name'); @@ -435,7 +445,7 @@ export default class ListControl extends React.Component { }; onChange(value.set(index, newObjectValue), parsedMetadata); }; - } + }) handleRemove = (index, event) => { event.preventDefault(); @@ -630,6 +640,8 @@ export default class ListControl extends React.Component { } } + getStableParentIds = memoize((parentIds, forID) => [...parentIds, forID], JSON.stringify /* Fast enough for only ids */); + // eslint-disable-next-line react/display-name renderItem = (item, index) => { const { @@ -714,7 +726,7 @@ export default class ListControl extends React.Component { collapsed={collapsed} data-testid={`object-control-${key}`} hasError={hasError} - parentIds={[...parentIds, forID, key]} + parentIds={this.getStableParentIds(parentIds, forID)} /> )} @@ -810,6 +822,8 @@ export default class ListControl extends React.Component { } render() { + console.log('Rerendering ListControl'); + if (this.getValueType() !== null) { return this.renderListControl(); } else { From c0b8ddc95955a1057cb5f3ce4e2ee8a1ac72ad3a Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:17:36 +0100 Subject: [PATCH 04/40] feat: optimize editor control components and widgets --- .../Editor/EditorControlPane/EditorControl.js | 107 +++++++++++++----- .../EditorControlPane/EditorControlPane.js | 46 ++++++-- .../Editor/EditorControlPane/Widget.js | 10 +- 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 14290381ee80..4aa66bf7f064 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -7,6 +7,7 @@ import { ClassNames, Global, css as coreCss } from '@emotion/react'; import styled from '@emotion/styled'; import partial from 'lodash/partial'; import uniqueId from 'lodash/uniqueId'; +import memoize from 'lodash/memoize'; import { connect } from 'react-redux'; import { FieldLabel, colors, transitions, lengths, borders } from 'decap-cms-ui-default'; import ReactMarkdown from 'react-markdown'; @@ -17,6 +18,7 @@ import { clearFieldErrors, tryLoadEntry, validateMetaField } from '../../../acti import { addAsset, boundGetAsset } from '../../../actions/media'; import { selectIsLoadingAsset } from '../../../reducers/medias'; import { query, clearSearch } from '../../../actions/search'; +import { store } from '../../../redux'; import { openMediaLibrary, removeInsertedMedia, @@ -147,11 +149,11 @@ class EditorControl extends React.Component { clearSearch: PropTypes.func.isRequired, clearFieldErrors: PropTypes.func.isRequired, loadEntry: PropTypes.func.isRequired, + getEntry: PropTypes.func.isRequired, t: PropTypes.func.isRequired, isEditorComponent: PropTypes.bool, isNewEditorComponent: PropTypes.bool, parentIds: PropTypes.arrayOf(PropTypes.string), - entry: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired, isDisabled: PropTypes.bool, isHidden: PropTypes.bool, @@ -176,6 +178,29 @@ class EditorControl extends React.Component { PropTypes.checkPropTypes(EditorControl.propTypes, this.props, 'prop', 'EditorControl'); } + shouldComponentUpdate(nextProps, nextState) { + const valuesThatWhereUpdated = [] + for (const key in nextProps) { + if (this.props[key] !== nextProps[key]) { + valuesThatWhereUpdated.push(key) + } + } + + const stateValuesThatWhereUpdated = [] + for (const key in nextState) { + if (this.state[key] !== nextState[key]) { + stateValuesThatWhereUpdated.push(key) + } + } + + console.log('EditorControl shouldComponentUpdate', { + valuesThatWhereUpdated, + stateValuesThatWhereUpdated, + }) + + return true; + } + isAncestorOfFieldError = () => { const { fieldsErrors } = this.props; @@ -187,10 +212,20 @@ class EditorControl extends React.Component { return false; }; + getEntry = () => { + // This will have the latest value even if the component doest rerender + return this.props.entry; + } + + onChange = (newValue, newMetadata) => { + this.props.onChange(this.props.field, newValue, newMetadata); + this.props.clearFieldErrors(this.uniqueFieldId); // We are deleting errors for this field only. + } + render() { const { value, - entry, + getEntry, collection, config, field, @@ -198,7 +233,6 @@ class EditorControl extends React.Component { fieldsErrors, mediaPaths, boundGetAsset, - onChange, openMediaLibrary, clearMediaControl, removeMediaControl, @@ -308,7 +342,7 @@ class EditorControl extends React.Component { ${styleStrings.labelActive}; `} controlComponent={widget.control} - entry={entry} + entry={getEntry()} // This field has been deprecated and can contain stale data, do not use it in widgets collection={collection} config={config} field={field} @@ -316,10 +350,7 @@ class EditorControl extends React.Component { value={value} mediaPaths={mediaPaths} metadata={metadata} - onChange={(newValue, newMetadata) => { - onChange(field, newValue, newMetadata); - clearFieldErrors(this.uniqueFieldId); // Видаляємо помилки лише для цього поля - }} + onChange={this.onChange} onValidate={onValidate && partial(onValidate, this.uniqueFieldId)} onOpenMediaLibrary={openMediaLibrary} onClearMediaControl={clearMediaControl} @@ -338,6 +369,7 @@ class EditorControl extends React.Component { editorControl={ConnectedEditorControl} query={query} loadEntry={loadEntry} + getEntry={this.getEntry} queryHits={queryHits[this.uniqueFieldId] || []} clearSearch={clearSearch} clearFieldErrors={clearFieldErrors} @@ -385,13 +417,10 @@ class EditorControl extends React.Component { } } -function mapStateToProps(state) { - const { collections, entryDraft } = state; - const entry = entryDraft.get('entry'); - const collection = collections.get(entryDraft.getIn(['entry', 'collection'])); - const isLoadingAsset = selectIsLoadingAsset(state.medias); - - async function loadEntry(collectionName, slug) { +const stable = { + loadEntry: async function stable_loadEntry(collectionName, slug) { + const state = store.getState(); + const { collections } = state; const targetCollection = collections.get(collectionName); if (targetCollection) { const loadedEntry = await tryLoadEntry(state, targetCollection, slug); @@ -399,21 +428,43 @@ function mapStateToProps(state) { } else { throw new Error(`Can't find collection '${collectionName}'`); } - } + }, - return { - mediaPaths: state.mediaLibrary.get('controlMedia'), - isFetching: state.search.isFetching, - queryHits: state.search.queryHits, - config: state.config, - entry, - collection, - isLoadingAsset, - loadEntry, - validateMetaField: (field, value, t) => validateMetaField(state, collection, field, value, t), - }; + // Will return the same function instance for the same collection. + validateMetaField: memoize((collection) => { + const state = store.getState(); + return (field, value, t) => validateMetaField(state, collection, field, value, t); + }), + + getEntry() { + const state = store.getState(); + return state.entryDraft.get('entry'); + }, + + getBoundedAsset(collection, entry) { + const dispatch = store.dispatch; + return boundGetAsset(dispatch, collection, entry); + } } +function mapStateToProps(state) { + const { collections, entryDraft } = state; + const collection = collections.get(entryDraft.getIn(['entry', 'collection'])); + const isLoadingAsset = selectIsLoadingAsset(state.medias); + + return { + mediaPaths: state.mediaLibrary.get('controlMedia'), + isFetching: state.search.isFetching, + queryHits: state.search.queryHits, + config: state.config, + collection, + isLoadingAsset, + getEntry: stable.getEntry, + loadEntry: stable.loadEntry, + validateMetaField: stable.validateMetaField(collection), + }; + } + function mapDispatchToProps(dispatch) { const creators = bindActionCreators( { @@ -431,7 +482,7 @@ function mapDispatchToProps(dispatch) { ); return { ...creators, - boundGetAsset: (collection, entry) => boundGetAsset(dispatch, collection, entry), + boundGetAsset: stable.getBoundedAsset, }; } diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js index dae2d3d3a241..8059f4d4ec89 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import memoize from 'lodash/memoize'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; @@ -107,9 +108,9 @@ export default class ControlPane extends React.Component { this.childRefs[name] = wrappedControl; }; - getControlRef = field => wrappedControl => { + getControlRef = memoize(field => wrappedControl => { this.controlRef(field, wrappedControl); - }; + }); handleLocaleChange = val => { this.setState({ selectedLocale: val }); @@ -179,8 +180,37 @@ export default class ControlPane extends React.Component { } } + getI18n() { + const { collection } = this.props; + const { locales, defaultLocale } = getI18nInfo(collection); + const locale = this.state.selectedLocale; + return ( + locales && { + currentLocale: locale, + locales, + defaultLocale, + } + ); + } + + onChange = (field, newValue, newMetadata) => { + this.props.onChange(field, newValue, newMetadata, this.getI18n()); + } + + isFieldDuplicate = field => { + const locale = this.state.selectedLocale; + const { defaultLocale } = getI18nInfo(this.props.collection); + return isFieldDuplicate(field, locale, defaultLocale); + } + + isFieldHidden = field => { + const locale = this.state.selectedLocale; + const { defaultLocale } = getI18nInfo(this.props.collection); + return isFieldHidden(field, locale, defaultLocale); + } + render() { - const { collection, entry, fields, fieldsMetaData, fieldsErrors, onChange, onValidate, t } = + const { collection, entry, fields, fieldsMetaData, fieldsErrors, onValidate, t } = this.props; if (!collection || !fields) { @@ -237,17 +267,15 @@ export default class ControlPane extends React.Component { })} fieldsMetaData={fieldsMetaData} fieldsErrors={fieldsErrors} - onChange={(field, newValue, newMetadata) => { - onChange(field, newValue, newMetadata, i18n); - }} + onChange={this.onChange} onValidate={onValidate} controlRef={this.getControlRef(field)} - entry={entry} + // entry={entry} For compatibility with existing controls, we pass the (stale) entry down to the widget. collection={collection} isDisabled={isDuplicate} isHidden={isHidden} - isFieldDuplicate={field => isFieldDuplicate(field, locale, defaultLocale)} - isFieldHidden={field => isFieldHidden(field, locale, defaultLocale)} + isFieldDuplicate={this.isFieldDuplicate} + isFieldHidden={this.isFieldHidden} locale={locale} /> ); diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js index c6d165e0bca7..ba65170164e8 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -67,7 +67,11 @@ export default class Widget extends Component { onValidateObject: PropTypes.func, isEditorComponent: PropTypes.bool, isNewEditorComponent: PropTypes.bool, + /** + * @deprecated Every update creates a new entry, passing a live value down is too expensive. Use the getEntry callback instead or get the value from the store directly in the widget via `useSelector` or `connect`. See + */ entry: ImmutablePropTypes.map.isRequired, + getEntry: PropTypes.func.isRequired, isDisabled: PropTypes.bool, isFieldDuplicate: PropTypes.func, isFieldHidden: PropTypes.func, @@ -281,7 +285,8 @@ export default class Widget extends Component { render() { const { controlComponent, - entry, + entry, // TODO: Remove this prop in favor of getEntry + getEntry, collection, config, field, @@ -329,7 +334,8 @@ export default class Widget extends Component { } = this.props; return React.createElement(controlComponent, { - entry, + entry, // TODO: Remove this deprecated prop in favor of getEntry + getEntry, collection, config, field, From e4fdab6d8f583638c0d092ab82b80b5e8ef3bd0b Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:23:57 +0100 Subject: [PATCH 05/40] feat: fixed the last violating unstable component --- .../src/MarkdownControl/components/Shortcode.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js index 7a835796de22..26d0cd95c126 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js @@ -1,8 +1,9 @@ /* eslint-disable react/prop-types */ -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { fromJS } from 'immutable'; import omit from 'lodash/omit'; +import noop from 'lodash/noop'; import { ReactEditor, useSlate } from 'slate-react'; import { Range, Transforms } from 'slate'; @@ -15,10 +16,10 @@ function Shortcode(props) { const plugin = getEditorComponents().get(element.data.shortcode); const fieldKeys = ['id', 'fromBlock', 'toBlock', 'toPreview', 'pattern', 'icon']; - const field = fromJS(omit(plugin, fieldKeys)); + const field = useMemo(() => fromJS(omit(plugin, fieldKeys)), [plugin]); const [value, setValue] = useState(fromJS(element.data[dataKey])); - function handleChange(fieldName, value, metadata) { + const handleChange = useCallback((fieldName, value, metadata) => { const path = ReactEditor.findPath(editor, element); const newProperties = { data: { @@ -31,7 +32,11 @@ function Shortcode(props) { at: path, }); setValue(value); - } + }, [editor, element, dataKey]); + + useEffect(() => { + console.log('editor or element or dataKey changed, updating value'); + }, [editor, element, dataKey]); function handleFocus() { const path = ReactEditor.findPath(editor, element); @@ -61,7 +66,7 @@ function Shortcode(props) { field={field} onChange={handleChange} isEditorComponent={true} - onValidateObject={() => {}} + onValidateObject={noop} isNewEditorComponent={element.data.shortcodeNew} isSelected={isSelected} /> From b22022d6f398d5bfbd0d9ce4eb7811f09787e452 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:26:01 +0100 Subject: [PATCH 06/40] fix: remove performance logging comments --- .../Editor/EditorControlPane/EditorControl.js | 23 ------------------- .../decap-cms-widget-list/src/ListControl.js | 2 -- .../MarkdownControl/components/Shortcode.js | 6 +---- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 4aa66bf7f064..a6552619b840 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -178,29 +178,6 @@ class EditorControl extends React.Component { PropTypes.checkPropTypes(EditorControl.propTypes, this.props, 'prop', 'EditorControl'); } - shouldComponentUpdate(nextProps, nextState) { - const valuesThatWhereUpdated = [] - for (const key in nextProps) { - if (this.props[key] !== nextProps[key]) { - valuesThatWhereUpdated.push(key) - } - } - - const stateValuesThatWhereUpdated = [] - for (const key in nextState) { - if (this.state[key] !== nextState[key]) { - stateValuesThatWhereUpdated.push(key) - } - } - - console.log('EditorControl shouldComponentUpdate', { - valuesThatWhereUpdated, - stateValuesThatWhereUpdated, - }) - - return true; - } - isAncestorOfFieldError = () => { const { fieldsErrors } = this.props; diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 4cc45390f822..7875de4626c8 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -822,8 +822,6 @@ export default class ListControl extends React.Component { } render() { - console.log('Rerendering ListControl'); - if (this.getValueType() !== null) { return this.renderListControl(); } else { diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js index 26d0cd95c126..eb9f634680be 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import { fromJS } from 'immutable'; import omit from 'lodash/omit'; @@ -34,10 +34,6 @@ function Shortcode(props) { setValue(value); }, [editor, element, dataKey]); - useEffect(() => { - console.log('editor or element or dataKey changed, updating value'); - }, [editor, element, dataKey]); - function handleFocus() { const path = ReactEditor.findPath(editor, element); Transforms.select(editor, path); From e8176dab61dc4d4511205b42a0a2255bcceb68d5 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Sun, 8 Feb 2026 19:32:20 +0100 Subject: [PATCH 07/40] refactor: code formatting --- packages/decap-cms-core/src/actions/media.ts | 31 +++++++------- .../Editor/EditorControlPane/EditorControl.js | 42 +++++++++---------- .../EditorControlPane/EditorControlPane.js | 9 ++-- .../decap-cms-widget-list/src/ListControl.js | 15 ++++--- .../MarkdownControl/components/Shortcode.js | 31 +++++++------- .../src/ObjectControl.js | 5 ++- 6 files changed, 70 insertions(+), 63 deletions(-) diff --git a/packages/decap-cms-core/src/actions/media.ts b/packages/decap-cms-core/src/actions/media.ts index 079b3991ade4..4bed9715523c 100644 --- a/packages/decap-cms-core/src/actions/media.ts +++ b/packages/decap-cms-core/src/actions/media.ts @@ -1,5 +1,5 @@ import { isAbsolutePath } from 'decap-cms-lib-util'; -import memoize from 'lodash/memoize' +import memoize from 'lodash/memoize'; import { createAssetProxy } from '../valueObjects/AssetProxy'; import { selectMediaFilePath } from '../reducers/entries'; @@ -81,22 +81,21 @@ const emptyAsset = createAssetProxy({ }), }); -export const boundGetAsset = memoize(( - dispatch: ThunkDispatch, - collection: Collection, - entry: EntryMap, -) => { - function bound(path: string, field: EntryField) { - const asset = dispatch(getAsset({ collection, entry, path, field })); - return asset; - } +export const boundGetAsset = memoize( + (dispatch: ThunkDispatch, collection: Collection, entry: EntryMap) => { + function bound(path: string, field: EntryField) { + const asset = dispatch(getAsset({ collection, entry, path, field })); + return asset; + } - return bound; -}, function resolveCacheKey(_dispatch, collection, entry) { - // Generate a unique cache key based on the collection name and entry slug - // The dispatch function is not included in the cache key since it is stable and does not change between calls - return `${collection?.get('name')}$$${entry?.get('slug')}`; -}); + return bound; + }, + function resolveCacheKey(_dispatch, collection, entry) { + // Generate a unique cache key based on the collection name and entry slug + // The dispatch function is not included in the cache key since it is stable and does not change between calls + return `${collection?.get('name')}$$${entry?.get('slug')}`; + }, +); export function getAsset({ collection, entry, path, field }: GetAssetArgs) { return (dispatch: ThunkDispatch, getState: () => State) => { diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index a6552619b840..7140d1a422c7 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -190,14 +190,14 @@ class EditorControl extends React.Component { }; getEntry = () => { - // This will have the latest value even if the component doest rerender + // This will have the latest value even if the component doest rerender return this.props.entry; - } + }; onChange = (newValue, newMetadata) => { this.props.onChange(this.props.field, newValue, newMetadata); this.props.clearFieldErrors(this.uniqueFieldId); // We are deleting errors for this field only. - } + }; render() { const { @@ -408,7 +408,7 @@ const stable = { }, // Will return the same function instance for the same collection. - validateMetaField: memoize((collection) => { + validateMetaField: memoize(collection => { const state = store.getState(); return (field, value, t) => validateMetaField(state, collection, field, value, t); }), @@ -421,26 +421,26 @@ const stable = { getBoundedAsset(collection, entry) { const dispatch = store.dispatch; return boundGetAsset(dispatch, collection, entry); - } -} + }, +}; function mapStateToProps(state) { - const { collections, entryDraft } = state; - const collection = collections.get(entryDraft.getIn(['entry', 'collection'])); - const isLoadingAsset = selectIsLoadingAsset(state.medias); + const { collections, entryDraft } = state; + const collection = collections.get(entryDraft.getIn(['entry', 'collection'])); + const isLoadingAsset = selectIsLoadingAsset(state.medias); - return { - mediaPaths: state.mediaLibrary.get('controlMedia'), - isFetching: state.search.isFetching, - queryHits: state.search.queryHits, - config: state.config, - collection, - isLoadingAsset, - getEntry: stable.getEntry, - loadEntry: stable.loadEntry, - validateMetaField: stable.validateMetaField(collection), - }; - } + return { + mediaPaths: state.mediaLibrary.get('controlMedia'), + isFetching: state.search.isFetching, + queryHits: state.search.queryHits, + config: state.config, + collection, + isLoadingAsset, + getEntry: stable.getEntry, + loadEntry: stable.loadEntry, + validateMetaField: stable.validateMetaField(collection), + }; +} function mapDispatchToProps(dispatch) { const creators = bindActionCreators( diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js index 8059f4d4ec89..7eba6e059aea 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControlPane.js @@ -195,23 +195,22 @@ export default class ControlPane extends React.Component { onChange = (field, newValue, newMetadata) => { this.props.onChange(field, newValue, newMetadata, this.getI18n()); - } + }; isFieldDuplicate = field => { const locale = this.state.selectedLocale; const { defaultLocale } = getI18nInfo(this.props.collection); return isFieldDuplicate(field, locale, defaultLocale); - } + }; isFieldHidden = field => { const locale = this.state.selectedLocale; const { defaultLocale } = getI18nInfo(this.props.collection); return isFieldHidden(field, locale, defaultLocale); - } + }; render() { - const { collection, entry, fields, fieldsMetaData, fieldsErrors, onValidate, t } = - this.props; + const { collection, entry, fields, fieldsMetaData, fieldsErrors, onValidate, t } = this.props; if (!collection || !fields) { return null; diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 7875de4626c8..6662dcb0f050 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -259,14 +259,14 @@ export default class ListControl extends React.Component { uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`); /** * Old comment: - * + * * Always update so that each nested widget has the option to update. This is * required because ControlHOC provides a default `shouldComponentUpdate` * which only updates if the value changes, but every widget must be allowed * to override this. - * + * * New comment: - * + * * Each Widget is wrapped with EditorControl which already tries to update every time. * Is there a specific reason we need to always rerender the list? * This seems overkill. @@ -429,7 +429,7 @@ export default class ListControl extends React.Component { */ getObjectValue = idx => this.props.value.get(idx) || Map(); - handleChangeFor = memoize((index) => { + handleChangeFor = memoize(index => { return (f, newValue, newMetadata) => { const { value, metadata, onChange, field } = this.props; const collectionName = field.get('name'); @@ -445,7 +445,7 @@ export default class ListControl extends React.Component { }; onChange(value.set(index, newObjectValue), parsedMetadata); }; - }) + }); handleRemove = (index, event) => { event.preventDefault(); @@ -640,7 +640,10 @@ export default class ListControl extends React.Component { } } - getStableParentIds = memoize((parentIds, forID) => [...parentIds, forID], JSON.stringify /* Fast enough for only ids */); + getStableParentIds = memoize( + (parentIds, forID) => [...parentIds, forID], + JSON.stringify /* Fast enough for only ids */, + ); // eslint-disable-next-line react/display-name renderItem = (item, index) => { diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js index eb9f634680be..691437f0785b 100644 --- a/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js +++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/components/Shortcode.js @@ -19,20 +19,23 @@ function Shortcode(props) { const field = useMemo(() => fromJS(omit(plugin, fieldKeys)), [plugin]); const [value, setValue] = useState(fromJS(element.data[dataKey])); - const handleChange = useCallback((fieldName, value, metadata) => { - const path = ReactEditor.findPath(editor, element); - const newProperties = { - data: { - ...element.data, - [dataKey]: value.toJS(), - metadata, - }, - }; - Transforms.setNodes(editor, newProperties, { - at: path, - }); - setValue(value); - }, [editor, element, dataKey]); + const handleChange = useCallback( + (fieldName, value, metadata) => { + const path = ReactEditor.findPath(editor, element); + const newProperties = { + data: { + ...element.data, + [dataKey]: value.toJS(), + metadata, + }, + }; + Transforms.setNodes(editor, newProperties, { + at: path, + }); + setValue(value); + }, + [editor, element, dataKey], + ); function handleFocus() { const path = ReactEditor.findPath(editor, element); diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index fa1b273dd2b1..d3570adce872 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -94,7 +94,10 @@ export default class ObjectControl extends React.Component { }); }; - getStableParentIds = memoize((parentIds, forID) => [...parentIds, forID], JSON.stringify /* Fast enough for only ids */); + getStableParentIds = memoize( + (parentIds, forID) => [...parentIds, forID], + JSON.stringify /* Fast enough for only ids */, + ); controlFor(field, key) { const { From 030898fd67ab851199e3cb8c991ab7f4a7adf50e Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 15:43:06 +0100 Subject: [PATCH 08/40] Update packages/decap-cms-widget-object/src/ObjectControl.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/decap-cms-widget-object/src/ObjectControl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-widget-object/src/ObjectControl.js b/packages/decap-cms-widget-object/src/ObjectControl.js index d3570adce872..c30c2bb0336c 100644 --- a/packages/decap-cms-widget-object/src/ObjectControl.js +++ b/packages/decap-cms-widget-object/src/ObjectControl.js @@ -96,7 +96,7 @@ export default class ObjectControl extends React.Component { getStableParentIds = memoize( (parentIds, forID) => [...parentIds, forID], - JSON.stringify /* Fast enough for only ids */, + (parentIds, forID) => JSON.stringify([parentIds, forID]) /* Fast enough for only ids */, ); controlFor(field, key) { From e50ce078e884f5c9a7e60a5d3b05d5c21e317b2d Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 15:43:48 +0100 Subject: [PATCH 09/40] fix: resolver Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/decap-cms-widget-list/src/ListControl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 6662dcb0f050..705c92c0ded1 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -642,7 +642,7 @@ export default class ListControl extends React.Component { getStableParentIds = memoize( (parentIds, forID) => [...parentIds, forID], - JSON.stringify /* Fast enough for only ids */, + (parentIds, forID) => JSON.stringify({ parentIds, forID }), ); // eslint-disable-next-line react/display-name From 047d97b4179555efd474e4967770a83002644d17 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 16:04:26 +0100 Subject: [PATCH 10/40] fix: code and memoization issues --- packages/decap-cms-widget-list/src/ListControl.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js index 705c92c0ded1..c5d642ec4e64 100644 --- a/packages/decap-cms-widget-list/src/ListControl.js +++ b/packages/decap-cms-widget-list/src/ListControl.js @@ -218,7 +218,6 @@ export default class ListControl extends React.Component { listCollapsed, itemsCollapsed, value: this.valueToString(value), - valueReference: value, keys, }; } @@ -641,8 +640,8 @@ export default class ListControl extends React.Component { } getStableParentIds = memoize( - (parentIds, forID) => [...parentIds, forID], - (parentIds, forID) => JSON.stringify({ parentIds, forID }), + (parentIds, forID, key) => [...parentIds, forID, key], + (parentIds, forID, key) => JSON.stringify([ ...parentIds, forID, key ]), ); // eslint-disable-next-line react/display-name @@ -729,7 +728,7 @@ export default class ListControl extends React.Component { collapsed={collapsed} data-testid={`object-control-${key}`} hasError={hasError} - parentIds={this.getStableParentIds(parentIds, forID)} + parentIds={this.getStableParentIds(parentIds, forID, key)} /> )} From 031ac70d24bc9393ea416aafe96295bf18a26f99 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 16:07:03 +0100 Subject: [PATCH 11/40] fix: bad memoization cache key --- packages/decap-cms-core/src/actions/media.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/decap-cms-core/src/actions/media.ts b/packages/decap-cms-core/src/actions/media.ts index 4bed9715523c..bbc3fa193749 100644 --- a/packages/decap-cms-core/src/actions/media.ts +++ b/packages/decap-cms-core/src/actions/media.ts @@ -89,14 +89,11 @@ export const boundGetAsset = memoize( } return bound; - }, - function resolveCacheKey(_dispatch, collection, entry) { - // Generate a unique cache key based on the collection name and entry slug - // The dispatch function is not included in the cache key since it is stable and does not change between calls - return `${collection?.get('name')}$$${entry?.get('slug')}`; - }, + }, (_, entry) => entry ); +boundGetAsset.cache = new WeakMap(); + export function getAsset({ collection, entry, path, field }: GetAssetArgs) { return (dispatch: ThunkDispatch, getState: () => State) => { if (!path) return emptyAsset; From ce58957941d283838a88f055b3768de5fc9e2109 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 16:07:39 +0100 Subject: [PATCH 12/40] fix: editor control issues --- .../Editor/EditorControlPane/EditorControl.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 7140d1a422c7..04f07623f01c 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -189,11 +189,6 @@ class EditorControl extends React.Component { return false; }; - getEntry = () => { - // This will have the latest value even if the component doest rerender - return this.props.entry; - }; - onChange = (newValue, newMetadata) => { this.props.onChange(this.props.field, newValue, newMetadata); this.props.clearFieldErrors(this.uniqueFieldId); // We are deleting errors for this field only. @@ -346,7 +341,7 @@ class EditorControl extends React.Component { editorControl={ConnectedEditorControl} query={query} loadEntry={loadEntry} - getEntry={this.getEntry} + getEntry={getEntry} queryHits={queryHits[this.uniqueFieldId] || []} clearSearch={clearSearch} clearFieldErrors={clearFieldErrors} @@ -409,8 +404,10 @@ const stable = { // Will return the same function instance for the same collection. validateMetaField: memoize(collection => { - const state = store.getState(); - return (field, value, t) => validateMetaField(state, collection, field, value, t); + return (field, value, t) => { + const state = store.getState(); + validateMetaField(state, collection, field, value, t); + }; }), getEntry() { @@ -468,7 +465,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { ...stateProps, ...dispatchProps, ...ownProps, - boundGetAsset: dispatchProps.boundGetAsset(stateProps.collection, stateProps.entry), + boundGetAsset: dispatchProps.boundGetAsset(stateProps.collection, stateProps.getEntry()), }; } From 3a5e748a3ac7d00e9569116d6bb005c0c6089b18 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Mon, 9 Feb 2026 16:07:57 +0100 Subject: [PATCH 13/40] fix: typo on deprecation comment --- .../src/components/Editor/EditorControlPane/Widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js index ba65170164e8..14037ff8dd8e 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -68,7 +68,7 @@ export default class Widget extends Component { isEditorComponent: PropTypes.bool, isNewEditorComponent: PropTypes.bool, /** - * @deprecated Every update creates a new entry, passing a live value down is too expensive. Use the getEntry callback instead or get the value from the store directly in the widget via `useSelector` or `connect`. See + * @deprecated Every update creates a new entry, passing a live value down is too expensive. Use the getEntry callback instead or get the value from the store directly in the widget via `useSelector` or `connect`. */ entry: ImmutablePropTypes.map.isRequired, getEntry: PropTypes.func.isRequired, From e3d8cec6965f44a3597cef8ce41d56302c25539d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:47:22 +0200 Subject: [PATCH 14/40] chore(deps-dev): bump axios from 1.13.5 to 1.15.0 (#7778) Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.15.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55dfa0557667..1af7d64c53a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9055,12 +9055,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-core": { @@ -26251,8 +26253,13 @@ "license": "MIT" }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/ps-tree": { "version": "1.2.0", From 9bdb53962ec5873e55020febd22abb2cd39de215 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:19:14 +0200 Subject: [PATCH 15/40] chore(deps): bump path-to-regexp from 0.1.12 to 0.1.13 (#7769) Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 0.1.12 to 0.1.13. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13) --- updated-dependencies: - dependency-name: path-to-regexp dependency-version: 0.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1af7d64c53a2..9ed81d37a333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25617,7 +25617,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.12", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { From 5577d3d4d43a418748509356523bb1715bb42b75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:41:08 +0000 Subject: [PATCH 16/40] chore(deps): bump lodash-es from 4.17.23 to 4.18.1 (#7771) Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash-es dependency-version: 4.18.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9ed81d37a333..ba60bb22f76f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20357,7 +20357,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.camelcase": { From 12c7142cab9349fbf0875b8b0cbbc5909be6829d Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Tue, 14 Apr 2026 13:49:25 +0200 Subject: [PATCH 17/40] Add linked image support for richtext widget (#7779) * feat: add linked image support for richtext widget * chore: revert config.yml * Apply suggestion from @yanthomasdev Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> * chore: format * chore: remove useElement * fix: formatting --------- Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- .../src/index.js | 2 +- .../src/RichtextControl/VisualEditor.js | 5 +++ .../RichtextControl/__tests__/parser.spec.js | 37 ++++++++++++++++ .../components/Element/ImageElement.js | 44 +++++++++++++++++++ .../components/Element/LinkElement.js | 30 +++++++------ .../RichtextControl/plugins/ImagePlugin.js | 15 +++++++ .../src/serializers/remarkShortcodes.js | 7 +++ 7 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ImageElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ImagePlugin.js diff --git a/packages/decap-cms-editor-component-image/src/index.js b/packages/decap-cms-editor-component-image/src/index.js index 866d0a1bcbd7..9541d2236e48 100644 --- a/packages/decap-cms-editor-component-image/src/index.js +++ b/packages/decap-cms-editor-component-image/src/index.js @@ -17,7 +17,7 @@ const image = { const src = getAsset(image, imageField); return {alt; }, - pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)/, + pattern: /^!\[([^\]]*)\]\((.*?)(\s"([^"]*)")?\)/, fields: [ { label: 'Image', diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index bdafb01460a5..713f71ec253b 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -25,7 +25,9 @@ import HeadingElement from './components/Element/HeadingElement'; import ListElement from './components/Element/ListElement'; import BlockquoteElement from './components/Element/BlockquoteElement'; import LinkElement from './components/Element/LinkElement'; +import ImageElement from './components/Element/ImageElement'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; +import ImagePlugin from './plugins/ImagePlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; import { TablePlugin, TableRowPlugin, TableCellPlugin } from './plugins/TablePlugin'; import defaultEmptyBlock from './defaultEmptyBlock'; @@ -59,6 +61,7 @@ export default function VisualEditor(props) { isShowModeToggle, onChange, getEditorComponents, + getAsset, } = props; let editorComponents = getEditorComponents(); @@ -106,6 +109,7 @@ export default function VisualEditor(props) { ['ol']: withProps(ListElement, { variant: 'ol' }), ['li']: withProps(ListElement, { variant: 'li' }), ['blockquote']: BlockquoteElement, + ['image']: withProps(ImageElement, { getAsset, field }), }, }, plugins: [ @@ -129,6 +133,7 @@ export default function VisualEditor(props) { shortcuts: { toggle: { keys: 'mod+shift+c' } }, }), ListPlugin, + ImagePlugin, LinkPlugin.configure({ node: { component: LinkElement }, shortcuts: { diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js index ccb87e3648ea..ced4bcadb991 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/__tests__/parser.spec.js @@ -534,6 +534,43 @@ Array [ `); }); + it('should compile linked images', () => { + const value = ` +[![Project logo](https://raw.githubusercontent.com/decaporg/decap-cms/main/img/decap.svg)](https://decapcms.org) +`; + expect(parser(value)).toMatchInlineSnapshot(` +Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "", + }, + ], + "data": Object { + "alt": "Project logo", + "title": null, + "url": "https://raw.githubusercontent.com/decaporg/decap-cms/main/img/decap.svg", + }, + "type": "image", + }, + ], + "data": Object { + "title": null, + "url": "https://decapcms.org", + }, + "type": "a", + }, + ], + "type": "p", + }, +] +`); + }); + it('should compile plugins', () => { const value = ` ![test](test.png) diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ImageElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ImageElement.js new file mode 100644 index 000000000000..0236dff2844e --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/ImageElement.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { PlateElement } from 'platejs/react'; + +function isAbsoluteAssetUrl(url) { + return /^(?:[a-z]+:)?\/\//i.test(url) || url.startsWith('data:') || url.startsWith('blob:'); +} + +function resolveImageSource(url, getAsset, field) { + if (!url) { + return ''; + } + + if (!getAsset || isAbsoluteAssetUrl(url)) { + return url; + } + + const asset = getAsset(url, field); + return asset && typeof asset.toString === 'function' ? asset.toString() : asset; +} + +function ImageElement({ children, element, getAsset, field, ...props }) { + const { alt, title, url } = element?.data || {}; + const src = resolveImageSource(url, getAsset, field); + + return ( + + {alt + {children} + + ); +} + +export default ImageElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js index 85d885812f5b..1257b915f0a4 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/LinkElement.js @@ -1,20 +1,24 @@ import React from 'react'; -import styled from '@emotion/styled'; -import { PlateLeaf, useElement } from 'platejs/react'; +import { PlateElement } from 'platejs/react'; import { useLink } from '@platejs/link/react'; -const StyledA = styled.a` - text-decoration: underline; - font-size: inherit; -`; - -function LinkElement({ children, ...rest }) { - const element = useElement(); - const { props } = useLink({ element }); +function LinkElement({ children, element, ...rest }) { + const { props: linkProps } = useLink({ element }); return ( - - {children} - + + {children} + ); } diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ImagePlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ImagePlugin.js new file mode 100644 index 000000000000..57c2ef94e3dc --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/ImagePlugin.js @@ -0,0 +1,15 @@ +import { createSlatePlugin } from 'platejs'; +import { toPlatePlugin } from 'platejs/react'; + +const plugin = createSlatePlugin({ + key: 'image', + node: { + isElement: true, + isInline: true, + isVoid: true, + }, +}); + +const ImagePlugin = toPlatePlugin(plugin); + +export default ImagePlugin; diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js index 6a31ace3393b..16fb5768704d 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js +++ b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js @@ -73,6 +73,13 @@ function createShortcodeTokenizer({ plugins }) { const shortcodeData = plugin.fromBlock(match); + // Only eat if the match starts at the beginning of the remaining text. + // If it's further in (e.g. found via matchFromLines inside a paragraph), + // fall through silently so remark's inline parsers can handle it. + if (value.slice(0, match[0].length) !== match[0]) { + return; + } + try { return eat(match[0])({ type: 'shortcode', From c98e72286706073a47c1a1e3af4d21f79a8013cc Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Wed, 15 Apr 2026 08:34:56 +0200 Subject: [PATCH 18/40] feat: add break element to widget richtext (#7782) * feat: add break element to widget richtext * fix: format * chore: revert config * chore: revert config --- .../src/RichtextControl/VisualEditor.js | 2 + .../components/Element/BreakElement.js | 12 +++++ .../RichtextControl/plugins/BreakPlugin.js | 18 +++++++ .../src/serializers/__tests__/index.spec.js | 47 ++++++++++++++++++- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BreakElement.js create mode 100644 packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BreakPlugin.js diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js index 713f71ec253b..adf166c689ca 100644 --- a/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/VisualEditor.js @@ -28,6 +28,7 @@ import LinkElement from './components/Element/LinkElement'; import ImageElement from './components/Element/ImageElement'; import ExtendedBlockquotePlugin from './plugins/ExtendedBlockquotePlugin'; import ImagePlugin from './plugins/ImagePlugin'; +import BreakPlugin from './plugins/BreakPlugin'; import ShortcodePlugin from './plugins/ShortcodePlugin'; import { TablePlugin, TableRowPlugin, TableCellPlugin } from './plugins/TablePlugin'; import defaultEmptyBlock from './defaultEmptyBlock'; @@ -133,6 +134,7 @@ export default function VisualEditor(props) { shortcuts: { toggle: { keys: 'mod+shift+c' } }, }), ListPlugin, + BreakPlugin, ImagePlugin, LinkPlugin.configure({ node: { component: LinkElement }, diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BreakElement.js b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BreakElement.js new file mode 100644 index 000000000000..17c7ee1319b6 --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/components/Element/BreakElement.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { PlateElement } from 'platejs/react'; + +function BreakElement(props) { + return ( + +
+
+ ); +} + +export default BreakElement; diff --git a/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BreakPlugin.js b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BreakPlugin.js new file mode 100644 index 000000000000..730c4847d54b --- /dev/null +++ b/packages/decap-cms-widget-richtext/src/RichtextControl/plugins/BreakPlugin.js @@ -0,0 +1,18 @@ +import { createSlatePlugin } from 'platejs'; +import { toPlatePlugin } from 'platejs/react'; + +import BreakElement from '../components/Element/BreakElement'; + +const plugin = createSlatePlugin({ + key: 'break', + node: { + isElement: true, + isInline: true, + isVoid: true, + component: BreakElement, + }, +}); + +const BreakPlugin = toPlatePlugin(plugin); + +export default BreakPlugin; diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js index 33a4fac515ac..187462e46059 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js +++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/index.spec.js @@ -1,7 +1,8 @@ import path from 'path'; import fs from 'fs'; +import { Map } from 'immutable'; -import { markdownToSlate, htmlToSlate } from '../'; +import { markdownToSlate, htmlToSlate, slateToMarkdown } from '../'; describe('markdownToSlate', () => { it('should not add duplicate identical marks under the same node (GitHub Issue 3280)', () => { @@ -31,6 +32,50 @@ describe('markdownToSlate', () => { }); }); +describe('slateToMarkdown', () => { + it('should preserve hard line breaks created with trailing backslashes for Decap timeline text', () => { + const slate = [ + { + type: 'p', + children: [ + { text: '2022', bold: true, marks: [{ type: 'bold' }] }, + { type: 'break', children: [{ text: '' }] }, + { text: 'Netlify CMS was renamed to Decap CMS.' }, + ], + }, + { + type: 'p', + children: [ + { text: '2023', bold: true, marks: [{ type: 'bold' }] }, + { type: 'break', children: [{ text: '' }] }, + { + text: 'The richtext widget added container editor components for richer editorial workflows.', + }, + ], + }, + { + type: 'p', + children: [ + { text: '2026', bold: true, marks: [{ type: 'bold' }] }, + { type: 'break', children: [{ text: '' }] }, + { text: 'Editors still expect hard line breaks to survive visual mode round-trips.' }, + ], + }, + ]; + + const markdown = slateToMarkdown(slate, {}, Map()); + + expect(markdown).toBe( + '**2022**\\\n' + + 'Netlify CMS was renamed to Decap CMS.\n\n' + + '**2023**\\\n' + + 'The richtext widget added container editor components for richer editorial workflows.\n\n' + + '**2026**\\\n' + + 'Editors still expect hard line breaks to survive visual mode round-trips.', + ); + }); +}); + describe('htmlToSlate', () => { it('should preserve spaces in rich html (GitHub Issue 3727)', () => { const html = `Bold Text regular text `; From 29634308d9b432837d4e2975c7606be76567f7e7 Mon Sep 17 00:00:00 2001 From: Sem Postma Date: Wed, 15 Apr 2026 11:09:31 +0200 Subject: [PATCH 19/40] Fix/propagate errors for i18n entries (#7773) * fix: throw proper errors when i18n entries cannot be retrieved * fix: format i18n code * fix: typescript errors for older versions * fix: typing issue Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --------- Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- packages/decap-cms-core/src/lib/i18n.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/decap-cms-core/src/lib/i18n.ts b/packages/decap-cms-core/src/lib/i18n.ts index f7a05aa6acaa..936aac538019 100644 --- a/packages/decap-cms-core/src/lib/i18n.ts +++ b/packages/decap-cms-core/src/lib/i18n.ts @@ -308,18 +308,26 @@ export async function getI18nEntry( if (structure === I18N_STRUCTURE.SINGLE_FILE) { entryValue = mergeSingleFileValue(await getEntryValue(path), defaultLocale, locales); } else { - const entryValues = await Promise.all( + const entryValuesResults = await Promise.allSettled( locales.map(async locale => { const entryPath = getFilePath(structure, extension, path, slug, locale); - const value = await getEntryValue(entryPath).catch(() => null); + const value = await getEntryValue(entryPath); return { value, locale }; }), ); - const nonNullValues = entryValues.filter(e => e.value !== null) as { - value: EntryValue; - locale: string; - }[]; + const nonNullValues = entryValuesResults + .map(e => (e.status === 'fulfilled' ? e.value : undefined)) + .filter((e): e is { value: EntryValue; locale: string } => e !== undefined); + + if (nonNullValues.length === 0) { + // mergeValues will throw on an empty list, and show the error messages. + const [error = new Error('No entry values found for any locale')] = entryValuesResults + .map(e => (e.status === 'rejected' ? e.reason : undefined)) + .filter(e => e !== undefined); + + throw error; + } entryValue = mergeValues(collection, structure, defaultLocale, nonNullValues); } From e5621454dd3477d82ddb080c46a5a8296ce38a14 Mon Sep 17 00:00:00 2001 From: Affi Date: Wed, 15 Apr 2026 15:13:10 +0530 Subject: [PATCH 20/40] fix(frontmatter): improve duplicate frontmatter key error handling (#7679) * fix(frontmatter): improve duplicate frontmatter key error handling * fix(frontmatter): improve duplicate key warning with file and line info * fix(frontmatter): made duplicate detection path awarness using indendation * fix(frontmatter): removed backslash * feat: use `yaml` diagnostics --------- Co-authored-by: Martin Jagodic Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- .../src/formats/__tests__/frontmatter.spec.js | 21 +++++++++++++++ .../src/formats/__tests__/yaml.spec.js | 27 +++++++++++++++++++ packages/decap-cms-core/src/formats/yaml.ts | 17 +++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js b/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js index 2f14d7324c12..428284c448de 100644 --- a/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js +++ b/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js @@ -72,6 +72,27 @@ describe('Frontmatter', () => { }); }); + it('should throw on duplicate frontmatter keys', () => { + expect(() => + FrontmatterInfer.fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'), + ).toThrow(/Map keys must be unique/); + }); + + it('should throw on duplicate frontmatter keys with explicit YAML format', () => { + expect(() => + frontmatterYAML().fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'), + ).toThrow(/Map keys must be unique/); + }); + + it('should not throw when body contains YAML-like patterns', () => { + expect( + FrontmatterInfer.fromFile('---\ntitle: Hello\n---\ntitle: this is not a duplicate'), + ).toEqual({ + title: 'Hello', + body: 'title: this is not a duplicate', + }); + }); + it('should stringify YAML with --- delimiters', () => { expect( FrontmatterInfer.toFile({ diff --git a/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js b/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js index bff71d7c4377..79f9edae67be 100644 --- a/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js +++ b/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js @@ -85,6 +85,33 @@ describe('yaml', () => { time: '10:05', }); }); + + test('throws on duplicate keys', () => { + expect(() => yaml.fromFile('title: Hello\ntitle: World')).toThrow( + /Map keys must be unique; "title" is repeated/, + ); + }); + + test('throws on duplicate nested keys', () => { + expect(() => yaml.fromFile('nested:\n a: 1\n a: 2')).toThrow( + /Map keys must be unique; "a" is repeated/, + ); + }); + + test('does not throw when same key appears in different nested objects', () => { + expect(yaml.fromFile('obj1:\n name: foo\nobj2:\n name: bar')).toEqual({ + obj1: { name: 'foo' }, + obj2: { name: 'bar' }, + }); + }); + + test('logs warnings to console.warn', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + // Valid YAML should produce no warnings + yaml.fromFile('title: Hello'); + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); }); describe('toFile', () => { test('outputs valid yaml', () => { diff --git a/packages/decap-cms-core/src/formats/yaml.ts b/packages/decap-cms-core/src/formats/yaml.ts index 923945b16240..bd2f42fe71c6 100644 --- a/packages/decap-cms-core/src/formats/yaml.ts +++ b/packages/decap-cms-core/src/formats/yaml.ts @@ -41,7 +41,22 @@ export default { if (content && content.trim().endsWith('---')) { content = content.trim().slice(0, -3); } - return yaml.parse(content, { customTags: [timestampTag] }); + + const doc = yaml.parseDocument(content, { + customTags: [timestampTag], + prettyErrors: true, + }); + + for (const warn of doc.warnings) { + console.warn(`YAML warning: ${warn.message}`); + } + + if (doc.errors.length > 0) { + const messages = doc.errors.map(e => e.message).join('\n'); + throw new Error(`YAML parsing error:\n${messages}`); + } + + return doc.toJSON(); }, toFile(data: object, sortedKeys: string[] = [], comments: Record = {}) { From 8fab8237c4a6e3c73fda505addd2f5704f660031 Mon Sep 17 00:00:00 2001 From: Yan <61414485+yanthomasdev@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:37:36 -0300 Subject: [PATCH 21/40] feat: remove HTML comments from pasted content (#7775) Co-authored-by: Kavanaugh Latiolais --- package-lock.json | 35 +++++++++++++++++++ .../decap-cms-widget-markdown/package.json | 1 + .../src/serializers/__tests__/index.spec.js | 15 ++++++++ .../src/serializers/index.js | 2 ++ .../decap-cms-widget-richtext/package.json | 1 + .../src/serializers/index.js | 2 ++ 6 files changed, 56 insertions(+) diff --git a/package-lock.json b/package-lock.json index ba60bb22f76f..fa4745f697fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15755,6 +15755,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-is-conditional-comment": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-is-conditional-comment/-/hast-util-is-conditional-comment-1.0.4.tgz", + "integrity": "sha512-rtULxWWknVeSuU/vsJ9tHo+M3ExyaOrZcWvLxqY2nUfCHbDcq60EJzSJC5zNm6ZlbxbJ8l7Ej8C1Kzsi5PJS1A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-element": { "version": "1.1.0", "license": "MIT", @@ -27559,6 +27569,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-remove-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rehype-remove-comments/-/rehype-remove-comments-4.0.2.tgz", + "integrity": "sha512-E2FNohTuIs7QzUnEQs3SdYdCScsTgUN7yPeDNWi+gsvx+pbLzIAyp27TWz3Gm64jpdLi7/6HxyRHxdd1NVQ37A==", + "license": "MIT", + "dependencies": { + "hast-util-is-conditional-comment": "^1.0.0", + "unist-util-filter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "7.0.0", "license": "MIT", @@ -31430,6 +31454,15 @@ "object-assign": "^4.1.0" } }, + "node_modules/unist-util-filter": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-2.0.3.tgz", + "integrity": "sha512-8k6Jl/KLFqIRTHydJlHh6+uFgqYHq66pV75pZgr1JwfyFSjbWb12yfb0yitW/0TbHXjr9U4G9BQpOvMANB+ExA==", + "license": "MIT", + "dependencies": { + "unist-util-is": "^4.0.0" + } + }, "node_modules/unist-util-find-after": { "version": "3.0.0", "license": "MIT", @@ -33763,6 +33796,7 @@ "mdast-util-to-string": "^1.0.5", "rehype-parse": "^6.0.0", "rehype-remark": "^8.0.0", + "rehype-remove-comments": "^4.0.2", "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", @@ -33914,6 +33948,7 @@ "platejs": "^49.2.21", "rehype-parse": "^6.0.0", "rehype-remark": "^8.0.0", + "rehype-remove-comments": "^4.0.2", "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json index 34e4713db39a..7305f725606a 100644 --- a/packages/decap-cms-widget-markdown/package.json +++ b/packages/decap-cms-widget-markdown/package.json @@ -29,6 +29,7 @@ "mdast-util-to-string": "^1.0.5", "rehype-parse": "^6.0.0", "rehype-remark": "^8.0.0", + "rehype-remove-comments": "^4.0.2", "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", diff --git a/packages/decap-cms-widget-markdown/src/serializers/__tests__/index.spec.js b/packages/decap-cms-widget-markdown/src/serializers/__tests__/index.spec.js index 1d828dadb46f..18b71d50edef 100644 --- a/packages/decap-cms-widget-markdown/src/serializers/__tests__/index.spec.js +++ b/packages/decap-cms-widget-markdown/src/serializers/__tests__/index.spec.js @@ -49,4 +49,19 @@ describe('htmlToSlate', () => { ], }); }); + + it('should remove HTML comments', () => { + const html = `regular text`; + + const actual = htmlToSlate(html); + expect(actual).toEqual({ + type: 'root', + children: [ + { + type: 'paragraph', + children: [{ text: 'regular text' }], + }, + ], + }); + }); }); diff --git a/packages/decap-cms-widget-markdown/src/serializers/index.js b/packages/decap-cms-widget-markdown/src/serializers/index.js index fd2218295ee6..aa7f2cabe43b 100644 --- a/packages/decap-cms-widget-markdown/src/serializers/index.js +++ b/packages/decap-cms-widget-markdown/src/serializers/index.js @@ -7,6 +7,7 @@ import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; +import rehypeRemoveComments from 'rehype-remove-comments'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; import rehypePaperEmoji from './rehypePaperEmoji'; @@ -198,6 +199,7 @@ export function htmlToSlate(html) { const mdast = unified() .use(rehypePaperEmoji) + .use(rehypeRemoveComments, { removeConditional: true }) .use(rehypeToRemark, { minify: false }) .runSync(hast); diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json index ce56821377dd..587b7f062469 100644 --- a/packages/decap-cms-widget-richtext/package.json +++ b/packages/decap-cms-widget-richtext/package.json @@ -31,6 +31,7 @@ "platejs": "^49.2.21", "rehype-parse": "^6.0.0", "rehype-remark": "^8.0.0", + "rehype-remove-comments": "^4.0.2", "rehype-stringify": "^7.0.0", "remark-parse": "^6.0.3", "remark-rehype": "^4.0.0", diff --git a/packages/decap-cms-widget-richtext/src/serializers/index.js b/packages/decap-cms-widget-richtext/src/serializers/index.js index 1f8e69e9190e..96e0cdf841db 100644 --- a/packages/decap-cms-widget-richtext/src/serializers/index.js +++ b/packages/decap-cms-widget-richtext/src/serializers/index.js @@ -7,6 +7,7 @@ import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; +import rehypeRemoveComments from 'rehype-remove-comments'; import { Map } from 'immutable'; import remarkToRehypeShortcodes from './remarkRehypeShortcodes'; @@ -202,6 +203,7 @@ export function htmlToSlate(html) { const mdast = unified() .use(rehypePaperEmoji) + .use(rehypeRemoveComments, { removeConditional: true }) .use(rehypeToRemark, { minify: false }) .runSync(hast); From 7dd2de7db5388c2e8253b670d9194c5a12d1686d Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Wed, 15 Apr 2026 13:12:07 +0200 Subject: [PATCH 22/40] perf: use Clipboard API (#7780) --- package-lock.json | 11 ----------- packages/decap-cms-core/package.json | 1 - .../components/MediaLibrary/MediaLibraryButtons.js | 5 ++--- .../decap-cms-core/src/components/UI/ErrorBoundary.js | 3 +-- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa4745f697fb..a5c95406af32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11263,16 +11263,6 @@ "node": ">=0.10.0" } }, - "node_modules/copy-text-to-clipboard": { - "version": "3.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", "dev": true, @@ -33445,7 +33435,6 @@ "@reduxjs/toolkit": "^1.9.1", "@vercel/stega": "^0.1.2", "buffer": "^6.0.3", - "copy-text-to-clipboard": "^3.0.0", "dayjs": "^1.11.10", "deepmerge": "^4.2.2", "diacritics": "^1.3.0", diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json index bcfecf362dba..95f7e88ac714 100644 --- a/packages/decap-cms-core/package.json +++ b/packages/decap-cms-core/package.json @@ -28,7 +28,6 @@ "@reduxjs/toolkit": "^1.9.1", "@vercel/stega": "^0.1.2", "buffer": "^6.0.3", - "copy-text-to-clipboard": "^3.0.0", "dayjs": "^1.11.10", "deepmerge": "^4.2.2", "diacritics": "^1.3.0", diff --git a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js index f3d7e726b335..fa3eb19b057e 100644 --- a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js +++ b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import copyToClipboard from 'copy-text-to-clipboard'; import { isAbsolutePath } from 'decap-cms-lib-util'; import { buttons, shadows, zIndex } from 'decap-cms-ui-default'; @@ -87,10 +86,10 @@ export class CopyToClipBoardButton extends React.Component { this.mounted = false; } - handleCopy = () => { + handleCopy = async () => { clearTimeout(this.timeout); const { path, draft, name } = this.props; - copyToClipboard(isAbsolutePath(path) || !draft ? path : name); + await navigator.clipboard.writeText(isAbsolutePath(path) || !draft ? path : name); this.setState({ copied: true }); this.timeout = setTimeout(() => this.mounted && this.setState({ copied: false }), 1500); }; diff --git a/packages/decap-cms-core/src/components/UI/ErrorBoundary.js b/packages/decap-cms-core/src/components/UI/ErrorBoundary.js index c0ce99e8f5ad..77186eb1fa70 100644 --- a/packages/decap-cms-core/src/components/UI/ErrorBoundary.js +++ b/packages/decap-cms-core/src/components/UI/ErrorBoundary.js @@ -4,7 +4,6 @@ import { translate } from 'react-polyglot'; import styled from '@emotion/styled'; import yaml from 'yaml'; import truncate from 'lodash/truncate'; -import copyToClipboard from 'copy-text-to-clipboard'; import { localForage } from 'decap-cms-lib-util'; import { buttons, colors } from 'decap-cms-ui-default'; @@ -117,7 +116,7 @@ function RecoveredEntry({ entry, t }) {

{t('ui.errorBoundary.recoveredEntry.heading')}

{t('ui.errorBoundary.recoveredEntry.warning')} - copyToClipboard(entry)}> + navigator.clipboard.writeText(entry)}> {t('ui.errorBoundary.recoveredEntry.copyButtonLabel')}

From ee3919b6b1adaf44e1ebb5786173c7d700ccbede Mon Sep 17 00:00:00 2001
From: Simon Ser 
Date: Wed, 15 Apr 2026 14:17:33 +0200
Subject: [PATCH 23/40] feat: add config option to enable Signed-off-by in
 commit messages (#7766)

* refactor: add email field to User type

Most backends don't need any change because they already populate
this field.

* feat: add config option to enable Signed-off-by in commit messages

Some organizations require Signed-off-by trailers in commit
messages (e.g. for Developer Certificate of Origin). GitHub has a
setting to enable this when authoring changes from the Web UI.
This patch adds a similar setting for DecapCMS.

Closes: https://github.com/decaporg/decap-cms/issues/7730

---------

Co-authored-by: Martin Jagodic 
---
 .../src/implementation.ts                     |  1 +
 packages/decap-cms-backend-gitea/src/API.ts   |  2 +-
 .../src/implementation.tsx                    |  1 +
 packages/decap-cms-backend-github/src/API.ts  |  3 ++-
 packages/decap-cms-core/index.d.ts            |  1 +
 packages/decap-cms-core/src/backend.ts        |  4 ++++
 .../src/lib/__tests__/formatters.spec.js      | 21 +++++++++++++++++++
 packages/decap-cms-core/src/lib/formatters.ts | 18 +++++++++++++---
 packages/decap-cms-core/src/types/redux.ts    |  1 +
 .../decap-cms-lib-util/src/implementation.ts  |  1 +
 10 files changed, 48 insertions(+), 5 deletions(-)

diff --git a/packages/decap-cms-backend-git-gateway/src/implementation.ts b/packages/decap-cms-backend-git-gateway/src/implementation.ts
index 25ab9d07f8d6..ead0c0adc25b 100644
--- a/packages/decap-cms-backend-git-gateway/src/implementation.ts
+++ b/packages/decap-cms-backend-git-gateway/src/implementation.ts
@@ -381,6 +381,7 @@ export default class GitGateway implements Implementation {
       return {
         name: userData.name,
         login: userData.email,
+        email: userData.email,
         avatar_url: userData.avatar_url,
       } as unknown as User;
     });
diff --git a/packages/decap-cms-backend-gitea/src/API.ts b/packages/decap-cms-backend-gitea/src/API.ts
index 18b4f91fd4aa..ed743f6e2065 100644
--- a/packages/decap-cms-backend-gitea/src/API.ts
+++ b/packages/decap-cms-backend-gitea/src/API.ts
@@ -131,7 +131,7 @@ export default class API {
 
   static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Static CMS';
 
-  user(): Promise<{ full_name: string; login: string; avatar_url: string }> {
+  user(): Promise<{ full_name: string; login: string; email: string; avatar_url: string }> {
     if (!this._userPromise) {
       this._userPromise = this.getUser();
     }
diff --git a/packages/decap-cms-backend-gitea/src/implementation.tsx b/packages/decap-cms-backend-gitea/src/implementation.tsx
index 8584dffdf935..da05a1cc241c 100644
--- a/packages/decap-cms-backend-gitea/src/implementation.tsx
+++ b/packages/decap-cms-backend-gitea/src/implementation.tsx
@@ -183,6 +183,7 @@ export default class Gitea implements Implementation {
     return {
       name: user.full_name,
       login: user.login,
+      email: user.email,
       avatar_url: user.avatar_url,
       token: state.token as string,
     };
diff --git a/packages/decap-cms-backend-github/src/API.ts b/packages/decap-cms-backend-github/src/API.ts
index 5120c0286195..13fb7327b692 100644
--- a/packages/decap-cms-backend-github/src/API.ts
+++ b/packages/decap-cms-backend-github/src/API.ts
@@ -253,13 +253,14 @@ export default class API {
 
   static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Decap CMS';
 
-  user(): Promise<{ name: string; login: string }> {
+  user(): Promise<{ name: string; login: string; email?: string }> {
     if (!this._userPromise) {
       this._userPromise = this.getUser({ token: this.token });
     }
     return this._userPromise.then(user => ({
       name: user.name || 'Unknown',
       login: user.login,
+      email: user.email ?? undefined,
     }));
   }
 
diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts
index 8c41d54f4450..6c47c278db2b 100644
--- a/packages/decap-cms-core/index.d.ts
+++ b/packages/decap-cms-core/index.d.ts
@@ -364,6 +364,7 @@ declare module 'decap-cms-core' {
     auth_type?: 'implicit' | 'pkce';
     cms_label_prefix?: string;
     squash_merges?: boolean;
+    signoff_commits?: boolean;
     proxy_url?: string;
     commit_messages?: {
       create?: string;
diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts
index 0025b26f4308..6e5f03d9e068 100644
--- a/packages/decap-cms-core/src/backend.ts
+++ b/packages/decap-cms-core/src/backend.ts
@@ -1178,6 +1178,7 @@ export class Backend {
         path,
         authorLogin: user.login,
         authorName: user.name,
+        authorEmail: user.email,
       },
       user.useOpenAuthoring,
     );
@@ -1253,6 +1254,7 @@ export class Backend {
           path: file.path,
           authorLogin: user.login,
           authorName: user.name,
+          authorEmail: user.email,
         },
         user.useOpenAuthoring,
       ),
@@ -1279,6 +1281,7 @@ export class Backend {
         path,
         authorLogin: user.login,
         authorName: user.name,
+        authorEmail: user.email,
       },
       user.useOpenAuthoring,
     );
@@ -1303,6 +1306,7 @@ export class Backend {
         path,
         authorLogin: user.login,
         authorName: user.name,
+        authorEmail: user.email,
       },
       user.useOpenAuthoring,
     );
diff --git a/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js b/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
index efa44e5bd1f8..00bf5896b444 100644
--- a/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
+++ b/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
@@ -247,6 +247,27 @@ describe('formatters', () => {
         'Ignoring unknown variable “author-email” in open authoring message template.',
       );
     });
+
+    it('should return commit with trailer when signoff_commits is enabled', () => {
+      const collection = Map({ label_singular: 'Collection' });
+      const config = {
+        backend: {
+          signoff_commits: true,
+        },
+      };
+
+      expect(
+        commitMessageFormatter('create', config, {
+          slug: 'doc-slug',
+          path: 'file-path',
+          collection,
+          authorName: 'Test User',
+          authorEmail: 'test-user@example.org',
+        }),
+      ).toEqual(
+        'Create Collection “doc-slug”\n\nSigned-off-by: Test User \n',
+      );
+    });
   });
 
   describe('prepareSlug', () => {
diff --git a/packages/decap-cms-core/src/lib/formatters.ts b/packages/decap-cms-core/src/lib/formatters.ts
index a958cd148e14..239fe3f2f62a 100644
--- a/packages/decap-cms-core/src/lib/formatters.ts
+++ b/packages/decap-cms-core/src/lib/formatters.ts
@@ -44,16 +44,28 @@ type Options = {
   collection?: Collection;
   authorLogin?: string;
   authorName?: string;
+  authorEmail?: string;
 };
 
 export function commitMessageFormatter(
   type: keyof typeof commitMessageTemplates,
   config: CmsConfig,
-  { slug, path, collection, authorLogin, authorName }: Options,
+  { slug, path, collection, authorLogin, authorName, authorEmail }: Options,
   isOpenAuthoring?: boolean,
 ) {
   const templates = { ...commitMessageTemplates, ...(config.backend.commit_messages || {}) };
 
+  let trailers = '';
+  if (config.backend.signoff_commits) {
+    if (!authorName) {
+      console.warn('Option signoff_commits is enabled, but author name is unknown');
+    } else if (!authorEmail) {
+      console.warn('Option signoff_commits is enabled, but author email is unknown');
+    } else {
+      trailers = `\n\nSigned-off-by: ${authorName} <${authorEmail}>\n`;
+    }
+  }
+
   const commitMessage = templates[type].replace(variableRegex, (_, variable) => {
     switch (variable) {
       case 'slug':
@@ -73,7 +85,7 @@ export function commitMessageFormatter(
   });
 
   if (!isOpenAuthoring) {
-    return commitMessage;
+    return commitMessage + trailers;
   }
 
   const message = templates.openAuthoring.replace(variableRegex, (_, variable) => {
@@ -90,7 +102,7 @@ export function commitMessageFormatter(
     }
   });
 
-  return message;
+  return message + trailers;
 }
 
 export function prepareSlug(slug: string) {
diff --git a/packages/decap-cms-core/src/types/redux.ts b/packages/decap-cms-core/src/types/redux.ts
index 4587741e24a5..294d118f534b 100644
--- a/packages/decap-cms-core/src/types/redux.ts
+++ b/packages/decap-cms-core/src/types/redux.ts
@@ -377,6 +377,7 @@ export interface CmsBackend {
   auth_endpoint?: string;
   cms_label_prefix?: string;
   squash_merges?: boolean;
+  signoff_commits?: boolean;
   proxy_url?: string;
   commit_messages?: {
     create?: string;
diff --git a/packages/decap-cms-lib-util/src/implementation.ts b/packages/decap-cms-lib-util/src/implementation.ts
index 52539852bde8..3080bf99072f 100644
--- a/packages/decap-cms-lib-util/src/implementation.ts
+++ b/packages/decap-cms-lib-util/src/implementation.ts
@@ -90,6 +90,7 @@ export type Credentials = { token: string | {}; refresh_token?: string };
 export type User = Credentials & {
   backendName?: string;
   login?: string;
+  email?: string;
   name: string;
   useOpenAuthoring?: boolean;
 };

From 71d01f1317ed18e21fa37cf844eda39ca6b7e216 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 16 Apr 2026 07:51:26 +0200
Subject: [PATCH 24/40] chore(deps): bump dompurify from 3.3.3 to 3.4.0 (#7784)

Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.3 to 3.4.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.3...3.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 package-lock.json                               | 10 +++++-----
 packages/decap-cms-widget-markdown/package.json |  2 +-
 packages/decap-cms-widget-richtext/package.json |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index a5c95406af32..ddbcc837913a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12550,9 +12550,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
-      "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
+      "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
       "license": "(MPL-2.0 OR Apache-2.0)",
       "optionalDependencies": {
         "@types/trusted-types": "^2.0.7"
@@ -33778,7 +33778,7 @@
       "license": "MIT",
       "dependencies": {
         "detab": "^2.0.4",
-        "dompurify": "^3.3.3",
+        "dompurify": "^3.4.0",
         "is-hotkey": "^0.2.0",
         "is-url": "^1.2.4",
         "mdast-util-definitions": "^1.2.3",
@@ -33932,7 +33932,7 @@
         "@platejs/list": "^50.2.0",
         "@platejs/list-classic": "^49.1.0",
         "class-variance-authority": "^0.7.0",
-        "dompurify": "^3.2.6",
+        "dompurify": "^3.4.0",
         "lucide-react": "^0.331.0",
         "platejs": "^49.2.21",
         "rehype-parse": "^6.0.0",
diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json
index 7305f725606a..feb65546c0c5 100644
--- a/packages/decap-cms-widget-markdown/package.json
+++ b/packages/decap-cms-widget-markdown/package.json
@@ -22,7 +22,7 @@
   },
   "dependencies": {
     "detab": "^2.0.4",
-    "dompurify": "^3.3.3",
+    "dompurify": "^3.4.0",
     "is-hotkey": "^0.2.0",
     "is-url": "^1.2.4",
     "mdast-util-definitions": "^1.2.3",
diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json
index 587b7f062469..4f3e11c0cfe6 100644
--- a/packages/decap-cms-widget-richtext/package.json
+++ b/packages/decap-cms-widget-richtext/package.json
@@ -22,11 +22,11 @@
   },
   "dependencies": {
     "@platejs/basic-nodes": "^49.0.0",
-    "dompurify": "^3.2.6",
     "@platejs/link": "^50.2.7",
     "@platejs/list": "^50.2.0",
     "@platejs/list-classic": "^49.1.0",
     "class-variance-authority": "^0.7.0",
+    "dompurify": "^3.4.0",
     "lucide-react": "^0.331.0",
     "platejs": "^49.2.21",
     "rehype-parse": "^6.0.0",

From 009ce09fd502942e78cf2a11c8adc20b986e0b39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alja=C5=BE?= 
Date: Thu, 16 Apr 2026 09:32:50 +0200
Subject: [PATCH 25/40] fix(markdown): extra whitespace before and after pasted
 content #7364 (#7440)

* fix(markdown): extra whitespace before and after pasted content #7364

* fix(markdown): better solution for extra whitespace before and after pasted content #7364

---------

Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 .../src/MarkdownControl/plugins/html/withHtml.js                | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js
index 9dc9a5d73af7..9039db7f871d 100644
--- a/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js
+++ b/packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js
@@ -37,7 +37,7 @@ const INLINE_STYLES = {
 
 function deserialize(el) {
   if (el.nodeType === 3) {
-    return el.textContent;
+    return el.textContent.replace(/(\r)?\n/g, '');
   } else if (el.nodeType !== 1) {
     return null;
   } else if (el.nodeName === 'BR') {

From f57da6f2e142b78ff6848afdca887d32c72becbc Mon Sep 17 00:00:00 2001
From: Martin Jagodic 
Date: Thu, 16 Apr 2026 10:28:41 +0200
Subject: [PATCH 26/40] chore(release): publish

 - decap-cms@3.12.0
 - decap-cms-app@3.12.0
 - decap-cms-backend-git-gateway@3.6.0
 - decap-cms-backend-gitea@3.4.0
 - decap-cms-backend-github@3.6.0
 - decap-cms-core@3.12.0
 - decap-cms-editor-component-image@3.3.0
 - decap-cms-lib-util@3.5.0
 - decap-cms-ui-default@3.6.0
 - decap-cms-widget-markdown@3.8.0
 - decap-cms-widget-object@3.5.0
 - decap-cms-widget-richtext@3.2.0
 - decap-server@3.7.0
---
 package-lock.json                             | 48 +++++++++----------
 packages/decap-cms-app/CHANGELOG.md           |  4 ++
 packages/decap-cms-app/package.json           | 20 ++++----
 .../CHANGELOG.md                              |  6 +++
 .../package.json                              |  2 +-
 packages/decap-cms-backend-gitea/CHANGELOG.md |  6 +++
 packages/decap-cms-backend-gitea/package.json |  2 +-
 .../decap-cms-backend-github/CHANGELOG.md     |  6 +++
 .../decap-cms-backend-github/package.json     |  2 +-
 packages/decap-cms-core/CHANGELOG.md          | 14 ++++++
 packages/decap-cms-core/package.json          |  2 +-
 .../CHANGELOG.md                              |  4 ++
 .../package.json                              |  2 +-
 packages/decap-cms-lib-util/CHANGELOG.md      |  6 +++
 packages/decap-cms-lib-util/package.json      |  2 +-
 packages/decap-cms-ui-default/CHANGELOG.md    |  4 ++
 packages/decap-cms-ui-default/package.json    |  2 +-
 .../decap-cms-widget-markdown/CHANGELOG.md    | 10 ++++
 .../decap-cms-widget-markdown/package.json    |  2 +-
 packages/decap-cms-widget-object/CHANGELOG.md |  6 +++
 packages/decap-cms-widget-object/package.json |  2 +-
 .../decap-cms-widget-richtext/CHANGELOG.md    | 15 ++++++
 .../decap-cms-widget-richtext/package.json    |  2 +-
 packages/decap-cms/CHANGELOG.md               |  4 ++
 packages/decap-cms/package.json               |  4 +-
 packages/decap-server/CHANGELOG.md            |  4 ++
 packages/decap-server/package.json            |  4 +-
 27 files changed, 137 insertions(+), 48 deletions(-)
 create mode 100644 packages/decap-cms-widget-richtext/CHANGELOG.md

diff --git a/package-lock.json b/package-lock.json
index ddbcc837913a..4b5cc458101e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33120,12 +33120,12 @@
       }
     },
     "packages/decap-cms": {
-      "version": "3.11.0",
+      "version": "3.12.0",
       "license": "MIT",
       "dependencies": {
         "codemirror": "^5.46.0",
         "create-react-class": "^15.7.0",
-        "decap-cms-app": "^3.11.0",
+        "decap-cms-app": "^3.12.0",
         "decap-cms-media-library-cloudinary": "^3.1.0",
         "decap-cms-media-library-uploadcare": "^3.0.2",
         "file-loader": "^6.2.0",
@@ -33135,7 +33135,7 @@
       }
     },
     "packages/decap-cms-app": {
-      "version": "3.11.0",
+      "version": "3.12.0",
       "license": "MIT",
       "dependencies": {
         "@emotion/react": "^11.11.1",
@@ -33148,20 +33148,20 @@
         "decap-cms-backend-aws-cognito-github-proxy": "^3.5.0",
         "decap-cms-backend-azure": "^3.4.0",
         "decap-cms-backend-bitbucket": "^3.3.1",
-        "decap-cms-backend-git-gateway": "^3.5.1",
-        "decap-cms-backend-gitea": "^3.3.0",
-        "decap-cms-backend-github": "^3.5.0",
+        "decap-cms-backend-git-gateway": "^3.6.0",
+        "decap-cms-backend-gitea": "^3.4.0",
+        "decap-cms-backend-github": "^3.6.0",
         "decap-cms-backend-gitlab": "^3.4.0",
         "decap-cms-backend-proxy": "^3.3.0",
         "decap-cms-backend-test": "^3.2.1",
-        "decap-cms-core": "^3.11.0",
-        "decap-cms-editor-component-image": "^3.2.1",
+        "decap-cms-core": "^3.12.0",
+        "decap-cms-editor-component-image": "^3.3.0",
         "decap-cms-lib-auth": "^3.0.6",
-        "decap-cms-lib-util": "^3.4.0",
+        "decap-cms-lib-util": "^3.5.0",
         "decap-cms-lib-widgets": "^3.3.0",
         "decap-cms-locales": "^3.5.1",
         "decap-cms-ui-auth": "^3.3.0",
-        "decap-cms-ui-default": "^3.5.1",
+        "decap-cms-ui-default": "^3.6.0",
         "decap-cms-widget-boolean": "^3.2.0",
         "decap-cms-widget-code": "^3.4.0",
         "decap-cms-widget-colorstring": "^3.2.0",
@@ -33170,9 +33170,9 @@
         "decap-cms-widget-image": "^3.2.1",
         "decap-cms-widget-list": "^3.5.0",
         "decap-cms-widget-map": "^3.2.0",
-        "decap-cms-widget-markdown": "^3.7.0",
+        "decap-cms-widget-markdown": "^3.8.0",
         "decap-cms-widget-number": "^3.2.0",
-        "decap-cms-widget-object": "^3.4.0",
+        "decap-cms-widget-object": "^3.5.0",
         "decap-cms-widget-relation": "^3.5.2",
         "decap-cms-widget-select": "^3.3.0",
         "decap-cms-widget-string": "^3.2.0",
@@ -33286,7 +33286,7 @@
       }
     },
     "packages/decap-cms-backend-git-gateway": {
-      "version": "3.5.1",
+      "version": "3.6.0",
       "license": "MIT",
       "dependencies": {
         "gotrue-js": "^0.9.24",
@@ -33330,7 +33330,7 @@
       }
     },
     "packages/decap-cms-backend-gitea": {
-      "version": "3.3.0",
+      "version": "3.4.0",
       "license": "MIT",
       "dependencies": {
         "js-base64": "^3.0.0",
@@ -33349,7 +33349,7 @@
       }
     },
     "packages/decap-cms-backend-github": {
-      "version": "3.5.0",
+      "version": "3.6.0",
       "license": "MIT",
       "dependencies": {
         "apollo-cache-inmemory": "^1.6.2",
@@ -33428,7 +33428,7 @@
       }
     },
     "packages/decap-cms-core": {
-      "version": "3.11.0",
+      "version": "3.12.0",
       "license": "MIT",
       "dependencies": {
         "@iarna/toml": "2.2.5",
@@ -33535,7 +33535,7 @@
       }
     },
     "packages/decap-cms-editor-component-image": {
-      "version": "3.2.1",
+      "version": "3.3.0",
       "license": "MIT",
       "devDependencies": {
         "cross-env": "^7.0.0"
@@ -33554,7 +33554,7 @@
       }
     },
     "packages/decap-cms-lib-util": {
-      "version": "3.4.0",
+      "version": "3.5.0",
       "license": "MIT",
       "dependencies": {
         "js-sha256": "^0.9.0",
@@ -33614,7 +33614,7 @@
       }
     },
     "packages/decap-cms-ui-default": {
-      "version": "3.5.1",
+      "version": "3.6.0",
       "license": "MIT",
       "dependencies": {
         "react-aria-menubutton": "^7.0.0",
@@ -33774,7 +33774,7 @@
       }
     },
     "packages/decap-cms-widget-markdown": {
-      "version": "3.7.0",
+      "version": "3.8.0",
       "license": "MIT",
       "dependencies": {
         "detab": "^2.0.4",
@@ -33888,7 +33888,7 @@
       }
     },
     "packages/decap-cms-widget-object": {
-      "version": "3.4.0",
+      "version": "3.5.0",
       "license": "MIT",
       "peerDependencies": {
         "@emotion/react": "^11.11.1",
@@ -33924,7 +33924,7 @@
       }
     },
     "packages/decap-cms-widget-richtext": {
-      "version": "3.1.0",
+      "version": "3.2.0",
       "license": "MIT",
       "dependencies": {
         "@platejs/basic-nodes": "^49.0.0",
@@ -34060,7 +34060,7 @@
       }
     },
     "packages/decap-server": {
-      "version": "3.6.0",
+      "version": "3.7.0",
       "license": "MIT",
       "dependencies": {
         "@hapi/joi": "^17.0.2",
@@ -34084,7 +34084,7 @@
         "@types/morgan": "^1.7.37",
         "@types/node": "^16.0.0",
         "@types/vfile-message": "^2.0.0",
-        "decap-cms-lib-util": "^3.4.0",
+        "decap-cms-lib-util": "^3.5.0",
         "jest": "^27.0.0",
         "nodemon": "^3.1.14",
         "ts-jest": "^27.0.0",
diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md
index 231fe8016e19..cbcdda878844 100644
--- a/packages/decap-cms-app/CHANGELOG.md
+++ b/packages/decap-cms-app/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.11.0...decap-cms-app@3.12.0) (2026-04-16)
+
+**Note:** Version bump only for package decap-cms-app
+
 # [3.11.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.10.1...decap-cms-app@3.11.0) (2026-03-24)
 
 **Note:** Version bump only for package decap-cms-app
diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json
index 539b7a24502d..d75b235ddd7d 100644
--- a/packages/decap-cms-app/package.json
+++ b/packages/decap-cms-app/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-app",
   "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.",
-  "version": "3.11.0",
+  "version": "3.12.0",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
@@ -37,20 +37,20 @@
     "decap-cms-backend-aws-cognito-github-proxy": "^3.5.0",
     "decap-cms-backend-azure": "^3.4.0",
     "decap-cms-backend-bitbucket": "^3.3.1",
-    "decap-cms-backend-git-gateway": "^3.5.1",
-    "decap-cms-backend-gitea": "^3.3.0",
-    "decap-cms-backend-github": "^3.5.0",
+    "decap-cms-backend-git-gateway": "^3.6.0",
+    "decap-cms-backend-gitea": "^3.4.0",
+    "decap-cms-backend-github": "^3.6.0",
     "decap-cms-backend-gitlab": "^3.4.0",
     "decap-cms-backend-proxy": "^3.3.0",
     "decap-cms-backend-test": "^3.2.1",
-    "decap-cms-core": "^3.11.0",
-    "decap-cms-editor-component-image": "^3.2.1",
+    "decap-cms-core": "^3.12.0",
+    "decap-cms-editor-component-image": "^3.3.0",
     "decap-cms-lib-auth": "^3.0.6",
-    "decap-cms-lib-util": "^3.4.0",
+    "decap-cms-lib-util": "^3.5.0",
     "decap-cms-lib-widgets": "^3.3.0",
     "decap-cms-locales": "^3.5.1",
     "decap-cms-ui-auth": "^3.3.0",
-    "decap-cms-ui-default": "^3.5.1",
+    "decap-cms-ui-default": "^3.6.0",
     "decap-cms-widget-boolean": "^3.2.0",
     "decap-cms-widget-code": "^3.4.0",
     "decap-cms-widget-colorstring": "^3.2.0",
@@ -59,9 +59,9 @@
     "decap-cms-widget-image": "^3.2.1",
     "decap-cms-widget-list": "^3.5.0",
     "decap-cms-widget-map": "^3.2.0",
-    "decap-cms-widget-markdown": "^3.7.0",
+    "decap-cms-widget-markdown": "^3.8.0",
     "decap-cms-widget-number": "^3.2.0",
-    "decap-cms-widget-object": "^3.4.0",
+    "decap-cms-widget-object": "^3.5.0",
     "decap-cms-widget-relation": "^3.5.2",
     "decap-cms-widget-select": "^3.3.0",
     "decap-cms-widget-string": "^3.2.0",
diff --git a/packages/decap-cms-backend-git-gateway/CHANGELOG.md b/packages/decap-cms-backend-git-gateway/CHANGELOG.md
index 3382a835a18c..eda351e4becc 100644
--- a/packages/decap-cms-backend-git-gateway/CHANGELOG.md
+++ b/packages/decap-cms-backend-git-gateway/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.5.1...decap-cms-backend-git-gateway@3.6.0) (2026-04-16)
+
+### Features
+
+- add config option to enable Signed-off-by in commit messages ([#7766](https://github.com/decaporg/decap-cms/issues/7766)) ([4b9ea40](https://github.com/decaporg/decap-cms/commit/4b9ea4040f3b858793052cee287caa3cce2f3af3))
+
 ## [3.5.1](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-git-gateway@3.5.0...decap-cms-backend-git-gateway@3.5.1) (2026-02-23)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-backend-git-gateway/package.json b/packages/decap-cms-backend-git-gateway/package.json
index 612abf4b6c0d..158cadced740 100644
--- a/packages/decap-cms-backend-git-gateway/package.json
+++ b/packages/decap-cms-backend-git-gateway/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-backend-git-gateway",
   "description": "Git Gateway backend for Decap CMS",
-  "version": "3.5.1",
+  "version": "3.6.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-git-gateway",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "module": "dist/esm/index.js",
diff --git a/packages/decap-cms-backend-gitea/CHANGELOG.md b/packages/decap-cms-backend-gitea/CHANGELOG.md
index d439ed59d852..0a25409bdb25 100644
--- a/packages/decap-cms-backend-gitea/CHANGELOG.md
+++ b/packages/decap-cms-backend-gitea/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.3.0...decap-cms-backend-gitea@3.4.0) (2026-04-16)
+
+### Features
+
+- add config option to enable Signed-off-by in commit messages ([#7766](https://github.com/decaporg/decap-cms/issues/7766)) ([4b9ea40](https://github.com/decaporg/decap-cms/commit/4b9ea4040f3b858793052cee287caa3cce2f3af3))
+
 # [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-gitea@3.1.5...decap-cms-backend-gitea@3.3.0) (2025-07-15)
 
 ### Features
diff --git a/packages/decap-cms-backend-gitea/package.json b/packages/decap-cms-backend-gitea/package.json
index c65d4fb85da7..b2478c42f1a2 100644
--- a/packages/decap-cms-backend-gitea/package.json
+++ b/packages/decap-cms-backend-gitea/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-backend-gitea",
   "description": "Gitea backend for Decap CMS",
-  "version": "3.3.0",
+  "version": "3.4.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitea",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "license": "MIT",
diff --git a/packages/decap-cms-backend-github/CHANGELOG.md b/packages/decap-cms-backend-github/CHANGELOG.md
index 24cb1d6b5579..f794b60b09ef 100644
--- a/packages/decap-cms-backend-github/CHANGELOG.md
+++ b/packages/decap-cms-backend-github/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.5.0...decap-cms-backend-github@3.6.0) (2026-04-16)
+
+### Features
+
+- add config option to enable Signed-off-by in commit messages ([#7766](https://github.com/decaporg/decap-cms/issues/7766)) ([4b9ea40](https://github.com/decaporg/decap-cms/commit/4b9ea4040f3b858793052cee287caa3cce2f3af3))
+
 # [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-backend-github@3.4.0...decap-cms-backend-github@3.5.0) (2025-12-18)
 
 **Note:** Version bump only for package decap-cms-backend-github
diff --git a/packages/decap-cms-backend-github/package.json b/packages/decap-cms-backend-github/package.json
index 09e744c0f068..bd7bc242f3dd 100644
--- a/packages/decap-cms-backend-github/package.json
+++ b/packages/decap-cms-backend-github/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-backend-github",
   "description": "GitHub backend for Decap CMS",
-  "version": "3.5.0",
+  "version": "3.6.0",
   "license": "MIT",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms-core/CHANGELOG.md b/packages/decap-cms-core/CHANGELOG.md
index 2009abb667cd..baf93921ddd6 100644
--- a/packages/decap-cms-core/CHANGELOG.md
+++ b/packages/decap-cms-core/CHANGELOG.md
@@ -3,6 +3,20 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.11.0...decap-cms-core@3.12.0) (2026-04-16)
+
+### Bug Fixes
+
+- **frontmatter:** improve duplicate frontmatter key error handling ([#7679](https://github.com/decaporg/decap-cms/issues/7679)) ([cbe162d](https://github.com/decaporg/decap-cms/commit/cbe162d740bcbfafc4e76a77ac49e2029bc3a4c4))
+
+### Features
+
+- add config option to enable Signed-off-by in commit messages ([#7766](https://github.com/decaporg/decap-cms/issues/7766)) ([4b9ea40](https://github.com/decaporg/decap-cms/commit/4b9ea4040f3b858793052cee287caa3cce2f3af3))
+
+### Performance Improvements
+
+- use Clipboard API ([#7780](https://github.com/decaporg/decap-cms/issues/7780)) ([c90804f](https://github.com/decaporg/decap-cms/commit/c90804fa623020dd7246cc8154c6992a1bc2a79e))
+
 # [3.11.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.10.1...decap-cms-core@3.11.0) (2026-03-24)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json
index 95f7e88ac714..d09c3f8acd06 100644
--- a/packages/decap-cms-core/package.json
+++ b/packages/decap-cms-core/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-core",
   "description": "Decap CMS core application, see decap-cms package for the main distribution.",
-  "version": "3.11.0",
+  "version": "3.12.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "module": "dist/esm/index.js",
diff --git a/packages/decap-cms-editor-component-image/CHANGELOG.md b/packages/decap-cms-editor-component-image/CHANGELOG.md
index b4944f9d2a42..9bbc43d15c08 100644
--- a/packages/decap-cms-editor-component-image/CHANGELOG.md
+++ b/packages/decap-cms-editor-component-image/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-editor-component-image@3.2.1...decap-cms-editor-component-image@3.3.0) (2026-04-16)
+
+**Note:** Version bump only for package decap-cms-editor-component-image
+
 ## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-editor-component-image@3.2.0...decap-cms-editor-component-image@3.2.1) (2025-09-30)
 
 **Note:** Version bump only for package decap-cms-editor-component-image
diff --git a/packages/decap-cms-editor-component-image/package.json b/packages/decap-cms-editor-component-image/package.json
index a98dcd346cbb..5282acf73908 100644
--- a/packages/decap-cms-editor-component-image/package.json
+++ b/packages/decap-cms-editor-component-image/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-editor-component-image",
   "description": "Image component for Decap CMS editor widget",
-  "version": "3.2.1",
+  "version": "3.3.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-editor-component-image",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "module": "dist/esm/index.js",
diff --git a/packages/decap-cms-lib-util/CHANGELOG.md b/packages/decap-cms-lib-util/CHANGELOG.md
index bc9c34747488..ae17efd5ed8b 100644
--- a/packages/decap-cms-lib-util/CHANGELOG.md
+++ b/packages/decap-cms-lib-util/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-lib-util@3.4.0...decap-cms-lib-util@3.5.0) (2026-04-16)
+
+### Features
+
+- add config option to enable Signed-off-by in commit messages ([#7766](https://github.com/decaporg/decap-cms/issues/7766)) ([4b9ea40](https://github.com/decaporg/decap-cms/commit/4b9ea4040f3b858793052cee287caa3cce2f3af3))
+
 # [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-lib-util@3.3.1...decap-cms-lib-util@3.4.0) (2025-12-18)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-lib-util/package.json b/packages/decap-cms-lib-util/package.json
index 380ba11c9f73..671027e7e6ad 100644
--- a/packages/decap-cms-lib-util/package.json
+++ b/packages/decap-cms-lib-util/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-lib-util",
   "description": "Shared utilities for Decap CMS.",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-lib-util",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "module": "dist/esm/index.js",
diff --git a/packages/decap-cms-ui-default/CHANGELOG.md b/packages/decap-cms-ui-default/CHANGELOG.md
index e84cecadc709..68afb6604b9f 100644
--- a/packages/decap-cms-ui-default/CHANGELOG.md
+++ b/packages/decap-cms-ui-default/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-cms-ui-default@3.5.1...decap-cms-ui-default@3.6.0) (2026-04-16)
+
+**Note:** Version bump only for package decap-cms-ui-default
+
 ## [3.5.1](https://github.com/decaporg/decap-cms/compare/decap-cms-ui-default@3.5.0...decap-cms-ui-default@3.5.1) (2026-02-23)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-ui-default/package.json b/packages/decap-cms-ui-default/package.json
index 0a05366aea89..e8b5b261158d 100644
--- a/packages/decap-cms-ui-default/package.json
+++ b/packages/decap-cms-ui-default/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-ui-default",
   "description": "Default UI components for Decap CMS.",
-  "version": "3.5.1",
+  "version": "3.6.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-ui-default",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "license": "MIT",
diff --git a/packages/decap-cms-widget-markdown/CHANGELOG.md b/packages/decap-cms-widget-markdown/CHANGELOG.md
index 39790a25a77f..3a95ef324723 100644
--- a/packages/decap-cms-widget-markdown/CHANGELOG.md
+++ b/packages/decap-cms-widget-markdown/CHANGELOG.md
@@ -3,6 +3,16 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.8.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.7.0...decap-cms-widget-markdown@3.8.0) (2026-04-16)
+
+### Bug Fixes
+
+- **markdown:** extra whitespace before and after pasted content [#7364](https://github.com/decaporg/decap-cms/issues/7364) ([#7440](https://github.com/decaporg/decap-cms/issues/7440)) ([fc16ff3](https://github.com/decaporg/decap-cms/commit/fc16ff3d303ddf0840c3dacea7f34449bb69f2f3))
+
+### Features
+
+- remove HTML comments from pasted content ([#7775](https://github.com/decaporg/decap-cms/issues/7775)) ([b7cf396](https://github.com/decaporg/decap-cms/commit/b7cf3968b157f13da9ca6c9448ab8e8bc51a4afa))
+
 # [3.7.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.6.1...decap-cms-widget-markdown@3.7.0) (2026-03-24)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-widget-markdown/package.json b/packages/decap-cms-widget-markdown/package.json
index feb65546c0c5..3cfe12353133 100644
--- a/packages/decap-cms-widget-markdown/package.json
+++ b/packages/decap-cms-widget-markdown/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-widget-markdown",
   "description": "Widget for editing markdown in Decap CMS.",
-  "version": "3.7.0",
+  "version": "3.8.0",
   "homepage": "https://www.decapcms.org/docs/widgets/#markdown",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms-widget-object/CHANGELOG.md b/packages/decap-cms-widget-object/CHANGELOG.md
index dcb25d7c7c32..5b62f2addcc4 100644
--- a/packages/decap-cms-widget-object/CHANGELOG.md
+++ b/packages/decap-cms-widget-object/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.4.0...decap-cms-widget-object@3.5.0) (2026-04-16)
+
+### Bug Fixes
+
+- **widget-object:** don't propagate isEditorComponent to nested fields ([#7762](https://github.com/decaporg/decap-cms/issues/7762)) ([bdd5d1c](https://github.com/decaporg/decap-cms/commit/bdd5d1cd01bfa1afc320065aca20035006b19cd7))
+
 # [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-object@3.3.1...decap-cms-widget-object@3.4.0) (2025-06-26)
 
 **Note:** Version bump only for package decap-cms-widget-object
diff --git a/packages/decap-cms-widget-object/package.json b/packages/decap-cms-widget-object/package.json
index e8cf1601be02..6478dc156d78 100644
--- a/packages/decap-cms-widget-object/package.json
+++ b/packages/decap-cms-widget-object/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-widget-object",
   "description": "Widget for displaying an object of fields for Decap CMS.",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "homepage": "https://www.decapcms.org/docs/widgets/#object",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-object",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms-widget-richtext/CHANGELOG.md b/packages/decap-cms-widget-richtext/CHANGELOG.md
new file mode 100644
index 000000000000..9cbbd73c37ac
--- /dev/null
+++ b/packages/decap-cms-widget-richtext/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# 3.2.0 (2026-04-16)
+
+### Bug Fixes
+
+- **widget-object:** don't propagate isEditorComponent to nested fields ([#7762](https://github.com/decaporg/decap-cms/issues/7762)) ([bdd5d1c](https://github.com/decaporg/decap-cms/commit/bdd5d1cd01bfa1afc320065aca20035006b19cd7))
+
+### Features
+
+- add break element to widget richtext ([#7782](https://github.com/decaporg/decap-cms/issues/7782)) ([7d99b3b](https://github.com/decaporg/decap-cms/commit/7d99b3bf3fb18c38e220415ea137a47d0fee8847))
+- remove HTML comments from pasted content ([#7775](https://github.com/decaporg/decap-cms/issues/7775)) ([b7cf396](https://github.com/decaporg/decap-cms/commit/b7cf3968b157f13da9ca6c9448ab8e8bc51a4afa))
diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json
index 4f3e11c0cfe6..51f245bac3d0 100644
--- a/packages/decap-cms-widget-richtext/package.json
+++ b/packages/decap-cms-widget-richtext/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-widget-richtext",
   "description": "Widget for editing richtext in Decap CMS.",
-  "version": "3.1.0",
+  "version": "3.2.0",
   "homepage": "https://www.decapcms.org/docs/widgets/#richtext",
   "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-widget-richtext",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md
index c3c838df6310..be33a8c160d8 100644
--- a/packages/decap-cms/CHANGELOG.md
+++ b/packages/decap-cms/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.11.0...decap-cms@3.12.0) (2026-04-16)
+
+**Note:** Version bump only for package decap-cms
+
 # [3.11.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.10.1...decap-cms@3.11.0) (2026-03-24)
 
 ### Features
diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json
index 86063489f186..e6ccfa19acd4 100644
--- a/packages/decap-cms/package.json
+++ b/packages/decap-cms/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms",
   "description": "An extensible, open source, Git-based, React CMS for static sites.",
-  "version": "3.11.0",
+  "version": "3.12.0",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
@@ -25,7 +25,7 @@
   "dependencies": {
     "codemirror": "^5.46.0",
     "create-react-class": "^15.7.0",
-    "decap-cms-app": "^3.11.0",
+    "decap-cms-app": "^3.12.0",
     "decap-cms-media-library-cloudinary": "^3.1.0",
     "decap-cms-media-library-uploadcare": "^3.0.2",
     "file-loader": "^6.2.0",
diff --git a/packages/decap-server/CHANGELOG.md b/packages/decap-server/CHANGELOG.md
index a65ed1c6dcdb..c97d14f85218 100644
--- a/packages/decap-server/CHANGELOG.md
+++ b/packages/decap-server/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.7.0](https://github.com/decaporg/decap-cms/compare/decap-server@3.6.0...decap-server@3.7.0) (2026-04-16)
+
+**Note:** Version bump only for package decap-server
+
 # [3.6.0](https://github.com/decaporg/decap-cms/compare/decap-server@3.5.2...decap-server@3.6.0) (2026-03-24)
 
 **Note:** Version bump only for package decap-server
diff --git a/packages/decap-server/package.json b/packages/decap-server/package.json
index b2f4f3f7c17b..491f7ec1c46d 100644
--- a/packages/decap-server/package.json
+++ b/packages/decap-server/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-server",
   "description": "Proxy server to be used with Decap CMS proxy backend",
-  "version": "3.6.0",
+  "version": "3.7.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-server",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "license": "MIT",
@@ -39,7 +39,7 @@
     "@types/morgan": "^1.7.37",
     "@types/node": "^16.0.0",
     "@types/vfile-message": "^2.0.0",
-    "decap-cms-lib-util": "^3.4.0",
+    "decap-cms-lib-util": "^3.5.0",
     "jest": "^27.0.0",
     "nodemon": "^3.1.14",
     "ts-jest": "^27.0.0",

From e88f151a75d1d158c371c3c2703667f72fef5195 Mon Sep 17 00:00:00 2001
From: Martin Jagodic 
Date: Thu, 16 Apr 2026 15:40:53 +0200
Subject: [PATCH 27/40] fix: add decap-cms-widget-richtext dependency

#7787
---
 packages/decap-cms-app/package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json
index d75b235ddd7d..ee66da3a4509 100644
--- a/packages/decap-cms-app/package.json
+++ b/packages/decap-cms-app/package.json
@@ -63,6 +63,7 @@
     "decap-cms-widget-number": "^3.2.0",
     "decap-cms-widget-object": "^3.5.0",
     "decap-cms-widget-relation": "^3.5.2",
+    "decap-cms-widget-richtext": "^3.2.0",
     "decap-cms-widget-select": "^3.3.0",
     "decap-cms-widget-string": "^3.2.0",
     "decap-cms-widget-text": "^3.2.0",

From 6e09a5a33cd541dc74cb5c69314a09bad6f4564c Mon Sep 17 00:00:00 2001
From: Martin Jagodic 
Date: Thu, 16 Apr 2026 15:42:19 +0200
Subject: [PATCH 28/40] fix: add decap-cms-widget-richtext dependency to
 package-lock

#7787
---
 package-lock.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/package-lock.json b/package-lock.json
index 4b5cc458101e..ccbf9491efa7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33174,6 +33174,7 @@
         "decap-cms-widget-number": "^3.2.0",
         "decap-cms-widget-object": "^3.5.0",
         "decap-cms-widget-relation": "^3.5.2",
+        "decap-cms-widget-richtext": "^3.2.0",
         "decap-cms-widget-select": "^3.3.0",
         "decap-cms-widget-string": "^3.2.0",
         "decap-cms-widget-text": "^3.2.0",

From fe6636502b351b5beaee1bac78f9833b2f5c0048 Mon Sep 17 00:00:00 2001
From: Martin Jagodic 
Date: Thu, 16 Apr 2026 15:43:06 +0200
Subject: [PATCH 29/40] chore(release): publish

 - decap-cms@3.12.1
 - decap-cms-app@3.12.1
---
 package-lock.json                   | 6 +++---
 packages/decap-cms-app/CHANGELOG.md | 6 ++++++
 packages/decap-cms-app/package.json | 2 +-
 packages/decap-cms/CHANGELOG.md     | 4 ++++
 packages/decap-cms/package.json     | 4 ++--
 5 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index ccbf9491efa7..52b4927561e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33120,12 +33120,12 @@
       }
     },
     "packages/decap-cms": {
-      "version": "3.12.0",
+      "version": "3.12.1",
       "license": "MIT",
       "dependencies": {
         "codemirror": "^5.46.0",
         "create-react-class": "^15.7.0",
-        "decap-cms-app": "^3.12.0",
+        "decap-cms-app": "^3.12.1",
         "decap-cms-media-library-cloudinary": "^3.1.0",
         "decap-cms-media-library-uploadcare": "^3.0.2",
         "file-loader": "^6.2.0",
@@ -33135,7 +33135,7 @@
       }
     },
     "packages/decap-cms-app": {
-      "version": "3.12.0",
+      "version": "3.12.1",
       "license": "MIT",
       "dependencies": {
         "@emotion/react": "^11.11.1",
diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md
index cbcdda878844..2c3a60a30c18 100644
--- a/packages/decap-cms-app/CHANGELOG.md
+++ b/packages/decap-cms-app/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+## [3.12.1](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.12.0...decap-cms-app@3.12.1) (2026-04-16)
+
+### Bug Fixes
+
+- add decap-cms-widget-richtext dependency ([be8cf6b](https://github.com/decaporg/decap-cms/commit/be8cf6bf18f832d1e8664b2604467872a5e8d83e)), closes [#7787](https://github.com/decaporg/decap-cms/issues/7787)
+
 # [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.11.0...decap-cms-app@3.12.0) (2026-04-16)
 
 **Note:** Version bump only for package decap-cms-app
diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json
index ee66da3a4509..ba85540f10c9 100644
--- a/packages/decap-cms-app/package.json
+++ b/packages/decap-cms-app/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-app",
   "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.",
-  "version": "3.12.0",
+  "version": "3.12.1",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md
index be33a8c160d8..b37fc4ba64a7 100644
--- a/packages/decap-cms/CHANGELOG.md
+++ b/packages/decap-cms/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+## [3.12.1](https://github.com/decaporg/decap-cms/compare/decap-cms@3.12.0...decap-cms@3.12.1) (2026-04-16)
+
+**Note:** Version bump only for package decap-cms
+
 # [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms@3.11.0...decap-cms@3.12.0) (2026-04-16)
 
 **Note:** Version bump only for package decap-cms
diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json
index e6ccfa19acd4..c2650660c278 100644
--- a/packages/decap-cms/package.json
+++ b/packages/decap-cms/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms",
   "description": "An extensible, open source, Git-based, React CMS for static sites.",
-  "version": "3.12.0",
+  "version": "3.12.1",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
@@ -25,7 +25,7 @@
   "dependencies": {
     "codemirror": "^5.46.0",
     "create-react-class": "^15.7.0",
-    "decap-cms-app": "^3.12.0",
+    "decap-cms-app": "^3.12.1",
     "decap-cms-media-library-cloudinary": "^3.1.0",
     "decap-cms-media-library-uploadcare": "^3.0.2",
     "file-loader": "^6.2.0",

From b2614be728bf96fe032e2142eea5e5a892ec909e Mon Sep 17 00:00:00 2001
From: Mike Stop Continues <150434+mikestopcontinues@users.noreply.github.com>
Date: Fri, 17 Apr 2026 01:11:55 -0500
Subject: [PATCH 30/40] feat: allow special characters to pass through
 slugification (#6220)

* feat: allow special characters to pass through slugification

* feat: allow special characters to pass through slugification

* chore: combine dev test collections and  rename vars

* chore: remove any

* chore: format

* fix typo in test

* fix: apply Copilot suggestions

* fix: remove unused import

* feat: add new preserve slashes tests

* fix: add missing type and schema updates

* fix: filter empty strings

---------

Co-authored-by: Erez Rokah 
Co-authored-by: Martin Jagodic 
Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 dev-test/config.yml                           |  2 +
 packages/decap-cms-core/index.d.ts            |  1 +
 .../src/constants/configSchema.js             |  2 +
 .../src/lib/__tests__/formatters.spec.js      | 60 +++++++++++++++++++
 .../src/lib/__tests__/urlHelper.spec.js       |  7 +++
 packages/decap-cms-core/src/lib/formatters.ts | 20 +++++--
 packages/decap-cms-core/src/lib/urlHelper.ts  | 42 ++++++++++---
 packages/decap-cms-core/src/types/redux.ts    |  3 +
 8 files changed, 123 insertions(+), 14 deletions(-)

diff --git a/dev-test/config.yml b/dev-test/config.yml
index 6c6b5c544a9d..fbdf95f73a8e 100644
--- a/dev-test/config.yml
+++ b/dev-test/config.yml
@@ -282,6 +282,8 @@ collections: # A list of collections the CMS should be able to edit
     label_singular: 'Page'
     folder: _pages
     create: true
+    preview_path: 'pages/{{slug}}'
+    preview_path_preserve_slashes: true
     nested: { depth: 100, subfolders: false }
     fields:
       - label: Title
diff --git a/packages/decap-cms-core/index.d.ts b/packages/decap-cms-core/index.d.ts
index 6c47c278db2b..03d23dcb4975 100644
--- a/packages/decap-cms-core/index.d.ts
+++ b/packages/decap-cms-core/index.d.ts
@@ -310,6 +310,7 @@ declare module 'decap-cms-core' {
     slug?: string;
     preview_path?: string;
     preview_path_date_field?: string;
+    preview_path_preserve_slashes?: boolean;
     create?: boolean;
     delete?: boolean;
     hide?: boolean;
diff --git a/packages/decap-cms-core/src/constants/configSchema.js b/packages/decap-cms-core/src/constants/configSchema.js
index a5fa3a13c5c9..1b3f28833b6b 100644
--- a/packages/decap-cms-core/src/constants/configSchema.js
+++ b/packages/decap-cms-core/src/constants/configSchema.js
@@ -224,6 +224,7 @@ function getConfigSchema() {
                   file: { type: 'string' },
                   preview_path: { type: 'string' },
                   preview_path_date_field: { type: 'string' },
+                  preview_path_preserve_slashes: { type: 'boolean' },
                   fields: fieldsConfig(),
                 },
                 required: ['name', 'label', 'file', 'fields'],
@@ -236,6 +237,7 @@ function getConfigSchema() {
             path: { type: 'string' },
             preview_path: { type: 'string' },
             preview_path_date_field: { type: 'string' },
+            preview_path_preserve_slashes: { type: 'boolean' },
             create: { type: 'boolean' },
             publish: { type: 'boolean' },
             hide: { type: 'boolean' },
diff --git a/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js b/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
index 00bf5896b444..41438a3ec98a 100644
--- a/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
+++ b/packages/decap-cms-core/src/lib/__tests__/formatters.spec.js
@@ -587,6 +587,66 @@ describe('formatters', () => {
         'Collection "posts" configuration error:\n  `preview_path_date_field` must be a field with a valid date. Ignoring `preview_path`.',
       );
     });
+
+    it('should preserve slashes in value when configured', () => {
+      expect(
+        previewUrlFormatter(
+          'https://www.example.com',
+          Map({
+            preview_path: 'prefix/{{value}}',
+            preview_path_preserve_slashes: true,
+          }),
+          'backendSlug',
+          Map({ data: Map({ value: 'nested/value' }) }),
+          slugConfig,
+        ),
+      ).toBe('https://www.example.com/prefix/nested/value');
+    });
+
+    it('should sanitize slashes in value when not configured', () => {
+      expect(
+        previewUrlFormatter(
+          'https://www.example.com',
+          Map({
+            preview_path: 'prefix/{{value}}',
+          }),
+          'backendSlug',
+          Map({ data: Map({ value: 'nested/value' }) }),
+          slugConfig,
+        ),
+      ).toBe('https://www.example.com/prefix/nested-value');
+    });
+
+    it('should preserve slashes in value for nested collections by default', () => {
+      expect(
+        previewUrlFormatter(
+          'https://www.example.com',
+          Map({
+            preview_path: 'prefix/{{value}}',
+            nested: { depth: 10 },
+          }),
+          'backendSlug',
+          Map({ data: Map({ value: 'nested/value' }) }),
+          slugConfig,
+        ),
+      ).toBe('https://www.example.com/prefix/nested/value');
+    });
+
+    it('should sanitize slashes in value for nested collections when explicitly disabled', () => {
+      expect(
+        previewUrlFormatter(
+          'https://www.example.com',
+          Map({
+            preview_path: 'prefix/{{value}}',
+            nested: { depth: 10 },
+            preview_path_preserve_slashes: false,
+          }),
+          'backendSlug',
+          Map({ data: Map({ value: 'nested/value' }) }),
+          slugConfig,
+        ),
+      ).toBe('https://www.example.com/prefix/nested-value');
+    });
   });
 
   describe('summaryFormatter', () => {
diff --git a/packages/decap-cms-core/src/lib/__tests__/urlHelper.spec.js b/packages/decap-cms-core/src/lib/__tests__/urlHelper.spec.js
index abcdc9c9fde0..4750e45710fd 100644
--- a/packages/decap-cms-core/src/lib/__tests__/urlHelper.spec.js
+++ b/packages/decap-cms-core/src/lib/__tests__/urlHelper.spec.js
@@ -125,6 +125,13 @@ describe('sanitizeSlug', () => {
       'test_test',
     );
   });
+
+  it('preserves slashes when requested', () => {
+    const input = '/this-is-a/nested/page';
+
+    expect(sanitizeSlug(input, slugConfig, false)).toEqual('this-is-a-nested-page');
+    expect(sanitizeSlug(input, slugConfig, true)).toEqual('this-is-a/nested/page');
+  });
 });
 
 describe('sanitizeChar', () => {
diff --git a/packages/decap-cms-core/src/lib/formatters.ts b/packages/decap-cms-core/src/lib/formatters.ts
index 239fe3f2f62a..80cd8c9ab338 100644
--- a/packages/decap-cms-core/src/lib/formatters.ts
+++ b/packages/decap-cms-core/src/lib/formatters.ts
@@ -120,11 +120,19 @@ export function prepareSlug(slug: string) {
   );
 }
 
-export function getProcessSegment(slugConfig?: CmsSlug, ignoreValues?: string[]) {
+export function getProcessSegment(
+  slugConfig?: CmsSlug,
+  ignoreValues?: string[],
+  preserveSlashes?: boolean,
+) {
   return (value: string) =>
     ignoreValues && ignoreValues.includes(value)
       ? value
-      : flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
+      : flow([
+          value => String(value),
+          prepareSlug,
+          partialRight(sanitizeSlug, slugConfig, preserveSlashes),
+        ])(value);
 }
 
 export function slugFormatter(
@@ -205,19 +213,21 @@ export function previewUrlFormatter(
   fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder'));
   const dateFieldName = getDateField() || selectInferredField(collection, 'date');
   const date = parseDateFromEntry(entry as unknown as Map, dateFieldName);
+  const previewPathPreserveSlashes = collection.get('preview_path_preserve_slashes');
+  const preserveSlashes = !!(previewPathPreserveSlashes ?? collection.has('nested'));
 
   // Prepare and sanitize slug variables only, leave the rest of the
   // `preview_path` template as is.
-  const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')]);
+  const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')], preserveSlashes);
   let compiledPath;
 
   try {
     compiledPath = compileStringTemplate(pathTemplate, date, slug, fields, processSegment);
-  } catch (err) {
+  } catch (err: unknown) {
     // Print an error and ignore `preview_path` if both:
     //   1. Date is invalid (according to DayJs), and
     //   2. A date expression (eg. `{{year}}`) is used in `preview_path`
-    if (err.name === SLUG_MISSING_REQUIRED_DATE) {
+    if (err instanceof Error && err.name === SLUG_MISSING_REQUIRED_DATE) {
       console.error(stripIndent`
         Collection "${collection.get('name')}" configuration error:
           \`preview_path_date_field\` must be a field with a valid date. Ignoring \`preview_path\`.
diff --git a/packages/decap-cms-core/src/lib/urlHelper.ts b/packages/decap-cms-core/src/lib/urlHelper.ts
index 43da84e07bfe..ae168477b1d5 100644
--- a/packages/decap-cms-core/src/lib/urlHelper.ts
+++ b/packages/decap-cms-core/src/lib/urlHelper.ts
@@ -51,7 +51,14 @@ function validIRIChar(char: string) {
   return uriChars.test(char) || ucsChars.test(char);
 }
 
-export function getCharReplacer(encoding: string, replacement: string) {
+export function getCharReplacer(
+  encoding: string,
+  options: {
+    replacement: NonNullable;
+    preserveSlashes?: boolean;
+  },
+) {
+  const { replacement, preserveSlashes } = options;
   let validChar: (char: string) => boolean;
 
   if (encoding === 'unicode') {
@@ -67,14 +74,24 @@ export function getCharReplacer(encoding: string, replacement: string) {
     throw new Error('The replacement character(s) (options.replacement) is itself unsafe.');
   }
 
-  return (char: string) => (validChar(char) ? char : replacement);
+  return (char: string, i = 0, arr: string[] = [char]) => {
+    if (preserveSlashes && char === '/' && i !== 0 && i !== arr.length - 1) {
+      return char;
+    }
+
+    return validChar(char) ? char : replacement;
+  };
 }
 // `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed.
 export function sanitizeURI(
   str: string,
-  options?: { replacement: CmsSlug['sanitize_replacement']; encoding: CmsSlug['encoding'] },
+  options?: {
+    replacement: CmsSlug['sanitize_replacement'];
+    encoding: CmsSlug['encoding'];
+    preserveSlashes?: boolean;
+  },
 ) {
-  const { replacement = '', encoding = 'unicode' } = options || {};
+  const { replacement = '', encoding = 'unicode', preserveSlashes } = options || {};
 
   if (!isString(str)) {
     throw new Error('The input slug must be a string.');
@@ -85,15 +102,15 @@ export function sanitizeURI(
 
   // `Array.from` must be used instead of `String.split` because
   //   `split` converts things like emojis into UTF-16 surrogate pairs.
-  return Array.from(str).map(getCharReplacer(encoding, replacement)).join('');
+  return Array.from(str).map(getCharReplacer(encoding, { replacement, preserveSlashes })).join('');
 }
 
 export function sanitizeChar(char: string, options?: CmsSlug) {
   const { encoding = 'unicode', sanitize_replacement: replacement = '' } = options || {};
-  return getCharReplacer(encoding, replacement)(char);
+  return getCharReplacer(encoding, { replacement })(char);
 }
 
-export function sanitizeSlug(str: string, options?: CmsSlug) {
+export function sanitizeSlug(str: string, options?: CmsSlug, preserveSlashes?: boolean) {
   if (!isString(str)) {
     throw new Error('The input slug must be a string.');
   }
@@ -106,8 +123,15 @@ export function sanitizeSlug(str: string, options?: CmsSlug) {
 
   const sanitizedSlug = flow([
     ...(stripDiacritics ? [diacritics.remove] : []),
-    partialRight(sanitizeURI, { replacement, encoding }),
-    partialRight(sanitizeFilename, { replacement }),
+    partialRight(sanitizeURI, { replacement, encoding, preserveSlashes }),
+    preserveSlashes
+      ? (slug: string) =>
+          slug
+            .split('/')
+            .filter(Boolean)
+            .map(part => sanitizeFilename(part, { replacement }))
+            .join('/')
+      : partialRight(sanitizeFilename, { replacement }),
   ])(str);
 
   // Remove any doubled or leading/trailing replacement characters (that were added in the sanitizers).
diff --git a/packages/decap-cms-core/src/types/redux.ts b/packages/decap-cms-core/src/types/redux.ts
index 294d118f534b..6ae1522a201b 100644
--- a/packages/decap-cms-core/src/types/redux.ts
+++ b/packages/decap-cms-core/src/types/redux.ts
@@ -290,6 +290,7 @@ export interface CmsCollectionFile {
   description?: string;
   preview_path?: string;
   preview_path_date_field?: string;
+  preview_path_preserve_slashes?: boolean;
   i18n?: boolean | CmsI18nConfig;
   media_folder?: string;
   public_folder?: string;
@@ -327,6 +328,7 @@ export interface CmsCollection {
   slug?: string;
   preview_path?: string;
   preview_path_date_field?: string;
+  preview_path_preserve_slashes?: boolean;
   create?: boolean;
   delete?: boolean;
   editor?: {
@@ -634,6 +636,7 @@ type CollectionObject = {
   public_folder?: string;
   preview_path?: string;
   preview_path_date_field?: string;
+  preview_path_preserve_slashes?: boolean;
   summary?: string;
   filter?: FilterRule;
   type: 'file_based_collection' | 'folder_based_collection';

From 641605f52665124f70a2cb2638467d87b3fb1795 Mon Sep 17 00:00:00 2001
From: Federico Tibaldo 
Date: Fri, 17 Apr 2026 08:32:08 +0200
Subject: [PATCH 31/40] chore: add `mdast-util-to-string` to direct deps of
 `decap-cms-widget-richtext` (#7788)

---
 package-lock.json                               | 11 +++++++++++
 packages/decap-cms-widget-richtext/package.json |  1 +
 2 files changed, 12 insertions(+)

diff --git a/package-lock.json b/package-lock.json
index 52b4927561e4..6fd86b510fcb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33935,6 +33935,7 @@
         "class-variance-authority": "^0.7.0",
         "dompurify": "^3.4.0",
         "lucide-react": "^0.331.0",
+        "mdast-util-to-string": "^2.0.0",
         "platejs": "^49.2.21",
         "rehype-parse": "^6.0.0",
         "rehype-remark": "^8.0.0",
@@ -33978,6 +33979,16 @@
         "xtend": "^4.0.1"
       }
     },
+    "packages/decap-cms-widget-richtext/node_modules/mdast-util-to-string": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz",
+      "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
     "packages/decap-cms-widget-richtext/node_modules/parse-entities": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json
index 51f245bac3d0..ed751f0c60db 100644
--- a/packages/decap-cms-widget-richtext/package.json
+++ b/packages/decap-cms-widget-richtext/package.json
@@ -28,6 +28,7 @@
     "class-variance-authority": "^0.7.0",
     "dompurify": "^3.4.0",
     "lucide-react": "^0.331.0",
+    "mdast-util-to-string": "^2.0.0",
     "platejs": "^49.2.21",
     "rehype-parse": "^6.0.0",
     "rehype-remark": "^8.0.0",

From 8d8ca256f76187e044f6ff8569609fbf998c9582 Mon Sep 17 00:00:00 2001
From: Martin Jagodic 
Date: Fri, 17 Apr 2026 08:45:19 +0200
Subject: [PATCH 32/40] chore(release): publish

 - decap-cms@3.12.2
 - decap-cms-app@3.12.2
 - decap-cms-core@3.13.0
 - decap-cms-widget-richtext@3.2.1
---
 package-lock.json                               | 14 +++++++-------
 packages/decap-cms-app/CHANGELOG.md             |  4 ++++
 packages/decap-cms-app/package.json             |  6 +++---
 packages/decap-cms-core/CHANGELOG.md            |  6 ++++++
 packages/decap-cms-core/package.json            |  2 +-
 packages/decap-cms-widget-richtext/CHANGELOG.md |  4 ++++
 packages/decap-cms-widget-richtext/package.json |  2 +-
 packages/decap-cms/CHANGELOG.md                 |  4 ++++
 packages/decap-cms/package.json                 |  4 ++--
 9 files changed, 32 insertions(+), 14 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 6fd86b510fcb..08b2b6fce199 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33120,12 +33120,12 @@
       }
     },
     "packages/decap-cms": {
-      "version": "3.12.1",
+      "version": "3.12.2",
       "license": "MIT",
       "dependencies": {
         "codemirror": "^5.46.0",
         "create-react-class": "^15.7.0",
-        "decap-cms-app": "^3.12.1",
+        "decap-cms-app": "^3.12.2",
         "decap-cms-media-library-cloudinary": "^3.1.0",
         "decap-cms-media-library-uploadcare": "^3.0.2",
         "file-loader": "^6.2.0",
@@ -33135,7 +33135,7 @@
       }
     },
     "packages/decap-cms-app": {
-      "version": "3.12.1",
+      "version": "3.12.2",
       "license": "MIT",
       "dependencies": {
         "@emotion/react": "^11.11.1",
@@ -33154,7 +33154,7 @@
         "decap-cms-backend-gitlab": "^3.4.0",
         "decap-cms-backend-proxy": "^3.3.0",
         "decap-cms-backend-test": "^3.2.1",
-        "decap-cms-core": "^3.12.0",
+        "decap-cms-core": "^3.13.0",
         "decap-cms-editor-component-image": "^3.3.0",
         "decap-cms-lib-auth": "^3.0.6",
         "decap-cms-lib-util": "^3.5.0",
@@ -33174,7 +33174,7 @@
         "decap-cms-widget-number": "^3.2.0",
         "decap-cms-widget-object": "^3.5.0",
         "decap-cms-widget-relation": "^3.5.2",
-        "decap-cms-widget-richtext": "^3.2.0",
+        "decap-cms-widget-richtext": "^3.2.1",
         "decap-cms-widget-select": "^3.3.0",
         "decap-cms-widget-string": "^3.2.0",
         "decap-cms-widget-text": "^3.2.0",
@@ -33429,7 +33429,7 @@
       }
     },
     "packages/decap-cms-core": {
-      "version": "3.12.0",
+      "version": "3.13.0",
       "license": "MIT",
       "dependencies": {
         "@iarna/toml": "2.2.5",
@@ -33925,7 +33925,7 @@
       }
     },
     "packages/decap-cms-widget-richtext": {
-      "version": "3.2.0",
+      "version": "3.2.1",
       "license": "MIT",
       "dependencies": {
         "@platejs/basic-nodes": "^49.0.0",
diff --git a/packages/decap-cms-app/CHANGELOG.md b/packages/decap-cms-app/CHANGELOG.md
index 2c3a60a30c18..4000d52d45a6 100644
--- a/packages/decap-cms-app/CHANGELOG.md
+++ b/packages/decap-cms-app/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+## [3.12.2](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.12.1...decap-cms-app@3.12.2) (2026-04-17)
+
+**Note:** Version bump only for package decap-cms-app
+
 ## [3.12.1](https://github.com/decaporg/decap-cms/compare/decap-cms-app@3.12.0...decap-cms-app@3.12.1) (2026-04-16)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json
index ba85540f10c9..a883cb7c414f 100644
--- a/packages/decap-cms-app/package.json
+++ b/packages/decap-cms-app/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-app",
   "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.",
-  "version": "3.12.1",
+  "version": "3.12.2",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-app",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
@@ -43,7 +43,7 @@
     "decap-cms-backend-gitlab": "^3.4.0",
     "decap-cms-backend-proxy": "^3.3.0",
     "decap-cms-backend-test": "^3.2.1",
-    "decap-cms-core": "^3.12.0",
+    "decap-cms-core": "^3.13.0",
     "decap-cms-editor-component-image": "^3.3.0",
     "decap-cms-lib-auth": "^3.0.6",
     "decap-cms-lib-util": "^3.5.0",
@@ -63,7 +63,7 @@
     "decap-cms-widget-number": "^3.2.0",
     "decap-cms-widget-object": "^3.5.0",
     "decap-cms-widget-relation": "^3.5.2",
-    "decap-cms-widget-richtext": "^3.2.0",
+    "decap-cms-widget-richtext": "^3.2.1",
     "decap-cms-widget-select": "^3.3.0",
     "decap-cms-widget-string": "^3.2.0",
     "decap-cms-widget-text": "^3.2.0",
diff --git a/packages/decap-cms-core/CHANGELOG.md b/packages/decap-cms-core/CHANGELOG.md
index baf93921ddd6..bf2ed518c8a1 100644
--- a/packages/decap-cms-core/CHANGELOG.md
+++ b/packages/decap-cms-core/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+# [3.13.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.12.0...decap-cms-core@3.13.0) (2026-04-17)
+
+### Features
+
+- allow special characters to pass through slugification ([#6220](https://github.com/decaporg/decap-cms/issues/6220)) ([074354d](https://github.com/decaporg/decap-cms/commit/074354d9a83d63d87818ff1c527c38262a421426))
+
 # [3.12.0](https://github.com/decaporg/decap-cms/compare/decap-cms-core@3.11.0...decap-cms-core@3.12.0) (2026-04-16)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json
index d09c3f8acd06..780e59e37ad0 100644
--- a/packages/decap-cms-core/package.json
+++ b/packages/decap-cms-core/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-core",
   "description": "Decap CMS core application, see decap-cms package for the main distribution.",
-  "version": "3.12.0",
+  "version": "3.13.0",
   "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
   "module": "dist/esm/index.js",
diff --git a/packages/decap-cms-widget-richtext/CHANGELOG.md b/packages/decap-cms-widget-richtext/CHANGELOG.md
index 9cbbd73c37ac..16cb65e39980 100644
--- a/packages/decap-cms-widget-richtext/CHANGELOG.md
+++ b/packages/decap-cms-widget-richtext/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-richtext@3.2.0...decap-cms-widget-richtext@3.2.1) (2026-04-17)
+
+**Note:** Version bump only for package decap-cms-widget-richtext
+
 # 3.2.0 (2026-04-16)
 
 ### Bug Fixes
diff --git a/packages/decap-cms-widget-richtext/package.json b/packages/decap-cms-widget-richtext/package.json
index ed751f0c60db..811d11585b94 100644
--- a/packages/decap-cms-widget-richtext/package.json
+++ b/packages/decap-cms-widget-richtext/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms-widget-richtext",
   "description": "Widget for editing richtext in Decap CMS.",
-  "version": "3.2.0",
+  "version": "3.2.1",
   "homepage": "https://www.decapcms.org/docs/widgets/#richtext",
   "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-widget-richtext",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
diff --git a/packages/decap-cms/CHANGELOG.md b/packages/decap-cms/CHANGELOG.md
index b37fc4ba64a7..436fe0d85659 100644
--- a/packages/decap-cms/CHANGELOG.md
+++ b/packages/decap-cms/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 
+## [3.12.2](https://github.com/decaporg/decap-cms/compare/decap-cms@3.12.1...decap-cms@3.12.2) (2026-04-17)
+
+**Note:** Version bump only for package decap-cms
+
 ## [3.12.1](https://github.com/decaporg/decap-cms/compare/decap-cms@3.12.0...decap-cms@3.12.1) (2026-04-16)
 
 **Note:** Version bump only for package decap-cms
diff --git a/packages/decap-cms/package.json b/packages/decap-cms/package.json
index c2650660c278..53c14b403f36 100644
--- a/packages/decap-cms/package.json
+++ b/packages/decap-cms/package.json
@@ -1,7 +1,7 @@
 {
   "name": "decap-cms",
   "description": "An extensible, open source, Git-based, React CMS for static sites.",
-  "version": "3.12.1",
+  "version": "3.12.2",
   "homepage": "https://www.decapcms.org",
   "repository": "https://github.com/decaporg/decap-cms",
   "bugs": "https://github.com/decaporg/decap-cms/issues",
@@ -25,7 +25,7 @@
   "dependencies": {
     "codemirror": "^5.46.0",
     "create-react-class": "^15.7.0",
-    "decap-cms-app": "^3.12.1",
+    "decap-cms-app": "^3.12.2",
     "decap-cms-media-library-cloudinary": "^3.1.0",
     "decap-cms-media-library-uploadcare": "^3.0.2",
     "file-loader": "^6.2.0",

From 09320d4c24a5a29c4ef71d6b32627ffbd69fe66c Mon Sep 17 00:00:00 2001
From: Felix Gnass 
Date: Mon, 20 Apr 2026 09:14:50 +0200
Subject: [PATCH 33/40] Fix(#7442): dynamic loading of codemirror language
 modes (#7443)

* fix: dynamic codemirror mode imports

* style: format code

* fix: await function and add error handling

---------

Co-authored-by: Martin Jagodic 
Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 .../scripts/process-languages.js              |  44 +++++++-
 .../decap-cms-widget-code/src/CodeControl.js  |  14 ++-
 .../src/languageLoaders.js                    | 103 ++++++++++++++++++
 3 files changed, 157 insertions(+), 4 deletions(-)
 create mode 100644 packages/decap-cms-widget-code/src/languageLoaders.js

diff --git a/packages/decap-cms-widget-code/scripts/process-languages.js b/packages/decap-cms-widget-code/scripts/process-languages.js
index b34865b7cddd..097828cef798 100644
--- a/packages/decap-cms-widget-code/scripts/process-languages.js
+++ b/packages/decap-cms-widget-code/scripts/process-languages.js
@@ -5,6 +5,7 @@ const uniq = require('lodash/uniq');
 
 const rawDataPath = '../data/languages-raw.yml';
 const outputPath = '../data/languages.json';
+const loaderPath = '../src/languageLoaders.js';
 
 async function fetchData() {
   const filePath = path.resolve(__dirname, rawDataPath);
@@ -17,6 +18,44 @@ function outputData(data) {
   return fs.writeJson(filePath, data);
 }
 
+function generateLoaders(transformedData) {
+  // Extract all unique modes
+  const modes = new Set();
+  transformedData.forEach(lang => {
+    if (lang.codemirror_mode) {
+      modes.add(lang.codemirror_mode);
+    }
+  });
+
+  // Generate loader functions for each mode
+  let fileContent = `// Generated file - DO NOT EDIT
+// This file contains dynamic loader functions for CodeMirror modes
+
+const loaders = {
+`;
+
+  // Create loader functions
+  Array.from(modes)
+    .sort()
+    .forEach(mode => {
+      fileContent += `  '${mode}': () => import('codemirror/mode/${mode}/${mode}.js'),
+`;
+    });
+
+  fileContent += `};
+
+// Get a loader for a specific mode
+export function getLanguageLoader(mode) {
+  return loaders[mode] || null;
+}
+
+export default loaders;
+`;
+
+  const filePath = path.resolve(__dirname, loaderPath);
+  return fs.writeFile(filePath, fileContent);
+}
+
 function transform(data) {
   return Object.entries(data).reduce((acc, [label, lang]) => {
     const { extensions = [], aliases = [], codemirror_mode, codemirror_mime_type } = lang;
@@ -39,7 +78,10 @@ function transform(data) {
 async function process() {
   const data = await fetchData();
   const transformedData = transform(data);
-  return outputData(transformedData);
+  await outputData(transformedData);
+  await generateLoaders(transformedData);
+
+  console.log('Generated language data and loaders');
 }
 
 process();
diff --git a/packages/decap-cms-widget-code/src/CodeControl.js b/packages/decap-cms-widget-code/src/CodeControl.js
index 4e827853c7d9..34d062c6f6e1 100644
--- a/packages/decap-cms-widget-code/src/CodeControl.js
+++ b/packages/decap-cms-widget-code/src/CodeControl.js
@@ -18,6 +18,7 @@ import materialTheme from 'codemirror/theme/material.css';
 import SettingsPane from './SettingsPane';
 import SettingsButton from './SettingsButton';
 import languageData from '../data/languages.json';
+import { getLanguageLoader } from './languageLoaders';
 
 // TODO: relocate as a utility function
 function getChangedProps(previous, next, keys) {
@@ -121,7 +122,7 @@ export default class CodeControl extends React.Component {
     }
   }
 
-  updateCodeMirrorProps(prevState) {
+  async updateCodeMirrorProps(prevState) {
     const keys = ['lang', 'theme', 'keyMap'];
     const changedProps = getChangedProps(prevState, this.state, keys);
     if (changedProps) {
@@ -133,7 +134,7 @@ export default class CodeControl extends React.Component {
 
       this.setState({ isLangInitialized: true });
 
-      this.handleChangeCodeMirrorProps(changedProps, shouldIgnoreLangChange);
+      await this.handleChangeCodeMirrorProps(changedProps, shouldIgnoreLangChange);
     }
   }
 
@@ -207,7 +208,14 @@ export default class CodeControl extends React.Component {
     if (changedProps.lang) {
       const { mode } = this.getLanguageByName(changedProps.lang) || {};
       if (mode) {
-        require(`codemirror/mode/${mode}/${mode}.js`);
+        const loader = getLanguageLoader(mode);
+        if (loader) {
+          try {
+            await loader();
+          } catch (e) {
+            console.warn(`Failed to load CodeMirror mode: ${mode}`, e);
+          }
+        }
       }
     }
 
diff --git a/packages/decap-cms-widget-code/src/languageLoaders.js b/packages/decap-cms-widget-code/src/languageLoaders.js
new file mode 100644
index 000000000000..4caa6fe64105
--- /dev/null
+++ b/packages/decap-cms-widget-code/src/languageLoaders.js
@@ -0,0 +1,103 @@
+// Generated file - DO NOT EDIT
+// This file contains dynamic loader functions for CodeMirror modes
+
+const loaders = {
+  apl: () => import('codemirror/mode/apl/apl.js'),
+  asciiarmor: () => import('codemirror/mode/asciiarmor/asciiarmor.js'),
+  'asn.1': () => import('codemirror/mode/asn.1/asn.1.js'),
+  brainfuck: () => import('codemirror/mode/brainfuck/brainfuck.js'),
+  clike: () => import('codemirror/mode/clike/clike.js'),
+  clojure: () => import('codemirror/mode/clojure/clojure.js'),
+  cmake: () => import('codemirror/mode/cmake/cmake.js'),
+  cobol: () => import('codemirror/mode/cobol/cobol.js'),
+  coffeescript: () => import('codemirror/mode/coffeescript/coffeescript.js'),
+  commonlisp: () => import('codemirror/mode/commonlisp/commonlisp.js'),
+  crystal: () => import('codemirror/mode/crystal/crystal.js'),
+  css: () => import('codemirror/mode/css/css.js'),
+  d: () => import('codemirror/mode/d/d.js'),
+  dart: () => import('codemirror/mode/dart/dart.js'),
+  diff: () => import('codemirror/mode/diff/diff.js'),
+  django: () => import('codemirror/mode/django/django.js'),
+  dockerfile: () => import('codemirror/mode/dockerfile/dockerfile.js'),
+  dylan: () => import('codemirror/mode/dylan/dylan.js'),
+  ebnf: () => import('codemirror/mode/ebnf/ebnf.js'),
+  ecl: () => import('codemirror/mode/ecl/ecl.js'),
+  eiffel: () => import('codemirror/mode/eiffel/eiffel.js'),
+  elm: () => import('codemirror/mode/elm/elm.js'),
+  erlang: () => import('codemirror/mode/erlang/erlang.js'),
+  factor: () => import('codemirror/mode/factor/factor.js'),
+  forth: () => import('codemirror/mode/forth/forth.js'),
+  fortran: () => import('codemirror/mode/fortran/fortran.js'),
+  gfm: () => import('codemirror/mode/gfm/gfm.js'),
+  go: () => import('codemirror/mode/go/go.js'),
+  groovy: () => import('codemirror/mode/groovy/groovy.js'),
+  haml: () => import('codemirror/mode/haml/haml.js'),
+  haskell: () => import('codemirror/mode/haskell/haskell.js'),
+  'haskell-literate': () => import('codemirror/mode/haskell-literate/haskell-literate.js'),
+  haxe: () => import('codemirror/mode/haxe/haxe.js'),
+  htmlembedded: () => import('codemirror/mode/htmlembedded/htmlembedded.js'),
+  htmlmixed: () => import('codemirror/mode/htmlmixed/htmlmixed.js'),
+  http: () => import('codemirror/mode/http/http.js'),
+  idl: () => import('codemirror/mode/idl/idl.js'),
+  javascript: () => import('codemirror/mode/javascript/javascript.js'),
+  jsx: () => import('codemirror/mode/jsx/jsx.js'),
+  julia: () => import('codemirror/mode/julia/julia.js'),
+  livescript: () => import('codemirror/mode/livescript/livescript.js'),
+  lua: () => import('codemirror/mode/lua/lua.js'),
+  mathematica: () => import('codemirror/mode/mathematica/mathematica.js'),
+  mirc: () => import('codemirror/mode/mirc/mirc.js'),
+  mllike: () => import('codemirror/mode/mllike/mllike.js'),
+  modelica: () => import('codemirror/mode/modelica/modelica.js'),
+  mumps: () => import('codemirror/mode/mumps/mumps.js'),
+  nginx: () => import('codemirror/mode/nginx/nginx.js'),
+  nsis: () => import('codemirror/mode/nsis/nsis.js'),
+  octave: () => import('codemirror/mode/octave/octave.js'),
+  oz: () => import('codemirror/mode/oz/oz.js'),
+  pascal: () => import('codemirror/mode/pascal/pascal.js'),
+  perl: () => import('codemirror/mode/perl/perl.js'),
+  php: () => import('codemirror/mode/php/php.js'),
+  powershell: () => import('codemirror/mode/powershell/powershell.js'),
+  properties: () => import('codemirror/mode/properties/properties.js'),
+  protobuf: () => import('codemirror/mode/protobuf/protobuf.js'),
+  pug: () => import('codemirror/mode/pug/pug.js'),
+  puppet: () => import('codemirror/mode/puppet/puppet.js'),
+  python: () => import('codemirror/mode/python/python.js'),
+  r: () => import('codemirror/mode/r/r.js'),
+  rpm: () => import('codemirror/mode/rpm/rpm.js'),
+  rst: () => import('codemirror/mode/rst/rst.js'),
+  ruby: () => import('codemirror/mode/ruby/ruby.js'),
+  rust: () => import('codemirror/mode/rust/rust.js'),
+  sas: () => import('codemirror/mode/sas/sas.js'),
+  sass: () => import('codemirror/mode/sass/sass.js'),
+  scheme: () => import('codemirror/mode/scheme/scheme.js'),
+  shell: () => import('codemirror/mode/shell/shell.js'),
+  slim: () => import('codemirror/mode/slim/slim.js'),
+  smalltalk: () => import('codemirror/mode/smalltalk/smalltalk.js'),
+  smarty: () => import('codemirror/mode/smarty/smarty.js'),
+  soy: () => import('codemirror/mode/soy/soy.js'),
+  sparql: () => import('codemirror/mode/sparql/sparql.js'),
+  spreadsheet: () => import('codemirror/mode/spreadsheet/spreadsheet.js'),
+  sql: () => import('codemirror/mode/sql/sql.js'),
+  stex: () => import('codemirror/mode/stex/stex.js'),
+  swift: () => import('codemirror/mode/swift/swift.js'),
+  tcl: () => import('codemirror/mode/tcl/tcl.js'),
+  textile: () => import('codemirror/mode/textile/textile.js'),
+  toml: () => import('codemirror/mode/toml/toml.js'),
+  troff: () => import('codemirror/mode/troff/troff.js'),
+  turtle: () => import('codemirror/mode/turtle/turtle.js'),
+  twig: () => import('codemirror/mode/twig/twig.js'),
+  vb: () => import('codemirror/mode/vb/vb.js'),
+  verilog: () => import('codemirror/mode/verilog/verilog.js'),
+  vhdl: () => import('codemirror/mode/vhdl/vhdl.js'),
+  webidl: () => import('codemirror/mode/webidl/webidl.js'),
+  xml: () => import('codemirror/mode/xml/xml.js'),
+  xquery: () => import('codemirror/mode/xquery/xquery.js'),
+  yaml: () => import('codemirror/mode/yaml/yaml.js'),
+};
+
+// Get a loader for a specific mode
+export function getLanguageLoader(mode) {
+  return loaders[mode] || null;
+}
+
+export default loaders;

From 6f0006606f3421262e221601bd1088dc86d5f9f8 Mon Sep 17 00:00:00 2001
From: Felix Gnass 
Date: Mon, 20 Apr 2026 09:34:08 +0200
Subject: [PATCH 34/40] fix: adhere to remark's tokenizer rules #7315 (#7444)

* fix: adhere to remark's tokenizer rules #7315

* fix: merge and improve tests

---------

Co-authored-by: Martin Jagodic 
Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 .../__tests__/remarkShortcodes.spec.js        | 54 ++++-------
 .../src/serializers/remarkShortcodes.js       | 75 ++++++----------
 .../__tests__/remarkShortcodes.spec.js        | 54 ++++-------
 .../src/serializers/remarkShortcodes.js       | 90 ++++++-------------
 4 files changed, 90 insertions(+), 183 deletions(-)

diff --git a/packages/decap-cms-widget-markdown/src/serializers/__tests__/remarkShortcodes.spec.js b/packages/decap-cms-widget-markdown/src/serializers/__tests__/remarkShortcodes.spec.js
index de17e092abfd..5104abadc152 100644
--- a/packages/decap-cms-widget-markdown/src/serializers/__tests__/remarkShortcodes.spec.js
+++ b/packages/decap-cms-widget-markdown/src/serializers/__tests__/remarkShortcodes.spec.js
@@ -2,7 +2,7 @@ import { Map, OrderedMap } from 'immutable';
 import unified from 'unified';
 import markdownToRemarkPlugin from 'remark-parse';
 
-import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes';
+import { remarkParseShortcodes } from '../remarkShortcodes';
 
 function process(value, plugins) {
   return unified()
@@ -33,30 +33,29 @@ describe('remarkParseShortcodes', () => {
         expect.arrayContaining(['foo\n\nbar']),
       );
     });
-    it('should match shortcodes based on order of occurrence in value', () => {
-      const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ });
-      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
+    it('should match shortcodes by first matching plugin', () => {
+      const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /^foo/ });
+      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /^bar/ });
       process(
-        'foo\n\nbar',
+        'bar\n\nfoo',
         OrderedMap([
-          [barEditorComponent.id, barEditorComponent],
           [fooEditorComponent.id, fooEditorComponent],
-        ]),
-      );
-      expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo']));
-    });
-    it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => {
-      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
-      const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ });
-      process(
-        'foo\n\nbar\n\nbaz',
-        OrderedMap([
-          [bazEditorComponent.id, bazEditorComponent],
           [barEditorComponent.id, barEditorComponent],
         ]),
       );
+      // 'bar' is the first block, but 'foo' plugin is first in registry,
+      // so 'foo' doesn't match 'bar'. 'bar' plugin matches 'bar'.
       expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
     });
+    it('should warn when pattern uses multiline flag', () => {
+      const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+      const editorComponent = EditorComponent({ pattern: /^foo$/m });
+      process('foo', Map({ [editorComponent.id]: editorComponent }));
+      expect(warnSpy).toHaveBeenCalledWith(
+        expect.stringContaining('must not use the multiline flag'),
+      );
+      warnSpy.mockRestore();
+    });
   });
   describe('parse', () => {
     describe('pattern with leading caret', () => {
@@ -122,24 +121,3 @@ describe('remarkParseShortcodes', () => {
     return obj;
   }
 });
-
-describe('getLinesWithOffsets', () => {
-  test('should split into lines', () => {
-    const value = ' line1\n\nline2 \n\n    line3   \n\n';
-
-    const lines = getLinesWithOffsets(value);
-    expect(lines).toEqual([
-      { line: ' line1', start: 0 },
-      { line: 'line2', start: 8 },
-      { line: '    line3', start: 16 },
-      { line: '', start: 30 },
-    ]);
-  });
-
-  test('should return single item on no match', () => {
-    const value = ' line1    ';
-
-    const lines = getLinesWithOffsets(value);
-    expect(lines).toEqual([{ line: ' line1', start: 0 }]);
-  });
-});
diff --git a/packages/decap-cms-widget-markdown/src/serializers/remarkShortcodes.js b/packages/decap-cms-widget-markdown/src/serializers/remarkShortcodes.js
index 80b90b03d4b7..bef1dd3bdd6e 100644
--- a/packages/decap-cms-widget-markdown/src/serializers/remarkShortcodes.js
+++ b/packages/decap-cms-widget-markdown/src/serializers/remarkShortcodes.js
@@ -8,57 +8,40 @@ export function remarkParseShortcodes({ plugins }) {
   methods.unshift('shortcode');
 }
 
-export function getLinesWithOffsets(value) {
-  const SEPARATOR = '\n\n';
-  const splitted = value.split(SEPARATOR);
-  const trimmedLines = splitted
-    .reduce(
-      (acc, line) => {
-        const { start: previousLineStart, originalLength: previousLineOriginalLength } =
-          acc[acc.length - 1];
-
-        return [
-          ...acc,
-          {
-            line: line.trimEnd(),
-            start: previousLineStart + previousLineOriginalLength + SEPARATOR.length,
-            originalLength: line.length,
-          },
-        ];
-      },
-      [{ start: -SEPARATOR.length, originalLength: 0 }],
-    )
-    .slice(1)
-    .map(({ line, start }) => ({ line, start }));
-  return trimmedLines;
-}
-
 function createShortcodeTokenizer({ plugins }) {
+  plugins.forEach(plugin => {
+    if (plugin.pattern.flags.includes('m')) {
+      console.warn(
+        `Invalid RegExp: editor component '${plugin.id}' must not use the multiline flag in its pattern.`,
+      );
+    }
+  });
   return function tokenizeShortcode(eat, value, silent) {
-    // Attempt to find a regex match for each plugin's pattern, and then
-    // select the first by its occurrence in `value`. This ensures we won't
-    // skip a plugin that occurs later in the plugin registry, but earlier
-    // in the `value`.
-    const [{ plugin, match } = {}] = plugins
-      .toArray()
-      .map(plugin => {
-        let { pattern } = plugin;
-        // Plugin patterns must start with a caret (^) to match the beginning of the line.
-        // If the pattern does not start with a caret, we add it
-        // to ensure that remark consumes only the shortcode, without any leading text.
-        if (!pattern.source.startsWith('^')) {
-          pattern = new RegExp(`^${pattern.source}`, pattern.flags);
-        }
+    let match;
+    const potentialMatchValue = value.split('\n\n')[0].trimEnd();
+    const plugin = plugins.find(plugin => {
+      let { pattern } = plugin;
+      // Plugin patterns must start with a caret (^) to match the beginning of the block.
+      // If the pattern does not start with a caret, we add it
+      // to ensure that remark consumes only the shortcode, without any leading text.
+      if (!pattern.source.startsWith('^')) {
+        pattern = new RegExp(`^${pattern.source}`, pattern.flags);
+      }
 
-        return {
-          match: value.match(pattern),
-          plugin,
-        };
-      })
-      .filter(({ match }) => !!match)
-      .sort((a, b) => a.match.index - b.match.index);
+      match = value.match(pattern);
+      if (!match) {
+        match = potentialMatchValue.match(pattern);
+      }
+
+      return !!match;
+    });
 
     if (match) {
+      if (match.index > 0) {
+        console.warn(
+          `Invalid RegExp: editor component '${plugin.id}' must match from the beginning of the block.`,
+        );
+      }
       if (silent) {
         return true;
       }
diff --git a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js
index de17e092abfd..21a5e8e3d55e 100644
--- a/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js
+++ b/packages/decap-cms-widget-richtext/src/serializers/__tests__/remarkShortcodes.spec.js
@@ -2,7 +2,7 @@ import { Map, OrderedMap } from 'immutable';
 import unified from 'unified';
 import markdownToRemarkPlugin from 'remark-parse';
 
-import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes';
+import { remarkParseShortcodes } from '../remarkShortcodes';
 
 function process(value, plugins) {
   return unified()
@@ -33,30 +33,29 @@ describe('remarkParseShortcodes', () => {
         expect.arrayContaining(['foo\n\nbar']),
       );
     });
-    it('should match shortcodes based on order of occurrence in value', () => {
-      const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ });
-      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
+    it('should match shortcodes by first matching plugin', () => {
+      const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /^foo/ });
+      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /^bar/ });
       process(
-        'foo\n\nbar',
+        'bar\n\nfoo',
         OrderedMap([
-          [barEditorComponent.id, barEditorComponent],
           [fooEditorComponent.id, fooEditorComponent],
-        ]),
-      );
-      expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo']));
-    });
-    it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => {
-      const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
-      const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ });
-      process(
-        'foo\n\nbar\n\nbaz',
-        OrderedMap([
-          [bazEditorComponent.id, bazEditorComponent],
           [barEditorComponent.id, barEditorComponent],
         ]),
       );
+      // 'bar' is the first block, but 'foo' plugin is first in registry.
+      // 'foo' doesn't match 'bar', so 'bar' plugin matches.
       expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
     });
+    it('should warn when pattern uses multiline flag', () => {
+      const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+      const editorComponent = EditorComponent({ pattern: /^foo$/m });
+      process('foo', Map({ [editorComponent.id]: editorComponent }));
+      expect(warnSpy).toHaveBeenCalledWith(
+        expect.stringContaining('must not use the multiline flag'),
+      );
+      warnSpy.mockRestore();
+    });
   });
   describe('parse', () => {
     describe('pattern with leading caret', () => {
@@ -122,24 +121,3 @@ describe('remarkParseShortcodes', () => {
     return obj;
   }
 });
-
-describe('getLinesWithOffsets', () => {
-  test('should split into lines', () => {
-    const value = ' line1\n\nline2 \n\n    line3   \n\n';
-
-    const lines = getLinesWithOffsets(value);
-    expect(lines).toEqual([
-      { line: ' line1', start: 0 },
-      { line: 'line2', start: 8 },
-      { line: '    line3', start: 16 },
-      { line: '', start: 30 },
-    ]);
-  });
-
-  test('should return single item on no match', () => {
-    const value = ' line1    ';
-
-    const lines = getLinesWithOffsets(value);
-    expect(lines).toEqual([{ line: ' line1', start: 0 }]);
-  });
-});
diff --git a/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js
index 16fb5768704d..bef1dd3bdd6e 100644
--- a/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js
+++ b/packages/decap-cms-widget-richtext/src/serializers/remarkShortcodes.js
@@ -8,78 +8,46 @@ export function remarkParseShortcodes({ plugins }) {
   methods.unshift('shortcode');
 }
 
-export function getLinesWithOffsets(value) {
-  const SEPARATOR = '\n\n';
-  const splitted = value.split(SEPARATOR);
-  const trimmedLines = splitted
-    .reduce(
-      (acc, line) => {
-        const { start: previousLineStart, originalLength: previousLineOriginalLength } =
-          acc[acc.length - 1];
-
-        return [
-          ...acc,
-          {
-            line: line.trimEnd(),
-            start: previousLineStart + previousLineOriginalLength + SEPARATOR.length,
-            originalLength: line.length,
-          },
-        ];
-      },
-      [{ start: -SEPARATOR.length, originalLength: 0 }],
-    )
-    .slice(1)
-    .map(({ line, start }) => ({ line, start }));
-  return trimmedLines;
-}
-
-function matchFromLines({ trimmedLines, plugin }) {
-  for (const { line, start } of trimmedLines) {
-    const match = line.match(plugin.pattern);
-    if (match) {
-      match.index += start;
-      return match;
-    }
-  }
-}
-
 function createShortcodeTokenizer({ plugins }) {
+  plugins.forEach(plugin => {
+    if (plugin.pattern.flags.includes('m')) {
+      console.warn(
+        `Invalid RegExp: editor component '${plugin.id}' must not use the multiline flag in its pattern.`,
+      );
+    }
+  });
   return function tokenizeShortcode(eat, value, silent) {
-    // Plugin patterns may rely on `^` and `$` tokens, even if they don't
-    // use the multiline flag. To support this, we fall back to searching
-    // through each line individually, trimming trailing whitespace and
-    // newlines, if we don't initially match on a pattern. We keep track of
-    // the starting position of each line so that we can sort correctly
-    // across the full multiline matches.
-    const trimmedLines = getLinesWithOffsets(value);
+    let match;
+    const potentialMatchValue = value.split('\n\n')[0].trimEnd();
+    const plugin = plugins.find(plugin => {
+      let { pattern } = plugin;
+      // Plugin patterns must start with a caret (^) to match the beginning of the block.
+      // If the pattern does not start with a caret, we add it
+      // to ensure that remark consumes only the shortcode, without any leading text.
+      if (!pattern.source.startsWith('^')) {
+        pattern = new RegExp(`^${pattern.source}`, pattern.flags);
+      }
+
+      match = value.match(pattern);
+      if (!match) {
+        match = potentialMatchValue.match(pattern);
+      }
 
-    // Attempt to find a regex match for each plugin's pattern, and then
-    // select the first by its occurrence in `value`. This ensures we won't
-    // skip a plugin that occurs later in the plugin registry, but earlier
-    // in the `value`.
-    const [{ plugin, match } = {}] = plugins
-      .toArray()
-      .map(plugin => ({
-        match: value.match(plugin.pattern) || matchFromLines({ trimmedLines, plugin }),
-        plugin,
-      }))
-      .filter(({ match }) => !!match)
-      .sort((a, b) => a.match.index - b.match.index);
+      return !!match;
+    });
 
     if (match) {
+      if (match.index > 0) {
+        console.warn(
+          `Invalid RegExp: editor component '${plugin.id}' must match from the beginning of the block.`,
+        );
+      }
       if (silent) {
         return true;
       }
 
       const shortcodeData = plugin.fromBlock(match);
 
-      // Only eat if the match starts at the beginning of the remaining text.
-      // If it's further in (e.g. found via matchFromLines inside a paragraph),
-      // fall through silently so remark's inline parsers can handle it.
-      if (value.slice(0, match[0].length) !== match[0]) {
-        return;
-      }
-
       try {
         return eat(match[0])({
           type: 'shortcode',

From ea93dd54afbb6ed4b177e503fe1b1f3bfe142438 Mon Sep 17 00:00:00 2001
From: Anthony Balaine 
Date: Wed, 22 Apr 2026 11:17:25 -0400
Subject: [PATCH 35/40] fix(core): return full entry object from invokeEvent
 instead of just data (#7667)

The `invokeEvent` function in registry.js was returning only the data
payload (`_data.entry.get('data')`) instead of returning the complete
entry object. This caused entry metadata fields (slug, path, meta, etc.)
to be lost during event processing, particularly affecting file
collections that rely on slug for file lookups.

When `invokePreSaveEvent` was called during entry persistence:

1. Entry starts with all metadata: slug, path, meta, isModification, etc.
2. `invokeEvent` is called with the full entry object
3. `invokeEvent` returns ONLY the data payload, stripping metadata
4. Calling code replaces the full entry with just the data payload
5. Downstream operations like `entryToRaw` receive incomplete entry
6. `fieldsOrder` method fails trying to match slug to file definitions

This manifested as errors like:
- "No file found for undefined in [collection]"
- "TypeError: can't access property 'toJS', file is undefined"

The bug was introduced in commit 0d7e36ba7 (January 31, 2020) and has
existed for ~5 years, but was only exposed in recent versions (3.2.0+)
when file collection persistence logic started calling `invokePreSaveEvent`
and fully relying on its return value to update the entry draft.

Changed `invokeEvent` to return `_data.entry` (the complete entry object)
instead of `_data.entry.get('data')` (just the data payload).

This preserves all entry metadata through the event handler chain, ensuring
that callers like `invokePreSaveEvent` receive the complete entry object
with all properties intact.

Verified fix resolves file collection publishing issues where:
- Custom widgets modify entry data during save
- File collections with single files rely on slug matching
- Entry metadata must be preserved through preSave event handlers

Fixes #[issue-number-if-exists]

fix(core): test returning full entry when invoking preSave handler

Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
Co-authored-by: Martin Jagodic 
---
 .../src/__tests__/backend.spec.js             | 62 +++++++++++++++++++
 .../src/lib/__tests__/registry.spec.js        |  6 +-
 packages/decap-cms-core/src/lib/registry.js   |  5 +-
 3 files changed, 69 insertions(+), 4 deletions(-)

diff --git a/packages/decap-cms-core/src/__tests__/backend.spec.js b/packages/decap-cms-core/src/__tests__/backend.spec.js
index c673c19a01c4..c6c74269dc1e 100644
--- a/packages/decap-cms-core/src/__tests__/backend.spec.js
+++ b/packages/decap-cms-core/src/__tests__/backend.spec.js
@@ -391,6 +391,68 @@ describe('Backend', () => {
       expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
       expect(backend.entryToRaw).toHaveBeenCalledWith(collection, newEntry);
     });
+
+    it('should preserve slug when preSave event handler modifies file collection entry', async () => {
+      const implementation = {
+        init: jest.fn(() => implementation),
+        persistEntry: jest.fn(() => implementation),
+      };
+
+      const config = {
+        backend: {
+          commit_messages: 'commit-messages',
+        },
+      };
+
+      // File collection with a single file
+      const collection = Map({
+        name: 'settings',
+        type: FILES,
+        files: List([
+          Map({
+            name: 'config',
+            file: 'data/config.json',
+            fields: List([Map({ name: 'title', widget: 'string' })]),
+          }),
+        ]),
+      });
+
+      const originalEntry = Map({
+        slug: 'config',
+        path: 'data/config.json',
+        data: Map({ title: 'original' }),
+        meta: Map({ path: 'data/config.json' }),
+      });
+
+      const entryDraft = Map({
+        entry: originalEntry,
+      });
+
+      const user = { login: 'login', name: 'name' };
+      const backend = new Backend(implementation, { config, backendName: 'github' });
+
+      backend.currentUser = jest.fn().mockResolvedValue(user);
+      backend.entryToRaw = jest.fn().mockReturnValue('content');
+
+      // Mock invokePreSaveEvent to simulate a preSave handler that modifies data
+      // This is what happens when custom widgets or event handlers modify entry data
+      // The key is that it returns the FULL entry with slug, not just the data
+      backend.invokePreSaveEvent = jest.fn().mockImplementation(async entry => {
+        // Simulate a preSave handler modifying the data field
+        return entry.setIn(['data', 'title'], 'modified');
+      });
+
+      await backend.persistEntry({ config, collection, entryDraft });
+
+      // Verify entryToRaw was called with an entry that has the slug
+      expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
+      const entryPassedToRaw = backend.entryToRaw.mock.calls[0][1];
+
+      // Critical assertion: slug must be preserved
+      expect(entryPassedToRaw.get('slug')).toBe('config');
+      expect(entryPassedToRaw.get('path')).toBe('data/config.json');
+      expect(entryPassedToRaw.getIn(['data', 'title'])).toBe('modified');
+    });
   });
 
   describe('persistMedia', () => {
diff --git a/packages/decap-cms-core/src/lib/__tests__/registry.spec.js b/packages/decap-cms-core/src/lib/__tests__/registry.spec.js
index 42d24addc03d..2961b84b3fa8 100644
--- a/packages/decap-cms-core/src/lib/__tests__/registry.spec.js
+++ b/packages/decap-cms-core/src/lib/__tests__/registry.spec.js
@@ -200,7 +200,7 @@ describe('registry', () => {
         });
       });
 
-      it(`should return an updated entry's DataMap`, async () => {
+      it(`should return the complete updated entry object`, async () => {
         const { registerEventListener, invokeEvent } = require('../registry');
 
         const event = 'preSave';
@@ -233,7 +233,7 @@ describe('registry', () => {
         expect(handler1).toHaveBeenCalledWith(data, options);
         expect(handler2).toHaveBeenCalledWith(dataAfterFirstHandlerExecution, options);
 
-        expect(result).toEqual(dataAfterSecondHandlerExecution.entry.get('data'));
+        expect(result).toEqual(dataAfterSecondHandlerExecution.entry);
       });
 
       it('should allow multiple events to not return a value', async () => {
@@ -254,7 +254,7 @@ describe('registry', () => {
 
         expect(handler1).toHaveBeenCalledWith(data, options);
         expect(handler2).toHaveBeenCalledWith(data, options);
-        expect(result).toEqual(data.entry.get('data'));
+        expect(result).toEqual(data.entry);
       });
     });
   });
diff --git a/packages/decap-cms-core/src/lib/registry.js b/packages/decap-cms-core/src/lib/registry.js
index 573d8867762c..5bce1d960999 100644
--- a/packages/decap-cms-core/src/lib/registry.js
+++ b/packages/decap-cms-core/src/lib/registry.js
@@ -257,7 +257,10 @@ export async function invokeEvent({ name, data }) {
       _data = { ...data, entry };
     }
   }
-  return _data.entry.get('data');
+  // Return the full entry object with all metadata (slug, path, meta, etc.)
+  // rather than just the data payload. Callers like invokePreSaveEvent expect
+  // the complete entry object to be preserved through the event handler chain.
+  return _data.entry;
 }
 
 export function removeEventListener({ name, handler }) {

From e1b40532793c522e5f1f5181bc5e77e76a977db1 Mon Sep 17 00:00:00 2001
From: Sem Postma 
Date: Sat, 23 May 2026 12:42:11 +0200
Subject: [PATCH 36/40] Update
 packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js

Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 .../src/components/Editor/EditorControlPane/EditorControl.js    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
index 04f07623f01c..dd0d0e6201a3 100644
--- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
+++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
@@ -465,7 +465,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
     ...stateProps,
     ...dispatchProps,
     ...ownProps,
-    boundGetAsset: dispatchProps.boundGetAsset(stateProps.collection, stateProps.getEntry()),
+    boundGetAsset: dispatchProps.boundGetAsset(stateProps.collection),
   };
 }
 

From 971bf906e37063ca73448d2e46fac364343e655f Mon Sep 17 00:00:00 2001
From: Sem Postma 
Date: Sat, 23 May 2026 12:42:30 +0200
Subject: [PATCH 37/40] Update packages/decap-cms-core/src/actions/media.ts

Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 packages/decap-cms-core/src/actions/media.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/decap-cms-core/src/actions/media.ts b/packages/decap-cms-core/src/actions/media.ts
index bbc3fa193749..c96ba6e0d3a4 100644
--- a/packages/decap-cms-core/src/actions/media.ts
+++ b/packages/decap-cms-core/src/actions/media.ts
@@ -89,7 +89,7 @@ export const boundGetAsset = memoize(
     }
 
     return bound;
-  }, (_, entry) => entry
+  }, (_, _, entry) => entry
 );
 
 boundGetAsset.cache = new WeakMap();

From 59cf2377c34f41ddff9c9aed39916e88ca3cc825 Mon Sep 17 00:00:00 2001
From: Sem Postma 
Date: Sat, 23 May 2026 12:42:42 +0200
Subject: [PATCH 38/40] Update
 packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js

Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
---
 .../Editor/EditorControlPane/EditorControl.js         | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
index dd0d0e6201a3..77e28012ce38 100644
--- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
+++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
@@ -415,10 +415,13 @@ const stable = {
     return state.entryDraft.get('entry');
   },
 
-  getBoundedAsset(collection, entry) {
-    const dispatch = store.dispatch;
-    return boundGetAsset(dispatch, collection, entry);
-  },
+  getBoundedAsset: memoize(collection => {
+    return (path, field) => {
+      const state = store.getState();
+      const entry = state.entryDraft.get('entry');
+      return store.dispatch(getAsset({ collection, entry, path, field }));
+    };
+  }),
 };
 
 function mapStateToProps(state) {

From 5a4c35168e6dd93478b184dc8bcd7747785f22de Mon Sep 17 00:00:00 2001
From: Sem Postma 
Date: Sat, 23 May 2026 13:07:54 +0200
Subject: [PATCH 39/40] fix: small refactor

---
 packages/decap-cms-core/src/actions/media.ts                  | 4 ++--
 .../src/components/Editor/EditorControlPane/EditorControl.js  | 2 +-
 packages/decap-cms-widget-list/src/ListControl.js             | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/decap-cms-core/src/actions/media.ts b/packages/decap-cms-core/src/actions/media.ts
index c96ba6e0d3a4..3c8e3d45cd03 100644
--- a/packages/decap-cms-core/src/actions/media.ts
+++ b/packages/decap-cms-core/src/actions/media.ts
@@ -87,9 +87,9 @@ export const boundGetAsset = memoize(
       const asset = dispatch(getAsset({ collection, entry, path, field }));
       return asset;
     }
-
     return bound;
-  }, (_, _, entry) => entry
+  },
+  (_dispatch, _collection, entry) => entry,
 );
 
 boundGetAsset.cache = new WeakMap();
diff --git a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
index 77e28012ce38..cef228cc506c 100644
--- a/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
+++ b/packages/decap-cms-core/src/components/Editor/EditorControlPane/EditorControl.js
@@ -15,7 +15,7 @@ import gfm from 'remark-gfm';
 
 import { resolveWidget, getEditorComponents } from '../../../lib/registry';
 import { clearFieldErrors, tryLoadEntry, validateMetaField } from '../../../actions/entries';
-import { addAsset, boundGetAsset } from '../../../actions/media';
+import { addAsset, getAsset } from '../../../actions/media';
 import { selectIsLoadingAsset } from '../../../reducers/medias';
 import { query, clearSearch } from '../../../actions/search';
 import { store } from '../../../redux';
diff --git a/packages/decap-cms-widget-list/src/ListControl.js b/packages/decap-cms-widget-list/src/ListControl.js
index c5d642ec4e64..f5c3c8d4803e 100644
--- a/packages/decap-cms-widget-list/src/ListControl.js
+++ b/packages/decap-cms-widget-list/src/ListControl.js
@@ -641,7 +641,7 @@ export default class ListControl extends React.Component {
 
   getStableParentIds = memoize(
     (parentIds, forID, key) => [...parentIds, forID, key],
-    (parentIds, forID, key) => JSON.stringify([ ...parentIds, forID, key ]),
+    (parentIds, forID, key) => JSON.stringify([...parentIds, forID, key]),
   );
 
   // eslint-disable-next-line react/display-name

From 9358b44e92730fa4bf3e67aad929c23927eb7c35 Mon Sep 17 00:00:00 2001
From: Sem Postma 
Date: Sat, 23 May 2026 13:13:57 +0200
Subject: [PATCH 40/40] fix: package-lock

---
 package-lock.json | 142 +++++++++++++++++++++++++++++++++-------------
 1 file changed, 102 insertions(+), 40 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 08b2b6fce199..96874f87a1d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -162,10 +162,12 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.27.1",
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.1.1"
       },
@@ -228,11 +230,13 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.28.5",
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.28.5",
-        "@babel/types": "^7.28.5",
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
         "@jridgewell/gen-mapping": "^0.3.12",
         "@jridgewell/trace-mapping": "^0.3.28",
         "jsesc": "^3.0.2"
@@ -356,24 +360,28 @@
       }
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.27.1",
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/traverse": "^7.27.1",
-        "@babel/types": "^7.27.1"
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.28.3",
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-imports": "^7.27.1",
-        "@babel/helper-validator-identifier": "^7.27.1",
-        "@babel/traverse": "^7.28.3"
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -394,7 +402,9 @@
       }
     },
     "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.27.1",
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
       "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
@@ -492,10 +502,12 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.28.5",
+      "version": "7.29.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+      "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
       "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.28.5"
+        "@babel/types": "^7.29.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -1205,14 +1217,16 @@
       }
     },
     "node_modules/@babel/plugin-transform-modules-systemjs": {
-      "version": "7.28.5",
+      "version": "7.29.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
+      "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.28.3",
-        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
         "@babel/helper-validator-identifier": "^7.28.5",
-        "@babel/traverse": "^7.28.5"
+        "@babel/traverse": "^7.29.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1806,27 +1820,31 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.27.2",
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
       "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.27.1",
-        "@babel/parser": "^7.27.2",
-        "@babel/types": "^7.27.1"
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.28.5",
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
       "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.27.1",
-        "@babel/generator": "^7.28.5",
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
         "@babel/helper-globals": "^7.28.0",
-        "@babel/parser": "^7.28.5",
-        "@babel/template": "^7.27.2",
-        "@babel/types": "^7.28.5",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
         "debug": "^4.3.1"
       },
       "engines": {
@@ -1834,7 +1852,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.28.5",
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
       "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.27.1",
@@ -6395,6 +6415,21 @@
         "cypress": ">10.0.0"
       }
     },
+    "node_modules/@simple-git/args-pathspec": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz",
+      "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==",
+      "license": "MIT"
+    },
+    "node_modules/@simple-git/argv-parser": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz",
+      "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@simple-git/args-pathspec": "^1.0.3"
+      }
+    },
     "node_modules/@sinclair/typebox": {
       "version": "0.27.8",
       "license": "MIT"
@@ -9055,16 +9090,36 @@
       "license": "MIT"
     },
     "node_modules/axios": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
-      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
+      "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
       "license": "MIT",
       "dependencies": {
-        "follow-redirects": "^1.15.11",
+        "follow-redirects": "^1.16.0",
         "form-data": "^4.0.5",
         "proxy-from-env": "^2.1.0"
       }
     },
+    "node_modules/axios/node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/babel-core": {
       "version": "7.0.0-bridge.0",
       "dev": true,
@@ -14286,7 +14341,9 @@
       "license": "MIT"
     },
     "node_modules/fast-uri": {
-      "version": "3.1.0",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+      "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
       "funding": [
         {
           "type": "github",
@@ -14694,6 +14751,7 @@
     },
     "node_modules/follow-redirects": {
       "version": "1.15.11",
+      "dev": true,
       "funding": [
         {
           "type": "individual",
@@ -25847,7 +25905,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.5.6",
+      "version": "8.5.14",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
       "dev": true,
       "funding": [
         {
@@ -28670,13 +28730,15 @@
       "license": "MIT"
     },
     "node_modules/simple-git": {
-      "version": "3.32.3",
-      "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.32.3.tgz",
-      "integrity": "sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==",
+      "version": "3.36.0",
+      "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz",
+      "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==",
       "license": "MIT",
       "dependencies": {
         "@kwsites/file-exists": "^1.1.1",
         "@kwsites/promise-deferred": "^1.1.1",
+        "@simple-git/args-pathspec": "^1.0.3",
+        "@simple-git/argv-parser": "^1.1.0",
         "debug": "^4.4.0"
       },
       "funding": {
@@ -34116,4 +34178,4 @@
       "license": "MIT"
     }
   }
-}
+}
\ No newline at end of file