data-* attributes.
This guide is the contract for rendering that HTML in iOS, Android, and other non-web clients.
Where this HTML appears
Locale selection follows
Accept-Language. The flat attribute is the resolved translation; *_i18n is the full map (admin only).
The same HTML shape is used in all three places. Implement one renderer and reuse it.
Storage format
Content is HTML, not TipTap JSON. Custom nodes serialize to semantic tags plusdata-* attributes so non-JS clients still have a fallback.
Allowed tags after server sanitization include h1–h3, p, br, strong, b, em, i, u, s, ul, ol, li, table, thead/tbody/tr/th/td, a, img, div, and iframe (video hosts only).
script, style, and javascript: URLs are stripped on save.
Images
Images persist as a normal<img> with extra metadata:
Native rendering
- Render
srcwith an image view. Ifsrcis root-relative (/storage/...), prefix the API origin. - Honor
data-width: treat percentages as a fraction of the content column, not the device width if you have padded margins. - Apply
data-alignas horizontal alignment of the image block. - Use
altfor VoiceOver / TalkBack. - Do not require
data-image-idto display the image. It is for lifecycle tracking on the backend.
Videos
Videos are not stored as<video> or as a live iframe. The editor persists a fallback link:
a[data-video-embed]or*[data-video-embed]a.video-embed/div.video-embediframewhosesrchost is an allowed provider (see below)
Attributes
Supported providers
Only HTTPS embed hosts from that list are accepted by the API. Reject any other iframe host.
YouTube thumbnail fallback when
data-thumbnail-url is missing:
data-thumbnail-url is present (resolved via oEmbed at edit time).
How to render videos in a native app
Match the web TipTap renderer: thumbnail + play button first, load the player only after a tap.1
Parse the node
Read
data-src / href as the watch URL, data-embed-url as the player URL, and data-thumbnail-url as the poster. If you only have an iframe, derive the watch URL from the embed src.2
Draw the poster
Show the thumbnail as a 16:9 cover image. If no thumbnail is available, use a neutral grey background (
#6B7280). Center a circular play control (dark translucent disc, white triangle) on top.3
Handle the tap
Preferred: replace the poster with an in-app player /
WKWebView / WebView loaded with data-embed-url (add autoplay=1 after the user gesture). Fallback: open the watch URL in the system browser or YouTube / Vimeo / Loom app.4
Always keep the plain URL
Under the poster, render the watch URL as a tappable text link. Signed PDFs do the same. This keeps the video reachable when embeds are blocked, offline, or inaccessible.
Layout
- Default aspect ratio: 16:9 (
data-aspect-ratiowins when present). - Width: fill the content column; do not exceed it.
- The play control should stay visually centered on the poster.
- Use
title(or “YouTube / Vimeo / Loom video”) as the accessibility label of the play control.
Do not
- Autoplay or preload the iframe on bind. That wastes bandwidth and breaks data-saving modes.
- Execute arbitrary iframe hosts. Stick to the allow-list above.
- Treat the anchor text (
Link to video) as the only UI. Replace the node with the poster + URL treatment.
Files and entity links
Rich content may also include:data-entity-model + data-entity-id (slug).
PDF rendering
Signed legal-document PDFs cannot play video. The backend transforms TipTap HTML before TCPDF:- Images are embedded (resolved from
data-image-idwhen the file is available). - Videos become a static 16:9 poster (thumbnail + play button, or grey if no thumbnail) and the plain watch URL as a clickable link under it.
Minimal detection cheat sheet
NSAttributedString + custom HTML, Android Html is not enough). Walk the DOM, swap image/video nodes for native views, and render the remaining safe tags as text.
Related
- JSON-API Conventions — locale headers and sparse fieldsets
- Authentication — tokens for fetching resources and legal documents