Page bodies
A card's body is a block document — validated on write, incapable of carrying HTML, and always readable as plain text too.
Every card has a body, and it is a document made of blocks — not a string of Markdown, not HTML.
Two columns, one rule
A card carries its body twice:
blocks— the document: an ordered list of typed blocks;body— the plain-text projection of that document, with no markers.
The rule for writing them is: never both.
Writing blocks sets the document and recomputes body from it. Writing body
sets the text and drops the document back to being derived from that text.
Reading is simpler: the API always returns both. A card written before block bodies existed has no stored document, and one is derived from its text on read — so no consumer ever has to know which régime a given card is in, and none has to parse Markdown to render content.
Block types
paragraph, heading, bulleted, numbered, todo, quote, callout,
code, divider, image.
Every block has a stable id (a UUIDv7, preserved across writes) and an
indent between 0 and 5. Then, depending on type: text for textual blocks,
level for a heading, checked for a todo, code and language for code, and
fileId plus alt for an image.
Text is spans, not markup
A textual block's text is a list of spans: a string plus its marks.
{
"type": "paragraph",
"text": [
{ "text": "See the " },
{ "text": "reference", "link": "https://cms.spunto.net/api/docs" },
{ "text": " for details. " },
{ "text": "Required.", "bold": true }
]
}The marks are bold, italic, code, strike and link. A link is an
absolute http, https or mailto URL.
There is nothing to sanitise
The format cannot express HTML. Not "HTML is stripped" — there is no place to put it. A span is a string and a set of boolean marks, and the closed list of block types is enforced when written.
That is the whole reason for the shape. A Markdown or HTML body would have made every consumer responsible for sanitising the same content, correctly, forever.
Images are identifiers
An image block carries a fileId, never a URL.
Files have no shareable address in this product at all — the bytes are served by the API from a scoped route, with the same authorisation as everything else. Each consumer composes the URL it knows how to serve. See files and attachments.
alt doubles as the caption, and it is what lands in the text projection.
Bodies are not in list results
Item — what a page of a view returns — has no body.
Only ItemDetail carries body and blocks, and only the routes that return a
single card use it. A 50-row view does not fetch 50 documents.
Neither body nor blocks can be sorted or filtered on. There is no search
index over bodies, so there is nothing honest to offer: workspace
search covers card titles and list names, and says
so.