dotCMS / dotCMS/core

Block Editor: a text-first HTML value silently drops dotcms-* rich nodes on save

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

Nobody has claimed this yet.

dotCMS : Rest API OKR : Customer Support Team : Modernization Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

The Block Editor save path decides between the HTML and the Markdown converter purely on how the submitted value starts: MapToContentletPopulator.HTML_START is anchored (^<(!|/?[a-zA-Z][a-zA-Z0-9]*[\s/>])), and #36841's carve-out (TiptapHtml.startsWithDotcmsElement) matches only a value opening with a known dotcms-* element.

So a value that begins with plain text and carries a dotcms-* element later is routed to the Markdown converter, which does not understand raw HTML elements — the element is dropped and the embedded contentlet, video, YouTube embed or grid is lost with no advisory message. The same content starting with any HTML tag stores correctly.

Intro text <dotcms-content identifier="…" language-id="1"></dotcms-content>
  → {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Intro text "}]}]}   ← block gone

<p>Intro text</p><dotcms-content identifier="…" language-id="1"></dotcms-content>
  → paragraph + dotContent                                                                              ← correct

This is not a regression from #36841 — the anchored routing predates it (#36470) and that PR narrowed the gap rather than opening it. But the vocabulary it added makes the gap reachable in a new way: clients are now told they can express embedded contentlets in HTML, and an LLM- or template-generated value that opens with a sentence silently drops them. Nothing in the response distinguishes this from a successful save.

Found while building an end-to-end REST validation of #36659 / PR #36841 (that fix's own acceptance criteria all hold — 85/85 assertions pass).

Suggested direction — matching <dotcms- anywhere in the value is the wrong fix: it would misroute a genuine Markdown document that merely mentions the tag inside a code block. Safer options, in increasing order of effort:

  1. Document it: one line in BLOCK_EDITOR_FIELD_NOTE (and the MCP guidance in core-web/apps/mcp-server/src/tools/execute.ts) saying an HTML value must start with a tag.
  2. Warn: when a value routed to Markdown contains a literal <dotcms-*> element, emit an advisory through the same messages channel the overwrite guard uses — the author learns the block was dropped instead of discovering it in the editor.
  3. Handle it: let the Markdown leg parse a recognized dotcms-* element the way it parses a fence.

Steps to Reproduce

  1. Create a content type with a Block Editor field (body) and note the identifier of any existing contentlet as <REF_ID>.

  2. Fire a value that starts with plain text and contains the element:

    curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
      -X PUT "$URL/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR" \
      -d '{"contentlet":{"contentType":"myType","title":"t",
           "body":"Intro text <dotcms-content identifier=\"<REF_ID>\" language-id=\"1\"></dotcms-content>"}}'
    
  3. Read the field back (GET /api/content/id/<id>body_raw): a single paragraph with the text. No dotContent node, and messages is empty.

  4. Repeat with "body":"<p>Intro text</p><dotcms-content identifier=\"<REF_ID>\" language-id=\"1\"></dotcms-content>" → paragraph plus the dotContent node, as expected.

Expected: the rich node is stored, or the caller is told it was dropped.
Actual: it is dropped silently; the save reports success with no message.

Acceptance Criteria

  • A Block Editor value carrying a valid dotcms-* element that is not at the start of the value either stores the rich node, or returns an advisory in the response messages naming the dropped element(s).
  • Routing for values that do not contain a dotcms-* element is unchanged — no Markdown document starts taking the HTML route.
  • A Markdown document that mentions <dotcms-content> inside a fenced code block still stores it as literal code text (no misroute, no false advisory).
  • The dotcms-* element documentation (WorkflowResource.BLOCK_EDITOR_FIELD_NOTE, the generated openapi.yaml, and the MCP execute tool guidance) states the positional requirement or the new behavior, whichever ships.
  • Status codes are unchanged: advisory only, never a rejection.
  • Covered by a test pinning both shapes — text-first and tag-first — so the behavior cannot drift silently.

dotCMS Version

main — reproduced on a build from 2026-09-10 (1.0.0-SNAPSHOT), with the dotcms-* HTML vocabulary from PR #36841 present.

Severity

Medium - Some functionality impacted

Links

  • Related: #36659, PR #36841 (dotcms-* vocabulary on HTML input — the routing carve-out)
  • Related: #36658, PR #36709 (Markdown fence vocabulary)
  • Related: #36470, PR #36473 (the anchored HTML/Markdown routing this inherits)
  • Freshdesk: NA

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 by tracing the Block Editor routing through MapToContentletPopulator.HTML_START and TiptapHtml.startsWithDotcmsElement, then reproduce the text-first and tag-first values described here. Review WorkflowResource.BLOCK_EDITOR_FIELD_NOTE, generated openapi.yaml, and core-web/apps/mcp-server/src/tools/execute.ts before choosing whether to document, warn, or handle the dropped node. Done means the acceptance criteria hold, including unchanged Markdown fence behavior, advisory status codes, and coverage for both input shapes.

Written by the indexing model from the issue text.

Assessment

Tech stack
html, java, markdown, typescript
Domain
api, backend, documentation, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.