Short description
When editing a page that contains a RepeaterMatrix (or plain Repeater) field, the MarkupQA "wakeup" of a rich-text/textarea subfield can emit a warning for a stored link that no longer resolves. The warning identifies the field's internal repeater page path rather than the content block the editor actually sees. For example (values illustrative):
Unable to resolve link on page /processwire/repeaters/for-field-12/for-page-1041/1700000000-1234-1/ in field "Body": wakeup: /old/moved-page/
For an editor this isn't actionable — /processwire/repeaters/for-field-12/for-page-1041/1700000000-1234-1/ gives no hint about which matrix item on which page holds the broken link, and it surfaces an internal implementation path in the UI.
Setup
- Observed on ProcessWire 3.0.262;
MarkupQA::linkWarning() is unchanged on current dev (3.0.267) and master — still protected, same message
- PHP 7.4
- Field:
FieldtypeTextarea (CKEditor) inside a RepeaterMatrix, with link abstraction enabled
Where it comes from
FieldtypeTextarea::___wakeupValue() → MarkupQA::wakeupLinks() → MarkupQA::linkWarning() in wire/core/Tools/MarkupQA.php, which builds the message and calls $this->warning():
protected function linkWarning($path, $logWarning = true) {
if($this->wire()->page->template == 'admin' && $this->wire()->process == 'ProcessPageEdit') {
$this->warning(sprintf(
$this->_('Unable to resolve link on page %1$s in field "%2$s": %3$s'),
$this->page->path,
$this->field->getLabel(),
$path
));
}
...
}
Suggested improvement
Two options, not mutually exclusive:
1. Enrich the message for repeater pages in core. When $this->page is a repeater page, core already has the context to render something the editor recognises — host page, field, and (for matrix) the item type + index. For example:
Broken link /old/moved-page/ in Text & Image item #3 → field "Body" on page About Us
2. Make MarkupQA::linkWarning() hookable (rename to ___linkWarning()), ideally passing structured context — $this->page, $this->field, and the unresolved $path — as arguments. Today the method is protected and Wire::warning() isn't hookable, so there's no clean seam to customise or suppress these warnings. The only option is to post-process rendered notice text, which is brittle because it depends on the translated message format.
Nested repeaters. The repeater path is always flat — for-page-{ownerId} is the immediate owner, which for a nested repeater is another repeater page, not the content page. Core already has RepeaterPage::getForPageRoot() / getForFieldRoot() (3.0.132+) to resolve the real root page, so enriching in core (or exposing the page/field via a hook) handles nesting correctly for free. A text-parsing workaround has to re-resolve the repeater page and call getForPageRoot() itself.
Related limitation: detection is render-time only
Worth noting because it bounds what any message-enrichment can achieve: these warnings are a side effect of MarkupQA::wakeupLinks(), which only runs when a field value is woken up. RepeaterMatrix items that load lazily/asynchronously (fetched via AJAX only when the item is opened) are not woken on initial page-edit load, so no warning is emitted for a broken link inside them until that item is opened and the page saved.
So both the raw warning and any enrichment of it cover only the items that happened to be woken during the request. Complete coverage would require a proactive scan of repeater/matrix subfields (or an eager-wakeup option), independent of render-time wakeup — which is feasible in core but not in a notice-rewriting hook.
Current workaround (fragile — illustrates why a hook is needed)
With no hook available, we rewrite the notice text after the edit form is built, parsing the repeater path back out of the message and resolving the item. This is exactly the kind of reverse-engineering a hookable linkWarning() would make unnecessary:
// site/ready.php
$wire->addHookAfter('ProcessPageEdit::execute', function (HookEvent $event) {
$sanitizer = $event->wire('sanitizer');
$pages = $event->wire('pages');
foreach ($event->wire('notices') as $notice) {
if (!($notice instanceof NoticeWarning)) continue;
$text = $notice->text;
if (strpos($text, '/repeaters/for-field-') === false) continue;
if (!preg_match('{/repeaters/for-field-\d+/for-page-\d+/([^/\s"]+)}', $text, $m)) continue;
// This is purely cosmetic, so never let it break the edit screen: any
// unexpected failure leaves the original warning untouched.
try {
// Resolve the repeater page, then use its own API so nested repeaters
// (whose for-page owner is another repeater page) attribute correctly.
$repeater = $pages->get('name=' . $sanitizer->pageName($m[1]) . ', include=all');
if (!$repeater->id || !method_exists($repeater, 'getForField')) continue;
$repeaterField = $repeater->getForField(); // the repeater/matrix field
$ownerPage = $repeater->getForPage(); // immediate owner (may be a repeater page)
$rootPage = method_exists($repeater, 'getForPageRoot') ? $repeater->getForPageRoot() : $ownerPage;
if (!$repeaterField || !$repeaterField->id || !$rootPage->id) continue;
// Recover the subfield label and broken target from the original message.
$fieldLabel = preg_match('/in field "([^"]+)"/', $text, $fm) ? $fm[1] : '';
$detail = preg_match('/in field "[^"]+":\s*(.+)$/s', $text, $dm) ? trim($dm[1]) : '';
$detail = preg_replace('/^wakeup:\s*/', '', $detail);
// Matrix type label, when this is a RepeaterMatrix item.
$typeLabel = '';
if (method_exists($repeater, 'getMatrixType') && method_exists($repeaterField, 'getMatrixTypeLabel')) {
$typeNum = (int) $repeater->getMatrixType(true);
if ($typeNum) $typeLabel = (string) $repeaterField->getMatrixTypeLabel($typeNum);
}
// Item number = position within its immediate owner's field value.
$itemNo = '';
$items = $ownerPage->get($repeaterField->name);
if ($items instanceof PageArray) {
$i = 0;
foreach ($items as $it) {
$i++;
if ($it->id === $repeater->id) { $itemNo = $i; break; }
}
}
$where = $sanitizer->entities($typeLabel !== '' ? $typeLabel : (string) $repeaterField->getLabel());
if ($itemNo !== '') $where .= " item #$itemNo";
$notice->text = sprintf(
'Broken link <code>%s</code> in <b>%s</b> → field "%s" on page <b>%s</b>',
$sanitizer->entities($detail !== '' ? $detail : '(unknown)'),
$where,
$sanitizer->entities($fieldLabel),
$sanitizer->entities($rootPage->title ?: $rootPage->path)
);
$notice->flags = $notice->flags | Notice::allowMarkup;
} catch (\Throwable $e) {
// leave the original notice as-is
}
}
});
Result (illustrative):
Broken link /old/moved-page/ in Text & Image item #3 → field "Body" on page About Us
For a plain (non-matrix) repeater there's no per-item type, so it falls back to the repeater field's own label:
Broken link /old/moved-page/ in Content Sections item #3 → field "Body" on page About Us
Note: an "edit this item" deep-link was considered but dropped — editing a RepeaterMatrix page standalone renders all matrix types' fields (per-type field visibility is applied by InputfieldRepeaterMatrix, not the page template), so there's no clean way to open just that one item.
Short description
When editing a page that contains a RepeaterMatrix (or plain Repeater) field, the MarkupQA "wakeup" of a rich-text/textarea subfield can emit a warning for a stored link that no longer resolves. The warning identifies the field's internal repeater page path rather than the content block the editor actually sees. For example (values illustrative):
For an editor this isn't actionable —
/processwire/repeaters/for-field-12/for-page-1041/1700000000-1234-1/gives no hint about which matrix item on which page holds the broken link, and it surfaces an internal implementation path in the UI.Setup
MarkupQA::linkWarning()is unchanged on currentdev(3.0.267) andmaster— stillprotected, same messageFieldtypeTextarea(CKEditor) inside aRepeaterMatrix, with link abstraction enabledWhere it comes from
FieldtypeTextarea::___wakeupValue()→MarkupQA::wakeupLinks()→MarkupQA::linkWarning()inwire/core/Tools/MarkupQA.php, which builds the message and calls$this->warning():Suggested improvement
Two options, not mutually exclusive:
1. Enrich the message for repeater pages in core. When
$this->pageis a repeater page, core already has the context to render something the editor recognises — host page, field, and (for matrix) the item type + index. For example:2. Make
MarkupQA::linkWarning()hookable (rename to___linkWarning()), ideally passing structured context —$this->page,$this->field, and the unresolved$path— as arguments. Today the method isprotectedandWire::warning()isn't hookable, so there's no clean seam to customise or suppress these warnings. The only option is to post-process rendered notice text, which is brittle because it depends on the translated message format.Nested repeaters. The repeater path is always flat —
for-page-{ownerId}is the immediate owner, which for a nested repeater is another repeater page, not the content page. Core already hasRepeaterPage::getForPageRoot()/getForFieldRoot()(3.0.132+) to resolve the real root page, so enriching in core (or exposing the page/field via a hook) handles nesting correctly for free. A text-parsing workaround has to re-resolve the repeater page and callgetForPageRoot()itself.Related limitation: detection is render-time only
Worth noting because it bounds what any message-enrichment can achieve: these warnings are a side effect of
MarkupQA::wakeupLinks(), which only runs when a field value is woken up. RepeaterMatrix items that load lazily/asynchronously (fetched via AJAX only when the item is opened) are not woken on initial page-edit load, so no warning is emitted for a broken link inside them until that item is opened and the page saved.So both the raw warning and any enrichment of it cover only the items that happened to be woken during the request. Complete coverage would require a proactive scan of repeater/matrix subfields (or an eager-wakeup option), independent of render-time wakeup — which is feasible in core but not in a notice-rewriting hook.
Current workaround (fragile — illustrates why a hook is needed)
With no hook available, we rewrite the notice text after the edit form is built, parsing the repeater path back out of the message and resolving the item. This is exactly the kind of reverse-engineering a hookable
linkWarning()would make unnecessary:Result (illustrative):
For a plain (non-matrix) repeater there's no per-item type, so it falls back to the repeater field's own label:
Note: an "edit this item" deep-link was considered but dropped — editing a RepeaterMatrix page standalone renders all matrix types' fields (per-type field visibility is applied by
InputfieldRepeaterMatrix, not the page template), so there's no clean way to open just that one item.