UVE canvas iframe silently fails to repaint on traditional-page navigation (regression from PR #36583)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
In the page editor (Universal Visual Editor / UVE), when editing a traditional (VTL) page, clicking a link inside the rendered page (e.g. a nav menu item) that navigates to another page can leave the visual canvas permanently blank — no page content renders. This is not a crash: the store correctly loads the new page's data (opening the page properties panel via the gear icon shows the correct page), no JavaScript error is thrown, and a HAR capture of the failure shows only successful (200) network responses, including a full, correct response for the target page. The failure is purely in how that already-fetched content ends up (or fails to end up) in the canvas.
Root cause is now confirmed (see below) via live debugging with before/after DOM evidence — this is a timing race in DotUveIframeComponent, not a network or store issue, and it does not depend on whether the navigation target is the same page or a different one.
Steps to Reproduce
This is a timing-dependent race — it does not reproduce on every click. Two ways to trigger it:
A. Minimal repro (any traditional-page UVE session, e.g. demo.dotcms.com):
- Open any traditional (VTL) page in the UVE editor that has a working in-page nav link (e.g. a header or sidebar link that is a real
<a href="...">inside the rendered page — not the dotCMS admin's own site browser). - Click that link once — it navigates correctly.
- Click the same link again (navigating to the page you're already on).
- Expected: canvas stays showing the page correctly.
- Actual (intermittently): canvas goes blank. No console error. The gear-icon page-properties panel still shows the correct page.
B. Cross-page repro (confirmed via live customer debugging, generalized — no customer specifics):
- Open a traditional (VTL) page (Page A) in the UVE editor.
- Click a link inside the rendered page's own navigation (a real
<a href="...">element) pointing to a different page (Page B) in the same site/folder. - Expected: canvas repaints with Page B's content.
- Actual (intermittently): canvas goes blank on the first click. No console error. Page properties (gear icon) correctly show Page B is loaded.
Diagnostic to confirm the failure mode, in either case: open DevTools console before clicking:
document.querySelector('iframe').contentDocument.body.innerHTML.length
Note the value (a large number matching the full rendered page, e.g. ~124,500). Click the nav link. Run the same command again — on a failed navigation, the value drops dramatically (observed: 124,520 → 544). Then run (no .length, full output):
document.querySelector('iframe').contentDocument.body.innerHTML
On a failed navigation, the output is only InlineEditService.INLINE_CONTENT_STYLES (the [data-inode][data-field-name]... CSS block) plus a single <script data-inline="true" src="/html/js/tinymce/js/tinymce/tinymce.min.js"> tag — with zero trace of the actual page content. This exact fragment is the confirmed signature of this bug.
Confirmed Evidence (from live debugging)
document.querySelector('iframe').contentDocument.body.innerHTML.lengthdropped from 124,520 to 544 characters at the moment of a failed navigation.- The resulting 544-character body was verified to be exactly the output of
InlineEditService#addStyles()+#addScript()(inline-edit.service.ts) — i.e.INLINE_CONTENT_STYLESplus the TinyMCE inline-edit script tag — and nothing else. - A HAR capture of the failing navigation showed a full, successful
200response for the target page (159,674 bytes of real HTML) — the backend and network layer are not at fault. - Other candidate causes were investigated and ruled out:
- Not a JS exception at the moment of failure. A separate, unrelated bug was found in one customer's own page template (a broken CDN URL pointing at an ES-module build of a syntax-highlighting library, throwing
Unexpected token 'export') — but this fires on every page load, not specifically the failing navigation, and produced no console output at the actual moment of the blank-out. Confirmed distraction, not the cause. - Not a Vanity URL / URL Map issue. The affected pages are literal HTML Pages (not URL-mapped content), and the only Vanity URL rule present is unrelated to the pages involved in the reproduction.
- Not a malformed link. The triggering link is a real
<a href="...">anchor, correctly picked up by the editor's internal-navigation click handling (handleInternalNav()inedit-ema-editor.component.ts) — not a JS-driven, non-anchor element. - Not a version-specific regression. No commits touch
dot-uve-iframe.component.ts,edit-ema-editor.component.ts,withEditor.ts,utils/index.ts(isSamePageNavigation), ordot-ema-shell.component.tsbetween two dotCMS builds ~12 days apart where the bug was present on both — this is a pre-existing timing race, not something a recent release introduced or changed.
- Not a JS exception at the moment of failure. A separate, unrelated bug was found in one customer's own page template (a broken CDN URL pointing at an ES-module build of a syntax-highlighting library, throwing
Root Cause
DotUveIframeComponent.insertPageContent() (dot-uve-iframe.component.ts, ~lines 154-181) sets the new page's HTML via iframeElement.srcdoc = content — which starts an asynchronous iframe navigation — and then, in the same synchronous call, invokes handleInlineScripts(enableInlineEdit), which calls InlineEditService.injectInlineEdit(this.iframe) (inline-edit.service.ts, ~lines 93-104):
injectInlineEdit(iframe: ElementRef<HTMLIFrameElement>): void {
const doc = iframe.nativeElement.contentDocument;
...
if (doc.querySelector('script[data-inline="true"]')) {
return;
}
this.#addStyles(doc);
this.#addScript(doc, '/html/js/tinymce/js/tinymce/tinymce.min.js');
}
injectInlineEdit() reads iframe.nativeElement.contentDocument synchronously, immediately after the srcdoc assignment — before the browser has necessarily finished tearing down the old document and installing the new one. #addStyles/#addScript (~lines 207-218) both use doc.body?.appendChild(...), which only appends — neither wipes content on its own. But if contentDocument is caught in a transitional/empty state during that async navigation, the appended <style> and <script> land in an effectively empty document instead of the real page content.
onIframeLoad() (fired once the real navigation actually completes) calls insertPageContent() a second time, which would normally be the safety net — except the writeKey dedup guard added in #36583 (`${src}::${content}`, and src is always '' for traditional pages, per withEditor.ts's $iframeURL) sees the same content string as before and skips the srcdoc re-write, since from its point of view nothing changed. handleInlineScripts() still runs on this second call, but there's no check that the current document actually contains the real page content before treating the canvas as up to date — so if the first call already landed content in the wrong (stale) document, there's no recovery.
Net effect: a timing race between an async srcdoc navigation and a synchronous DOM read/write in injectInlineEdit() can permanently leave the canvas showing only the inline-edit CSS/script stub, with no error and no retry — matching every symptom observed (correct store state, correct page-properties panel, silent blank canvas, no console error). This does not depend on same-page vs. different-page navigation, which is why it was reproduced two different-looking ways (repro A and repro B above) — both are the same underlying race, just triggered by different timing conditions.
Acceptance Criteria
- Root cause race is fixed:
injectInlineEdit()(or its caller) never appends the inline-edit CSS/script to a transitional/stalecontentDocumentthat doesn't yet contain the real page content. - Clicking a link inside a traditional (VTL) page's rendered content — to the same page or a different page — always repaints the UVE canvas with the correct content. No blank canvas, no stub-only body.
- The original bug fixed by #36583 (infinite reload loop / "Identifier has already been declared" on VTL pages with top-level
let/const) remains fixed — no regression. - Add a Jest/Spectator test (
dot-uve-iframe.component.spec.tsand/orinline-edit.service.spec.ts) that simulatescontentDocumentbeing a stale/transitional (empty-body) document at the momenthandleInlineScripts()/injectInlineEdit()runs, and asserts the real page content is not permanently lost. - Manually verify using repro A (click same in-page nav link twice) and repro B (click a different in-page nav link once) on a traditional page with inline editing enabled.
dotCMS Version
Current Release (dotEvergreen). Confirmed present on two builds ~12 days apart (see "Not a version-specific regression" above) — this is a pre-existing bug, not a recent regression.
Severity
High - Major functionality broken
Links
- Freshdesk ticket #39005
- Related PR: https://github.com/dotCMS/core/pull/36583
- Related issue (original reload-loop bug): https://github.com/dotCMS/core/issues/36141
- Relevant files:
core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-iframe/dot-uve-iframe.component.ts(insertPageContent,handleInlineScripts,onIframeLoad, lines ~139-181)core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts(injectInlineEdit,#addStyles,#addScript, lines ~93-104, 207-218)core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts($iframeURL, lines 276-302)core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts(handleInternalNav, lines ~763-816)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with insertPageContent(), handleInlineScripts(), and onIframeLoad() in core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-iframe/dot-uve-iframe.component.ts, then inspect injectInlineEdit() in inline-edit.service.ts. Run the relevant Jest/Spectator tests and simulate an empty transitional contentDocument. Done means same-page and cross-page traditional navigation repaint reliably while the #36583 reload-loop fix remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100