dotCMS / dotCMS/core

UVE canvas iframe silently fails to repaint on traditional-page navigation (regression from PR #36583)

Open
#37,327 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OKR : Customer Support Team : Maintenance Type : Defect
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):

  1. 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).
  2. Click that link once — it navigates correctly.
  3. Click the same link again (navigating to the page you're already on).
  4. Expected: canvas stays showing the page correctly.
  5. 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):

  1. Open a traditional (VTL) page (Page A) in the UVE editor.
  2. 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.
  3. Expected: canvas repaints with Page B's content.
  4. 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.length dropped 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_STYLES plus the TinyMCE inline-edit script tag — and nothing else.
  • A HAR capture of the failing navigation showed a full, successful 200 response 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() in edit-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), or dot-ema-shell.component.ts between 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.

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/stale contentDocument that 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.ts and/or inline-edit.service.spec.ts) that simulates contentDocument being a stale/transitional (empty-body) document at the moment handleInlineScripts()/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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.