Hey @ryancramerdesign
a customer of mine mentioned a strange bug that occurred while copying unpublished pages. Since I couldn't find any obvious errors, I gave Claude-Code access to the log files and the database, and here is his error report.
I hope this approach to report an issue is fine to you.
Cheers Jens
Short description of the issue
When a page tree is copied with "Make cloned children unpublished?" checked,
ProcessPageClone registers a cloneReady hook that adds statusUnpublished to every
page cloned during that request. Repeater item pages are cloned in the same request by
FieldtypeRepeater::___savePageField() via $pages->clone() — so they are unpublished too,
although they are not children in the page tree.
The items end up at status = 2049 (statusOn + statusUnpublished, without
statusHidden) and drop out of the field's value list: field_<name>.data for the copy stays
empty and count is 0. The content itself is fully intact in the database — text in all
languages, images, settings — it is just no longer linked, and therefore invisible in the
admin and in the frontend. No error, no warning.
ProcessPageClone.module:384–389:
if($cloneTree && $input->post('clone_tree_unpublished')) {
$this->wire()->pages->addHook('cloneReady', function(HookEvent $event) use($page) {
$child = $event->arguments(1); /** @var Page $child */
if($page->id !== $child->id) $child->addStatus(Page::statusUnpublished);
});
}
FieldtypeRepeater.module:1608–1615 — the clone that gets caught by it. It runs because the
copied page's repeater items still point at the original page's repeater parent:
if($p->parent->id != $parent_id) {
// clone the individual repeater pages
$value->remove($p);
$p = $pages->clone($p, $repeaterParent, false, $saveOptions);
$value->add($p);
continue;
}
The $page->id !== $child->id guard does not help here — see the separate note on it below —
so repeater fields on the copied page are affected in the same way as those on its children.
Expected behavior
"Make cloned children unpublished?" unpublishes the cloned children, grandchildren etc. in the
page tree. Repeater item pages below /admin/repeaters/ are internal storage and keep the
status they had on the original.
Actual behavior
Every page cloned in the request is unpublished, repeater item pages included. Repeater and
RepeaterMatrix fields on the copy silently come out empty.
What makes this hard to notice: FieldtypeRepeater.module:1628–1631 re-adopts unpublished,
non-hidden items on the next save and publishes them (line 1652) once _repeater_processed is
set — which only the repeater editor in the admin does. So every section that happens to be
opened and saved after the copy heals itself, and every other one stays empty. The damage
therefore looks random rather than systematic, which is what delays the diagnosis.
On a production site this hit 170 repeater items across three page trees, at every nesting
level (repeaters inside repeater items included). Some of the affected items were still being
edited by the team four months later without anyone realising they were not rendering.
Steps to reproduce the issue
- Create page A with a child page B. Give B a Repeater (or RepeaterMatrix) field with
2–3 filled items.
- In the page list, choose Copy on A, check "Copy children too?" and check
"Make cloned children unpublished?", then save.
- Open the copy of B in the page editor → the repeater field is empty, while B's repeater
field is unchanged.
- In the database, the items are still there:
SELECT id, status FROM pages WHERE parent_id = <repeater parent of the copy>
→ status = 2049. And SELECT data FROM field_<repeatername> WHERE pages_id = <copy of B>
→ no rows.
- Publish the copy, then open its repeater field in the editor and save once → the items
reappear and are published (the self-healing path described above). Any section not touched
this way stays empty indefinitely.
Measured on a real page tree (5 sections, 38 repeater items at section level, 268 pages
created by the clone), by registering exactly the hook from ProcessPageClone above and then
calling $pages->clone($page, $parent, true):
|
items at status = 2049 |
| directly after the clone |
206 |
| after publishing the copy, as an editor would |
138 — permanently lost |
The difference of 68 are FieldtypeFieldsetPage items, and those are not part of this bug:
FieldtypeFieldsetPage::___savePageField() (around line 446) intentionally mirrors the owner
page's unpublished/hidden status onto its fieldset item and removes it again when the owner is
published. Only the FieldtypeRepeater items stay behind.
Optional: Suggestion for a possible fix
Exclude repeater pages from the hook:
$this->wire()->pages->addHook('cloneReady', function(HookEvent $event) use($page) {
$child = $event->arguments(1); /** @var Page $child */
if($page->id === $child->id) return;
if($child instanceof RepeaterPage) return; // internal repeater storage, not a tree child
$child->addStatus(Page::statusUnpublished);
});
A parent-based check would work as well (whether $child->parent lives below the repeaters
root, FieldtypeRepeater::repeatersRootPageName / name prefix
FieldtypeRepeater::repeaterPageNamePrefix) — you will know which of the two is the canonical
test here.
Possibly related in mechanism: #2212 (a status bit propagating onto repeater item pages that
should not receive it).
Side note: the $page->id !== $child->id guard never matches
PagesEditor::_clone() sets the copy's id to 0 before firing cloneReady
($copy->setQuietly('id', $options['forceID'] > 1 ? (int) $options['forceID'] : 0);,
wire/core/Pages/PagesEditor.php:1593 in 3.0.271 dev; cloneReady is called at line 1620).
$page->id !== $child->id is therefore always true, and the top-level copy itself gets
unpublished as well — not only the children, as the option's label suggests. That may be
intended, but if it is, the guard is dead code; if it is not, it is a second small bug in the
same five lines.
Workaround for anyone hitting this before a fix ships
A userland hook in site/ready.php, verified against the reproduction above. The core hook is
registered at request time with the default priority of 100, so a higher number is required in
order to run after it (WireHooks sorts ascending and re-sorts on every addHook()):
$this->wire()->pages->addHookAfter('cloneReady', function(HookEvent $event) {
$source = $event->arguments(0);
$copy = $event->arguments(1);
if(!$copy instanceof RepeaterPage) return; // repeater items only
if($source->isUnpublished()) return; // source itself unpublished: leave it
if(!$source->hasStatus(Page::statusOn)) return; // deliberately switched off: leave it
if(!$copy->isUnpublished()) return;
$copy->removeStatus(Page::statusUnpublished);
}, array('priority' => 200));
⚠ Note for anyone writing repair code for existing damage
| Status |
Meaning |
Treatment |
2049 = statusOn + unpublished |
lost by the copy bug |
repair |
2048 = unpublished without statusOn |
deliberately switched off by an editor |
do not touch |
3073 = + hidden |
empty "ready page" |
ignore |
The clone hook cannot produce 2048 (it never removes statusOn), so the distinction is
provable rather than merely plausible. On the site mentioned above, a repair run without this
check would have re-enabled 93 deliberately disabled elements in a single page tree.
Setup/Environment
- ProcessWire version: observed on 3.0.255, reproduced on 3.0.246; the code in question is
unchanged on the current dev branch (3.0.271, checked 2026-08-31)
- (Optional) PHP version: 8.x
- (Optional) Any 3rd party modules that are installed and could be related to the issue:
FieldtypeRepeaterMatrix (the same happens with the core Repeater fieldtype)
Hey @ryancramerdesign
a customer of mine mentioned a strange bug that occurred while copying unpublished pages. Since I couldn't find any obvious errors, I gave Claude-Code access to the log files and the database, and here is his error report.
I hope this approach to report an issue is fine to you.
Cheers Jens
Short description of the issue
When a page tree is copied with "Make cloned children unpublished?" checked,
ProcessPageCloneregisters acloneReadyhook that addsstatusUnpublishedto everypage cloned during that request. Repeater item pages are cloned in the same request by
FieldtypeRepeater::___savePageField()via$pages->clone()— so they are unpublished too,although they are not children in the page tree.
The items end up at
status = 2049(statusOn+statusUnpublished, withoutstatusHidden) and drop out of the field's value list:field_<name>.datafor the copy staysempty and
countis0. The content itself is fully intact in the database — text in alllanguages, images, settings — it is just no longer linked, and therefore invisible in the
admin and in the frontend. No error, no warning.
ProcessPageClone.module:384–389:FieldtypeRepeater.module:1608–1615— the clone that gets caught by it. It runs because thecopied page's repeater items still point at the original page's repeater parent:
The
$page->id !== $child->idguard does not help here — see the separate note on it below —so repeater fields on the copied page are affected in the same way as those on its children.
Expected behavior
"Make cloned children unpublished?" unpublishes the cloned children, grandchildren etc. in the
page tree. Repeater item pages below
/admin/repeaters/are internal storage and keep thestatus they had on the original.
Actual behavior
Every page cloned in the request is unpublished, repeater item pages included. Repeater and
RepeaterMatrix fields on the copy silently come out empty.
What makes this hard to notice:
FieldtypeRepeater.module:1628–1631re-adopts unpublished,non-hidden items on the next save and publishes them (line 1652) once
_repeater_processedisset — which only the repeater editor in the admin does. So every section that happens to be
opened and saved after the copy heals itself, and every other one stays empty. The damage
therefore looks random rather than systematic, which is what delays the diagnosis.
On a production site this hit 170 repeater items across three page trees, at every nesting
level (repeaters inside repeater items included). Some of the affected items were still being
edited by the team four months later without anyone realising they were not rendering.
Steps to reproduce the issue
2–3 filled items.
"Make cloned children unpublished?", then save.
field is unchanged.
SELECT id, status FROM pages WHERE parent_id = <repeater parent of the copy>→
status = 2049. AndSELECT data FROM field_<repeatername> WHERE pages_id = <copy of B>→ no rows.
reappear and are published (the self-healing path described above). Any section not touched
this way stays empty indefinitely.
Measured on a real page tree (5 sections, 38 repeater items at section level, 268 pages
created by the clone), by registering exactly the hook from
ProcessPageCloneabove and thencalling
$pages->clone($page, $parent, true):status = 2049The difference of 68 are
FieldtypeFieldsetPageitems, and those are not part of this bug:FieldtypeFieldsetPage::___savePageField()(around line 446) intentionally mirrors the ownerpage's unpublished/hidden status onto its fieldset item and removes it again when the owner is
published. Only the
FieldtypeRepeateritems stay behind.Optional: Suggestion for a possible fix
Exclude repeater pages from the hook:
A parent-based check would work as well (whether
$child->parentlives below the repeatersroot,
FieldtypeRepeater::repeatersRootPageName/ name prefixFieldtypeRepeater::repeaterPageNamePrefix) — you will know which of the two is the canonicaltest here.
Possibly related in mechanism: #2212 (a status bit propagating onto repeater item pages that
should not receive it).
Side note: the
$page->id !== $child->idguard never matchesPagesEditor::_clone()sets the copy's id to0before firingcloneReady(
$copy->setQuietly('id', $options['forceID'] > 1 ? (int) $options['forceID'] : 0);,wire/core/Pages/PagesEditor.php:1593in 3.0.271 dev;cloneReadyis called at line 1620).$page->id !== $child->idis therefore always true, and the top-level copy itself getsunpublished as well — not only the children, as the option's label suggests. That may be
intended, but if it is, the guard is dead code; if it is not, it is a second small bug in the
same five lines.
Workaround for anyone hitting this before a fix ships
A userland hook in
site/ready.php, verified against the reproduction above. The core hook isregistered at request time with the default priority of 100, so a higher number is required in
order to run after it (
WireHookssorts ascending and re-sorts on everyaddHook()):⚠ Note for anyone writing repair code for existing damage
statusOn+unpublishedunpublishedwithoutstatusOnhiddenThe clone hook cannot produce 2048 (it never removes
statusOn), so the distinction isprovable rather than merely plausible. On the site mentioned above, a repair run without this
check would have re-enabled 93 deliberately disabled elements in a single page tree.
Setup/Environment
unchanged on the current dev branch (3.0.271, checked 2026-08-31)
FieldtypeRepeaterMatrix (the same happens with the core Repeater fieldtype)