Skip to content
Foliosodev
Menu
On this page

REST API

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:

bash
curl "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b?format=markdown" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"
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} and GET /pages/{slug}/snapshots/{id}) with ?format=markdown answer 422 on format:

    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 /pages and PATCH /pages/{slug}) with ?format=markdown have already saved the page by the time the response is written, so they don't answer 422. They succeed as usual and answer in the JSON shape instead: content and contentEpoch, with no markdown key. Check for markdown before 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, or null.
  • markdown: a Markdown string, or null.

Sending both is refused with a 422, whatever their values, even "content": null alongside "markdown": "":

json
{
  "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:

json
{
  "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 new contentEpoch, 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 the contentEpoch you last read as epoch to be refused with 409 Conflict if the page was restored or replaced since. See Avoid overwriting a restore.
  • POST: a new page starts at contentEpoch 0. There's nothing to conflict with, so epoch isn'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:

  1. 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.
  2. Split text. Neighbouring text nodes with the same marks may be joined into one.
  3. Mark order. Marks on a text node are kept in a fixed order: link, bold, code, italic, strike, underline.
  4. Missing content. A node with no content key 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:

markdown
## 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 ```folioso blocks valid. Each must still hold exactly one JSON object with a string type, or the whole request is refused with a 422. 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 page if 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.