Rich text
Every rich text field (a product's text, an entry's teaser, the texts a body's blocks hold, a quiz's text) comes back as HTML in one closed format, stored as validated when it was written, so a client can render it as it is or map each element to its own.
Allowed
- Text:
p,h1(Title),h2(Heading),h3(Subheading),br. - Marks:
strong,em,u,s,sup,sub,code, read also asbasstrong,iasem,delass,strikeass; links (a) to http, https, mailto, tel, ftp, ftps, callto, sms, cid, xmpp, irc, ircs or a relative path, optionallytarget="_blank"(reltakes noopener, noreferrer, nofollow and is never stored). - Style:
colorandbackground-coloron aspan, as#rgbor#rrggbb;text-align(left, center, right, justify) on a paragraph or heading. - Structure:
ul,ol(start),li(data-checked="true"or"false"for a task list), tables (table,thead,tbody,tr,th,td, spanning at most 1000 columns or rows), code blocks (preholding onecode,class="language-<id>"with an id among bash, c, cpp, csharp, css, go, xml, java, javascript, json, kotlin, markdown, php, python, ruby, rust, sql, swift, typescript, yaml),blockquote,hr,details(open) with itssummaryfirst. - Figures: a
figureholds any of the above, with at most onefigcaption, first or last. No figure inside a figure. - Blocks: images, videos, downloads and the other blocks are
<block type="…" props="…"></block>elements, their props a JSON object with its quotes escaped. A field's description names the types it takes. There is no rawimg,videooriframe.
Attributes, by tag: p (style), h1 (style), h2 (style), h3 (style), span (style), a (href, target, rel), ol (start), li (data-checked), th (colspan, rowspan), td (colspan, rowspan), code (class), details (open), block (type, props). Styles, by tag: p (text-align), h1 (text-align), h2 (text-align), h3 (text-align), span (color, background-color). Every other tag, attribute, style property or value is refused, as are comments, a link or a block inside a link, and a misplaced summary or figcaption.
Limits
A text holds at most 1 MiB of UTF-8, before and after it is stored, within its field's own limit (in its schema), and its markup nests at most 32 elements deep, counting the empty paragraph an empty container is stored with. A table whose rows are so uneven that evening them out would outgrow the text, or whose merged cells take too long to lay out, is refused, and a text's tables cover at most 10000 cells in all: each table counts its rows times its widest row, a row counting the columns cells from earlier rows span into it.
Reads
A read answers the stored canonical form, wrapped in <rich-text version="1">: attributes in a fixed order, entities and spacing normalized, loose inline content in a paragraph, an empty list item, cell, quote, details or figure holding an empty paragraph, an empty list holding one empty item, and a table made rectangular (a short row padded with empty cells, a column span cut where it runs into a row span, a table with no cell dropped). Sending it back unchanged keeps the text as it is.
Example
<h1>Welcome</h1><p style="text-align: center">Start with <strong>the intro</strong>, then <a href="https://example.com/notes" target="_blank">read the notes</a>.</p><ul><li data-checked="true"><p>Watch the intro</p></li><li data-checked="false"><p>Take the <span style="color: #047857">quiz</span></p></li></ul><details><summary>Why it works</summary><p>Short lessons stick.</p></details>
A block, in a field that takes images:
<block type="image" props="{"id":"cover","fileId":"FILE_ID","alt":"The cover"}"></block>