Workflow fire: convert Block Editor HTML to ProseMirror JSON on save (server-side)
@hassandotcms is already working on this.
Since Jul 8, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
#36002 (PR #36253) made the content save path convert a Story Block (Block Editor) field value supplied as Markdown to Tiptap/ProseMirror JSON server-side. HTML was deliberately deferred: a value starting with < still passes through and is stored exactly as sent, so the conversion only happens later, client-side, when a human opens the contentlet in the editor and re-saves it.
This is the same problem #36002 fixed for Markdown, still open for HTML: non-interactive clients (AI agents via the MCP server, headless imports, migrations) that send HTML get content that looks broken to any consumer reading the field as ProseMirror JSON (GraphQL, REST, SDK renderers) until a person manually re-saves each contentlet. This issue completes #36002's deferred HTML acceptance criterion.
What we want
On the same save path, when a Story Block field's incoming value is HTML (the <-prefixed branch of the existing detection ladder), the backend converts it to a Tiptap/ProseMirror JSON object server-side — with sanitization — and stores that, so no human editor round-trip is required and no unsafe markup is persisted.
Why this is now feasible
#36002 shipped everything except the converter itself: the shared ingestion seam (MapToContentletPopulator), the JSON/HTML/Markdown detection ladder with the HTML branch reserved as passthrough, the isTiptapDoc no-double-convert guard, the rich-content overwrite guard, and the test scaffold. This issue is a localized swap: a new pure TiptapHtml converter (jsoup DOM walk in com.dotcms.tiptap, mirroring TiptapMarkdown's structure, restricted to the same primitive node whitelist) replacing the passthrough. jsoup is already on the classpath — no new dependencies.
Alternative approaches were evaluated and rejected (Node sidecar running @tiptap/html, Atlassian's prosemirror-kotlin, GraalJS embedding) — a pure-Java subset converter is the pragmatic fit for our runtime and the planned rich-block roadmap.
Depends on #36002 (merged).
Acceptance Criteria
- A Story Block field value supplied as HTML through the fire endpoints (and the content REST API sharing the populator) is converted to a valid Tiptap/ProseMirror JSON document and stored as such.
- Output is restricted to the same primitive whitelist as the Markdown leg (headings, paragraphs, lists, blockquote, code, tables, hr/br, images, and markdown-expressible marks); unknown/unsupported tags degrade gracefully and never throw.
- Sanitization:
script/style/iframe/object/embedsubtrees are dropped entirely;href/srcvalues pass a protocol allow-list (javascript:/data:rejected); no event-handler attributes survive. No executable content reaches stored JSON. - A value that is already valid Tiptap/ProseMirror JSON is still detected and stored unchanged (reuses
isTiptapDoc). - The rich-content overwrite guard from #36002 still applies to HTML input.
- After firing with HTML, the field reads back as structured ProseMirror JSON via GraphQL/REST/SDK without any human opening and re-saving the contentlet.
- Covered by tests: HTML→stored-JSON through the fire endpoint, a security/sanitization matrix, and an editor round-trip check (server-converted JSON opens and re-saves in the Block Editor unchanged).
Priority
Medium
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.
Assessment
This issue has not been assessed yet.