TipTap JSON reference
A page's body is stored as a TipTap JSON document, the format the editor itself saves. This page describes every node, mark and attribute the editor uses, with an example of each and the Markdown Folioso writes for it.
You get this format from content on a page, a recycle-bin page or a snapshot, and you can send it as content when you create or update a page. To work in Markdown instead, see Content formats.
The document
The body is one doc node. Its content is an array of the page's top-level blocks:
{
"type": "doc",
"content": [
{ "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Install" }] },
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Run " },
{ "type": "text", "text": "composer install", "marks": [{ "type": "code" }] },
{ "type": "text", "text": " first." }
]
}
]
}## Install
Run `composer install` first.A page with no body has content of null, not a document. A document with an empty content array is a page with no blocks.
Nodes
Every node is a JSON object:
| Key | Present on | Description |
|---|---|---|
type |
Every node | The node's type, such as paragraph. |
attrs |
Nodes with attributes | An object of attribute values. An attribute left out takes its default, so {"type":"heading","attrs":{"level":2}} has textAlign null. |
content |
Nodes that hold other nodes | An array of child nodes. Left out, it's the same as []. |
text |
text nodes |
The text. |
marks |
text, mention and pageLink nodes |
An array of marks, such as bold or a link. |
Folioso keeps whatever JSON you send in content, without checking it against the rules below. The editor does apply them when it opens the page, so a node it doesn't know, or one in a place it can't go, may be dropped or moved when the page is next edited. Follow the rules on this page, and when in doubt, make the content in the editor and read it back with GET /pages/{slug}.
What can go where
| Node | Kind | Content |
|---|---|---|
doc |
The document | Blocks and alert boxes, at least one. The editor opens a doc with "content": [] as one empty paragraph. |
paragraph, heading |
Block | Inline nodes. |
blockquote |
Block | One or more blocks. |
bulletList, orderedList |
Block | One or more listItem. |
listItem |
Inside a list | A paragraph, then any blocks. |
taskList |
Block | One or more taskItem. |
taskItem |
Inside a task list | A paragraph, then any blocks. |
codeBlock |
Block | text nodes without marks. |
mermaid |
Block | Nothing. The source is an attribute. |
horizontalRule, image |
Block | Nothing. |
table |
Block | One or more tableRow. |
tableRow |
Inside a table | tableHeader and tableCell nodes. |
tableHeader, tableCell |
Inside a row | One or more blocks. |
alertBox |
Top level only | An alertTitle, then an alertBody. |
alertTitle |
Inside an alert box | Inline nodes. |
alertBody |
Inside an alert box | One or more paragraph. |
text, hardBreak, mention, pageLink |
Inline | Nothing. |
"Blocks" means every block node except alertBox: an alert box can only sit at the top level of the page.
Block nodes
paragraph
A paragraph of text.
| Attribute | Type | Default | Description |
|---|---|---|---|
textAlign |
string or null | null |
"center" or "right". null is left-aligned. |
{ "type": "paragraph", "attrs": { "textAlign": "center" }, "content": [{ "type": "text", "text": "Centred text." }] }[](folioso:attrs?textAlign=center)Centred text.An empty paragraph, {"type":"paragraph"}, is an empty line on the page.
heading
A heading.
| Attribute | Type | Default | Description |
|---|---|---|---|
level |
integer | 1 |
1 to 6. |
textAlign |
string or null | null |
As for paragraph. |
{ "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "Rollback" }] }### Rollbackblockquote
A quote. It has no attributes and holds blocks.
{ "type": "blockquote", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Measure twice." }] }] }> Measure twice.bulletList and listItem
A bulleted list. Neither node has attributes. Each listItem starts with a paragraph and can hold further blocks, including nested lists.
{
"type": "bulletList",
"content": [
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Redis" }] }] },
{
"type": "listItem",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "Postgres" }] },
{
"type": "bulletList",
"content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Version 18" }] }] }]
}
]
}
]
}- Redis
- Postgres
- Version 18orderedList
A numbered list of listItem nodes.
| Attribute | Type | Default | Description |
|---|---|---|---|
start |
integer | 1 |
The first item's number. |
type |
string or null | null |
The numbering style, as HTML's type: "a", "A", "i" or "I". null numbers with digits. |
{
"type": "orderedList",
"attrs": { "start": 1 },
"content": [
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Build" }] }] },
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Ship" }] }] }
]
}1. Build
2. ShipMarkdown has no syntax for type, so a list with one is written as a JSON block.
taskList and taskItem
A checklist. Each taskItem starts with a paragraph and can hold further blocks, including nested task lists.
| Attribute | On | Type | Default | Description |
|---|---|---|---|---|
checked |
taskItem |
boolean | false |
Whether the box is ticked. |
{
"type": "taskList",
"content": [
{ "type": "taskItem", "attrs": { "checked": true }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Tests pass" }] }] },
{ "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Changelog" }] }] }
]
}- [x] Tests pass
- [ ] ChangelogcodeBlock
A block of code, holding plain text nodes with no marks. Line breaks are \n characters in the text.
| Attribute | Type | Default | Description |
|---|---|---|---|
language |
string or null | Set by each reader | The language slug, such as "php" or "bash". null is plain text. |
The editor's language menu uses typescript, javascript, php, python, go, rust, bash, sql, html, css, yaml, json and dockerfile, with null for Plain text. Any other string is kept and shown by name. See Code blocks.
language has no fixed default. A block with no language key at all is shown in each reader's own preferred language, which is why the editor always saves language explicitly. Send it explicitly too.
{ "type": "codeBlock", "attrs": { "language": "php" }, "content": [{ "type": "text", "text": "echo 'Hello';\necho 'World';" }] }```php
echo 'Hello';
echo 'World';
```An empty code block has no content.
mermaid
A diagram, drawn from its source. The type is named mermaid for both engines.
| Attribute | Type | Default | Description |
|---|---|---|---|
code |
string | "" |
The diagram's source. |
engine |
string | "mermaid" |
"mermaid" or "plantuml". PlantUML source includes its @startuml and @enduml lines. |
{ "type": "mermaid", "attrs": { "code": "@startuml\nAlice -> Bob: Hello\n@enduml", "engine": "plantuml" } }```plantuml
@startuml
Alice -> Bob: Hello
@enduml
```See Diagrams.
horizontalRule
A horizontal line. No attributes or content.
{ "type": "horizontalRule" }---image
An image, always a block of its own.
| Attribute | Type | Default | Description |
|---|---|---|---|
src |
string | null |
The image's address. |
alt |
string or null | null |
The alt text, shown as the caption. |
title |
string or null | null |
The image's title. |
width |
integer or null | null |
The width in pixels, set by the resize handles. null is the image's natural width, up to the page width. |
height |
integer or null | null |
The height in pixels. The editor leaves it null and keeps the image's proportions. |
textAlign |
string or null | null |
"center" or "right" places the image. |
{
"type": "image",
"attrs": {
"src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp",
"alt": "Request flow",
"title": null,
"width": 640,
"height": null,
"textAlign": null
}
}[](folioso:attrs?width=640)To use an image you upload through the API, put the url the upload returns in src, exactly as returned. A page write that names it keeps the upload. See Images.
An image whose address no longer works shows Image unavailable in the editor.
table, tableRow, tableHeader and tableCell
A table is made of rows, and each row of header and ordinary cells. table and tableRow have no attributes. A new table in the editor has a header row, and Toggle header row and Toggle header column change which cells are tableHeader.
| Attribute | On | Type | Default | Description |
|---|---|---|---|---|
colspan |
cells | integer | 1 |
How many columns the cell spans. |
rowspan |
cells | integer | 1 |
How many rows the cell spans. |
colwidth |
cells | array or null | null |
The column widths in pixels, one per column the cell spans, such as [240]. null lets the column size itself. |
align |
cells | string or null | null |
A cell alignment the editor doesn't set. Align a cell's paragraph with its textAlign instead. |
Cells hold blocks, so a cell's text is a paragraph:
{
"type": "table",
"content": [
{
"type": "tableRow",
"content": [
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Queue" }] }] },
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Latency" }] }] }
]
},
{
"type": "tableRow",
"content": [
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "mail" }] }] },
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "seconds" }] }] }
]
}
]
}| Queue | Latency |
| --- | --- |
| mail | seconds |When cells span, a row has fewer cells. A table with spans, with cells holding more than one paragraph or other blocks, with no header row, or with header cells outside the first row has no pipe-table form, so it's written as a JSON block:
{
"type": "table",
"content": [
{
"type": "tableRow",
"content": [{ "type": "tableHeader", "attrs": { "colspan": 2 }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Merged" }] }] }]
},
{
"type": "tableRow",
"content": [
{ "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "a" }] }] },
{ "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "b" }] }] }
]
}
]
}```folioso
{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":2},"content":[{"type":"paragraph","content":[{"type":"text","text":"Merged"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"a"}]}]},{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"b"}]}]}]}]}
```alertBox, alertTitle and alertBody
An alert box: a titled, coloured panel. It's made of exactly one alertTitle, a line of inline content that can carry marks, then one alertBody of paragraphs. Neither part has attributes. Alert boxes can only be top-level blocks, and their paragraphs can't be aligned.
| Attribute | On | Type | Default | Description |
|---|---|---|---|---|
type |
alertBox |
string | "info" |
"info", "success", "warning" or "error". |
{
"type": "alertBox",
"attrs": { "type": "success" },
"content": [
{ "type": "alertTitle", "content": [{ "type": "text", "text": "Deployed" }] },
{ "type": "alertBody", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Version 4.2 is live." }] }] }
]
}> [!TIP] Deployed
>
> Version 4.2 is live.An empty title is {"type":"alertTitle"}. The body always holds at least one paragraph, which can be empty.
type |
Shown as | Written in Markdown as |
|---|---|---|
info |
Info | [!NOTE] |
success |
Success | [!TIP] |
warning |
Warning | [!WARNING] |
error |
Error | [!CAUTION] |
Inline nodes
Inline nodes sit inside paragraphs, headings and alert titles.
text
A run of text. text must be a non-empty string: the editor refuses a document holding an empty text node. Neighbouring text with the same marks can be one node or several; both are the same.
{ "type": "text", "text": "Hello" }hardBreak
A line break within a block. No attributes.
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Line one" }, { "type": "hardBreak" }, { "type": "text", "text": "Line two" }]
}Line one\
Line twomention
A mention of a person, shown as @ and their name. Saving a page with a new mention notifies the person, following the rules in Page links and mentions.
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
number or string | null |
The person's user id. The editor stores it as a number. |
label |
string or null | null |
The name shown. |
mentionSuggestionChar |
string | "@" |
The character the mention is shown with. |
taggedBy |
number or null | null |
The user id of the person who added the mention. The editor sets it. |
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Thanks " },
{ "type": "mention", "attrs": { "id": 5, "label": "Ada Lovelace", "mentionSuggestionChar": "@", "taggedBy": 7 } },
{ "type": "text", "text": "!" }
]
}Thanks [@Ada Lovelace](folioso:mention?id=5&taggedBy=7)\!Mentions aren't included in search.
pageLink
A link to another Folioso page, shown with the target page's current title. Pages that link to a page are listed in its Linked from.
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
number | null |
The target page's numeric id. |
slug |
string | null |
The target page's slug, which the editor uses to open it. |
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "See " },
{ "type": "pageLink", "attrs": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b" } },
{ "type": "text", "text": "." }
]
}See [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b).- The title isn't stored. Each reader sees the title of a page they can open, and Restricted page for one they can't, so a page link never reveals a title to someone who can't see it. The Markdown label works the same way.
- Links to a page that doesn't exist are kept in the content but don't count in Linked from.
- Page links aren't included in search.
Marks
Marks style the text they're on. A node's marks array lists each mark once, and Folioso keeps them in this order: link, bold, code, italic, strike, underline.
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "bold", "marks": [{ "type": "bold" }] },
{ "type": "text", "text": " " },
{ "type": "text", "text": "italic", "marks": [{ "type": "italic" }] },
{ "type": "text", "text": " " },
{ "type": "text", "text": "struck", "marks": [{ "type": "strike" }] },
{ "type": "text", "text": " " },
{ "type": "text", "text": "underlined", "marks": [{ "type": "underline" }] },
{ "type": "text", "text": " " },
{ "type": "text", "text": "code", "marks": [{ "type": "code" }] }
]
}**bold** *italic* ~~struck~~ [underlined](folioso:span?marks=underline) `code`| Mark | Attributes | Description |
|---|---|---|
bold |
None | Bold. |
italic |
None | Italic. |
strike |
None | Strikethrough. |
underline |
None | Underline. |
code |
None | Inline code. It can't be combined with any other mark. |
link |
See below | A link to an address. |
Marks combine, so [{"type":"bold"},{"type":"italic"}] is bold italic, written ***both*** in Markdown.
link
A link on a run of text.
| Attribute | Type | Default | Description |
|---|---|---|---|
href |
string | null |
The address. |
target |
string or null | "_blank" |
Where the link opens. The editor always uses "_blank", a new tab. |
rel |
string or null | "noopener noreferrer" |
The link relationship. The editor always uses "noopener noreferrer". |
class |
string or null | null |
Not used by the editor. |
title |
string or null | null |
The link's title. |
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Read the " },
{
"type": "text",
"text": "guide",
"marks": [
{ "type": "link", "attrs": { "href": "https://example.com/guide", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null } },
{ "type": "bold" }
]
},
{ "type": "text", "text": "." }
]
}Read the [**guide**](<https://example.com/guide>).href is stored as given in JSON, whatever its scheme. A link with target, rel or class other than the defaults has no Markdown form and is written as a JSON block. To link to another Folioso page, use a pageLink rather than a link to its address.