Description
ContentBlock::_normalizeValueInternal() constrains its batched lookup by the owner's revision status:
https://github.com/craftcms/cms/blob/5.10.13.2/src/fields/ContentBlock.php#L503-L511
->revisions($element->getIsRevision())
When a batched resave processes a revision-owned nested element, this lookup misses. The method falls through to createContentBlockElement(), and NestedElementManager::deleteOtherNestedElements() then soft-deletes the canonical ContentBlock, leaving the new empty one in its place.
Conditions required
- A ContentBlock field on an entry type used as a Matrix block type (nested, not top-level)
- Revisions exist for the owning entries
- A batch resave that includes revisions —
Entries::saveSection() queues exactly this ('revisions' => null) on any uriFormat / hasUrls change
Single-entry resaves and single-entry-type batches do not trigger it. I could not reproduce with a ContentBlock directly on a top-level entry type; the nesting appears to be required.
Steps to reproduce
- Multi-site install, ContentBlock field inside a Matrix block entry type, entries populated and with revisions
- Change a section's
uriFormat (adding a key for a new entry type is enough — no existing URI need change)
- Run the queued
ResaveElements job
Populated ContentBlocks are soft-deleted and replaced with empty ones.
Observed
Instrumented _normalizeValueInternal() against a production dataset during a section-wide resave: 125 lookup misses, 123 of them on revision elements. Two failure shapes — the query returns 0 rows, or returns rows for other owners but not the wanted ownerId:
[marketoFields el=Entry#3168499/site2:REV] BRANCH=batched qResult=13 sameSite=13
types[border=1,singleColumn=8,twoColumn=3,singleImage=1]
blocksReturned=0 keys=[] wanted=3168499 -> MISS, new empty block created
Result: 1,824 canonical ContentBlocks soft-deleted and replaced.
Confirmation
Changing that single line to ->revisions(null) — same job, same data:
|
before |
after |
| batched lookup misses |
125 |
4 |
| canonical blocks deleted |
1,824 |
1,118 |
| deleted blocks holding content |
10 |
0 |
The remaining deletions are empty placeholder blocks only. (revisions(null) is a diagnostic, not a proposed fix — it presumably reverts what #18161 addressed.)
Worth noting: the vast majority of churned blocks are empty, so this is normally invisible. It only surfaces where an author had actually populated a block, which is why it can run for months unnoticed and then appear as sudden, selective content loss.
Possibly related
#19058 / CMS-2186 — ContentBlock content loss, closed as unreproducible, same symptom (populated ContentBlock empty in a later revision after an unrelated change).
Craft CMS version
5.10.13.2 (ContentBlock.php, NestedElementManager.php, Elements.php and ResaveElements.php are byte-identical in 5.10.14). Line introduced in 5.9.0 — absent in 5.8.0.
PHP version
8.3
Database type and version
MySQL 8.0
Notes
Multi-site install (8 sites).
Description
ContentBlock::_normalizeValueInternal()constrains its batched lookup by the owner's revision status:https://github.com/craftcms/cms/blob/5.10.13.2/src/fields/ContentBlock.php#L503-L511
When a batched resave processes a revision-owned nested element, this lookup misses. The method falls through to
createContentBlockElement(), andNestedElementManager::deleteOtherNestedElements()then soft-deletes the canonical ContentBlock, leaving the new empty one in its place.Conditions required
Entries::saveSection()queues exactly this ('revisions' => null) on anyuriFormat/hasUrlschangeSingle-entry resaves and single-entry-type batches do not trigger it. I could not reproduce with a ContentBlock directly on a top-level entry type; the nesting appears to be required.
Steps to reproduce
uriFormat(adding a key for a new entry type is enough — no existing URI need change)ResaveElementsjobPopulated ContentBlocks are soft-deleted and replaced with empty ones.
Observed
Instrumented
_normalizeValueInternal()against a production dataset during a section-wide resave: 125 lookup misses, 123 of them on revision elements. Two failure shapes — the query returns 0 rows, or returns rows for other owners but not the wantedownerId:Result: 1,824 canonical ContentBlocks soft-deleted and replaced.
Confirmation
Changing that single line to
->revisions(null)— same job, same data:The remaining deletions are empty placeholder blocks only. (
revisions(null)is a diagnostic, not a proposed fix — it presumably reverts what #18161 addressed.)Worth noting: the vast majority of churned blocks are empty, so this is normally invisible. It only surfaces where an author had actually populated a block, which is why it can run for months unnoticed and then appear as sudden, selective content loss.
Possibly related
#19058 / CMS-2186 — ContentBlock content loss, closed as unreproducible, same symptom (populated ContentBlock empty in a later revision after an unrelated change).
Craft CMS version
5.10.13.2 (
ContentBlock.php,NestedElementManager.php,Elements.phpandResaveElements.phpare byte-identical in 5.10.14). Line introduced in 5.9.0 — absent in 5.8.0.PHP version
8.3
Database type and version
MySQL 8.0
Notes
Multi-site install (8 sites).