Content formats
A page's body can be read and written in two formats: TipTap JSON, the document format the editor stores, and Markdown, in a dialect that reads like ordinary Markdown but carries everything the editor can store. This page covers choosing a format, the rules for empty bodies, how content writes affect people editing the page, and exactly what the Markdown round trip guarantees.
The two formats
| Format | Field | What it is |
|---|---|---|
| TipTap JSON | content |
The page's document exactly as stored: a doc node holding block nodes, text and marks. See TipTap JSON reference. |
| Markdown | markdown |
A string of CommonMark with GitHub tables, task lists and strikethrough, plus a few Folioso extensions for what plain Markdown can't express. See Writing Markdown and Folioso Markdown extensions. |
The page is always stored as TipTap JSON. Markdown you send is converted to TipTap JSON on the way in, and Markdown you read is written from it on the way out.
Reading Markdown
Add ?format=markdown to read a body as Markdown. ?format=json is the default and can be left out.
| Endpoint | Body returned |
|---|---|
GET /pages/{slug} |
The page's body. |
POST /pages and PATCH /pages/{slug} |
The body of the page you created or updated. |
GET /recycle-bin/{slug} |
The body of a page in your recycle bin. |
GET /pages/{slug}/snapshots/{id} |
The body of a snapshot. |
With ?format=markdown, the response has a markdown string in place of content. Everything else stays the same, including contentEpoch on pages:
curl "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b?format=markdown" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"{
"data": {
"slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
"title": "Deploying the API",
"markdown": "## Install\n\nRun `composer install` first.",
"contentEpoch": 3
}
}The other fields are left out above. A page with no body at all answers "markdown": null.
Any other format value is a 422 on format. A page you can't reach is still a 404, and one you can't view a 403, whatever format says.
The format on a POST or PATCH only chooses the format of the response. You can send Markdown and get JSON back, or the other way round. It's normally a query parameter, but a format field in the body works too.
When Markdown isn't available
Folioso never answers with Markdown that wouldn't read back to the stored page (see The Markdown round trip). Almost every page can be written that way, if need be with JSON blocks. A page whose stored JSON isn't a well-formed document can't, for example one written through the API with an extra key beside type and content, a top-level type other than doc, or a content that isn't a list.
-
Reads (
GET /pages/{slug},GET /recycle-bin/{slug}andGET /pages/{slug}/snapshots/{id}) with?format=markdownanswer422onformat:json{ "message": "This page can't be represented as Markdown without loss; read it with format=json.", "errors": { "format": ["This page can't be represented as Markdown without loss; read it with format=json."] } } -
Writes (
POST /pagesandPATCH /pages/{slug}) with?format=markdownhave already saved the page by the time the response is written, so they don't answer422. They succeed as usual and answer in the JSON shape instead:contentandcontentEpoch, with nomarkdownkey. Check formarkdownbefore reading it.
The same page can always be read with format=json.
Writing a body
Send the body in one of two fields on POST /pages or PATCH /pages/{slug}:
content: a TipTap JSON document, ornull.markdown: a Markdown string, ornull.
Sending both is refused with a 422, whatever their values, even "content": null alongside "markdown": "":
{
"message": "Send the page body as content or as markdown, not both.",
"errors": {
"markdown": ["Send the page body as content or as markdown, not both."]
}
}A markdown value that isn't a string or null is a 422 too.
Empty and blank bodies
| You send | The page stores | GET returns as JSON |
GET returns as Markdown |
|---|---|---|---|
Neither field, on PATCH |
The body is unchanged. | ||
Neither field, on POST |
No body. | "content": null |
"markdown": null |
"content": null |
No body. | "content": null |
"markdown": null |
"markdown": null or "markdown": "" |
No body, exactly as "content": null. |
"content": null |
"markdown": null |
| Markdown that's only spaces or blank lines | One empty paragraph. | {"type":"doc","content":[{"type":"paragraph"}]} |
[](folioso:attrs) |
"markdown": "[](folioso:empty)" |
A document with no blocks. | {"type":"doc","content":[]} |
[](folioso:empty) |
Warning
On a PATCH, leaving both fields out keeps the body, but "markdown": "" empties the page, just as "content": null does.
Markdown isn't trimmed. Spaces, non-breaking spaces and zero-width characters at its start and end are kept, so a body read as Markdown and sent back loses nothing at its edges. The title is still trimmed.
When Markdown is refused
A folioso: extension written wrongly, or a ```folioso block that doesn't hold valid JSON, is a 422 on markdown, with a message saying what's wrong:
{
"message": "A folioso:page link takes an id and an optional slug, and nothing else.",
"errors": {
"markdown": ["A folioso:page link takes an id and an optional slug, and nothing else."]
}
}Nothing is changed when this happens, and a POST doesn't create the page. Ordinary Markdown is never refused: anything that isn't a Folioso extension is read as Markdown, however unusual. Folioso Markdown extensions lists every message.
Content writes and people editing the page
A body sent as Markdown is stored exactly as if you'd sent the resulting JSON as content. Everything that follows from a content write applies to both:
PATCH: if the body you send differs from the stored one, the page gets a newcontentEpoch, which the response returns, and anyone with the page open in the editor is reloaded with your content. Sending back the body exactly as stored changes neither. Send thecontentEpochyou last read asepochto be refused with409 Conflictif the page was restored or replaced since. See Avoid overwriting a restore.POST: a new page starts atcontentEpoch0. There's nothing to conflict with, soepochisn't used.- Both: people mentioned in the body are notified, page links update the target's Linked from list, and the page is re-indexed for search, as when you edit in the app.
The Markdown round trip
Markdown read with ?format=markdown and sent back unchanged as markdown stores the same document. Page links keep their target, mentions keep the person, and alert boxes, image sizes, alignment, table column widths, code languages, diagrams, underline and every other attribute survive. You can read a page as Markdown, change part of it, and write it back without losing anything you didn't change.
What "the same document" means
The stored JSON is the same document the editor would load. It may differ from the original only in details the editor itself evens out every time it opens a page:
- Default attributes. An attribute at its default value may be left out, or filled in.
{"type":"paragraph"}and{"type":"paragraph","attrs":{"textAlign":null}}are the same paragraph. TipTap JSON reference lists every default. - Split text. Neighbouring text nodes with the same marks may be joined into one.
- Mark order. Marks on a text node are kept in a fixed order:
link,bold,code,italic,strike,underline. - Missing content. A node with no
contentkey is the same as one with"content": [].
One attribute is never filled in: a code block's language. Its default is each reader's own preferred language, so a code block with no language key at all is kept that way, and stays different from one whose language is null.
Values keep their JSON types. A mention whose id is the number 5 and one whose id is the string "5" stay different.
How Folioso keeps it exact
Folioso writes each top-level block as ordinary Markdown wherever that reads back exactly, using a Folioso extension where plain Markdown has no syntax, such as [text](folioso:span?marks=underline) for underlined text. Before answering, it reads its own Markdown back and compares the result with the stored page. Any block that wouldn't come back exactly is written instead as a ```folioso block holding that block's TipTap JSON, as stored:
## Install
```folioso
{"type":"orderedList","attrs":{"start":1,"type":"a"},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Lettered"}]}]}]}
```Most pages have no ```folioso blocks. They appear for what Markdown can't carry, such as a lettered list, a table with merged cells, or a link with extra attributes. See JSON blocks.
Editing Markdown you've read
- Change anything. Whatever you edit is read with the ordinary Markdown rules, and the rest comes back as it was.
- Leave the Folioso extensions alone unless you mean to change them. Deleting
(folioso:span?marks=underline)from a link removes the underline, for example. - Keep
```foliosoblocks valid. Each must still hold exactly one JSON object with a stringtype, or the whole request is refused with a422. You can also replace one with ordinary Markdown. - Link labels are for reading only. A page link's text is the target's title as you see it, or
Restricted pageif you can't open it. Folioso ignores it when you send the Markdown back, so a page link keeps its target whatever its text says.