---
title: Rich text
status: current
phase: 2
order: 1
---

# Rich text

`rich_text` fields are stored as **JSON blocks**, not HTML. That way the content doesn't depend on any framework: the API also delivers it converted to markdown and to HTML.

In the dashboard you write in a formatting editor (bold, italic, headings, bulleted and numbered lists, links) and it saves the document below. Blocks the toolbar can't edit (images, tables, code) show as a locked box and are kept as they are. Through the API you send markdown (a string) or the document: as an object, or as that object written as JSON text. Text that isn't a valid document as JSON is read as markdown.

## Document

```json
{
  "type": "doc",
  "content": [
    { "type": "heading", "level": 2, "content": [ { "type": "text", "text": "Open House" } ] },
    { "type": "paragraph", "content": [
      { "type": "text", "text": "A " },
      { "type": "text", "text": "great", "marks": ["bold"] },
      { "type": "text", "text": " day. " },
      { "type": "link", "href": "https://acton.example", "content": [ { "type": "text", "text": "Sign up" } ] }
    ] }
  ]
}
```

## Blocks

| `type` | Fields | Markdown |
| --- | --- | --- |
| `paragraph` | `content`: inline | A paragraph |
| `heading` | `level` (1 to 6), `content`: inline | `## Title` |
| `list` | `ordered` (bool), `content`: items `{ "type": "item", "content": [blocks] }` | `- one` or `1. one` |
| `quote` | `content`: blocks | `> quote` |
| `code` | `text`, `language` (optional) | Block with three backticks |
| `hr` | — | `---` |
| `image` | `asset` (ID of a Pluma asset) or `src` (external URL), and `alt` | `![alt](asset:ID)` or `![alt](https://…)` in its own paragraph |
| `table` | `content`: rows `{ "type": "row", "content": [ { "type": "cell", "content": [inline] } ] }`. The first row is the header | GitHub table: `\| A \| B \|` and the line `\| --- \| --- \|` |

## Inline

| `type` | Fields | Markdown |
| --- | --- | --- |
| `text` | `text`, `marks` (optional): `bold`, `italic`, `code`, `strike` | `**bold**`, `*italic*`, `` `code` ``, `~~strikethrough~~` |
| `link` | `href`, or `entry` (ID of another entry, see [Links to entries](#links-to-entries)); `content`: inline | `[text](url)` or `[text](entry:ID)` |
| `break` | — | Two spaces at the end of the line |

## Links to entries

To link to another entry of your site, write `[text](entry:12)`: it's stored as `{ "type": "link", "entry": "12", … }`, a link to **that entry**, not to its address today.

- **Reading** with `rich_text=markdown` or `html`, it comes out as the entry's path on your site, from its type's [page URL](../guides/content-model.md#page-url): `[seeded rye](/menu/rye-seeded)`. Change the slug and every link follows it, without editing the posts.
- If the type has no page URL, or the entry isn't published (with a delivery key), the link stays `entry:12` in markdown and becomes `#` in HTML.
- **Publishing** checks every entry link: the target has to exist and be published, or you get [`validation_failed`](../errors/validation_failed.md) with `links to entry:12, which isn't published yet`. Publish the target first (in a [batch](../api/management.md#batch), put its `publish_entry` earlier).

## Rules

- Markdown → blocks → markdown gives the same document (lossless round trip).
- When **reading** with `rich_text=markdown` or `rich_text=html`, Pluma images come out with their real URL, ready for your site. With `rich_text=json` they stay as `"asset": "ID"` and the URL is in `includes` (with `include=1`).
- HTML comes out escaped. Links that don't start with `http`, `https`, `mailto`, `/` or `#` are replaced with `#`: no `javascript:`.
- Raw HTML inside the markdown (`<u>`, `<div>`) is stored as **text**, not interpreted. If your content needs HTML, store it in a `text` field with the markdown as is and render it on your site.
- Table column alignment (`:---:`) is not stored.
