dotCMS / dotCMS/core

Adjacent text nodes sharing one link mark render as two anchors in VTL and the SDKs

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

Nobody has claimed this yet.

dotCMS: Block Editor 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

Any stored Story Block JSON containing adjacent text nodes that carry an identical link mark renders as two separate <a> elements — in the VTL macro and in all three SDK TextBlock components.

The consequences are accessibility failures, reported by a customer as Urgent:

  • two tab stops where the author created one link
  • two entries in the NVDA elements list
  • WCAG 2.2 A failures: 1.3.1, 2.4.4, 4.1.2
Why this is still open after #37340

#37340 declared two defects and stated: "Both must be fixed — they are separate defects that happen to surface together here." Only one was. PR #37442 states its own scope plainly:

"Jest only: no Java, DB, REST, renderer or build artifact is touched."

It solved the emoji-driven split by healing the data in the editor — merging the text nodes on load. That closes the path for content authored from now on through the new editor. It does nothing for the delivery gap:

  • VTL emits one <a> per text node (VM_global_library.vm:35); the three SDK TextBlock components do the same
  • That shape reaches storage from migrations, imports, the legacy editor, the REST API and any third-party writer — none of which pass through the new editor's heal
  • So every already-published document with this shape still renders two anchors today
Why it cannot be fixed inside #renderMarks

The macro receives one text node at a time. It is called from #renderContentBlock at dotCMS/src/main/webapp/WEB-INF/velocity/static/storyblock/render.vtl:97, inside the per-element loop, and that is its only call site in the codebase.

Coalescing adjacent nodes that share a link mark therefore requires a look-ahead in the caller, not a change in renderMarks. This is why this issue's footprint is the caller plus the three SDK TextBlock components, and why it is separate from the mark-coverage work (Defect D) even though it touches neighbouring code in the same .vm file. Keeping them apart keeps both the review and the revert legible.

Steps to Reproduce

Reported verbatim by the customer in FD #39197:

  1. Open a content type with a Story Block field (customer used COM BLOG PAGE).
  2. Add some text in the Block Editor.
  3. Select the text and convert it into a hyperlink.
  4. Edit the link and insert a special character such as ® somewhere in between the linked text.
  5. Save the content.
  6. Render the page on the front end (customer renders via the SDK).

Equivalently, and independent of the editor: store JSON with two adjacent text nodes sharing one link mark and render it through VTL or any SDK renderer.

Expected: one <a> element; one tab stop; one entry in the screen-reader elements list.

Actual: two <a> elements with identical href, two tab stops, two screen-reader entries.

Acceptance Criteria

  • Adjacent text nodes sharing a link mark with identical attributes (href, target, rel, and any other link attrs) render inside a single <a> element
  • Adjacent text nodes carrying link marks with differing attributes still render as separate anchors — two links to different URLs must never be merged
  • A run of three or more such adjacent nodes collapses into one anchor
  • Any non-text node between them (hardBreak, dotImage, an inline atom) ends the run — nodes on either side are not merged across it
  • Scope is link only. Runs sharing other marks (bold, italic, …) are left exactly as they render today — this keeps the change off the vast majority of published HTML while fixing the case with an accessibility consequence
  • Marks nested inside the link (e.g. [link, bold] then [link]) are preserved per text node; merging the anchor must not flatten or drop inner marks
  • Implemented as a look-ahead in the caller (render.vtl #renderContentBlock), not inside #renderMarks
  • The same coalescing is applied in the React, Vue and Angular TextBlock components so all four delivery paths agree
  • VTL output remains balanced HTML in every case above (validator or DOM parse)
  • Unit tests per renderer covering: two-node run, three-node run, differing-attrs pair, run interrupted by a hardBreak, and a run with mixed inner marks
  • Verified against the customer's actual shape from FD #39197 (© / ® / between linked words), rendered through the SDK

dotCMS Version

main @ 667fc831ee (2026-09-17). Affects every consumer of the Angular / React / Vue SDK renderers and the VTL macro, regardless of FEATURE_FLAG_NEW_BLOCK_EDITOR.

Severity

High - Major functionality broken

Links

  • Freshdesk ticket #39197 — Lennox Commercial, reported Urgent. The customer renders via the SDK and reported the failure on the published website, so #37340 / PR #37442 (editor-side heal) does not resolve their case — this issue does.
  • Parent / umbrella: https://github.com/dotCMS/core/issues/37601 (Defect D2)
  • Origin: #37340 (Gap B, declared in scope and not delivered), PR #37442 (editor-side heal; scope explicitly excluded renderers)

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 at dotCMS/src/main/webapp/WEB-INF/velocity/static/storyblock/render.vtl and its #renderContentBlock caller, then trace the React, Vue, and Angular TextBlock components and their renderer tests. Run the relevant renderer unit tests before and after the change. Done means adjacent text nodes with identical link attributes produce one balanced anchor across all four renderers, while differing attributes, interruptions, and inner marks remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, java, react
Domain
accessibility, frontend
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.