slateToMarkdown
From @slate-serializers/markdown. Converts Slate JSON to a GitHub Flavored Markdown string. Use it to export editor content to README files, issue trackers, chat tools or anything else that accepts Markdown.
npm install @slate-serializers/markdown
All
output.md file content on this page is generated with the
slateToMarkdown serializer.
Default
The default configuration understands the element names used by the other @slate-serializers packages (p, h1, ul, li, blockquote, link, …) and by the Slate examples (paragraph, heading-one, bulleted-list, list-item, block-quote, check-list-item, …), so you can often call it without a config.
import { slateToMarkdown } from '@slate-serializers/markdown'
const slate = [
{
"type": "h1",
"children": [
{
"text": "Heading 1"
}
]
},
{
"type": "p",
"children": [
{
"text": "A paragraph with "
},
{
"text": "bold",
"bold": true
},
{
"text": ", "
},
{
"text": "italic",
"italic": true
},
{
"text": " and "
},
{
"text": "inline code",
"code": true
},
{
"text": ", plus a "
},
{
"type": "link",
"url": "https://docs.slatejs.org",
"children": [
{
"text": "link"
}
]
},
{
"text": "."
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "One"
}
]
},
{
"type": "li",
"children": [
{
"text": "Two"
}
]
}
]
},
{
"type": "blockquote",
"children": [
{
"text": "A quote."
}
]
}
]
const markdown = slateToMarkdown(slate)
# Heading 1
A paragraph with **bold**, *italic* and `inline code`, plus a [link](https://docs.slatejs.org).
- One
- Two
> A quote.
Supported Markdown
- Headings, paragraphs, block quotes, horizontal rules, line breaks, links and images.
- Bullet and ordered lists, including nested lists and task lists.
- Fenced code blocks and tables with column alignment.
- Bold, italic, strikethrough and inline code. Underline, subscript and superscript have no Markdown syntax, so they are written as HTML tags, which GFM allows.
- Text is escaped, so characters such as
*, _, [ or a leading 1. stay literal.
Some constructs read extra properties from the Slate node:
| Construct | Properties read from the node |
|---|
link | url (or href) |
image | url (or src), alt (or caption, or the node's text) |
ol | start |
li, task | checked: a boolean makes the item a task list item |
code-block | language (or lang). Child elements (e.g. code-line) become lines. |
table-cell | align (or textAlign) on the first row sets the column alignment |
import { slateToMarkdown } from '@slate-serializers/markdown'
const slate = [
{
"type": "ol",
"start": 3,
"children": [
{
"type": "li",
"children": [
{
"text": "Third"
}
]
},
{
"type": "li",
"children": [
{
"text": "Fourth, with a nested list"
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "Nested"
}
]
}
]
}
]
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"checked": true,
"children": [
{
"text": "Done"
}
]
},
{
"type": "li",
"checked": false,
"children": [
{
"text": "To do"
}
]
}
]
},
{
"type": "code-block",
"language": "ts",
"children": [
{
"type": "code-line",
"children": [
{
"text": "const greeting = 'Hello'"
}
]
},
{
"type": "code-line",
"children": [
{
"text": "console.log(greeting)"
}
]
}
]
},
{
"type": "table",
"children": [
{
"type": "tr",
"children": [
{
"type": "th",
"children": [
{
"text": "Package"
}
]
},
{
"type": "th",
"align": "right",
"children": [
{
"text": "Version"
}
]
}
]
},
{
"type": "tr",
"children": [
{
"type": "td",
"children": [
{
"text": "@slate-serializers/markdown"
}
]
},
{
"type": "td",
"children": [
{
"text": "2.8.1"
}
]
}
]
}
]
},
{
"type": "image",
"url": "https://example.com/diagram.png",
"alt": "Diagram",
"children": [
{
"text": ""
}
]
},
{
"type": "hr",
"children": [
{
"text": ""
}
]
},
{
"type": "p",
"children": [
{
"text": "Line one\nLine two"
}
]
}
]
const markdown = slateToMarkdown(slate)
3. Third
4. Fourth, with a nested list
- Nested
- [x] Done
- [ ] To do
```ts
const greeting = 'Hello'
console.log(greeting)
```
| Package | Version |
| --- | ---: |
| @slate-serializers/markdown | 2.8.1 |

---
Line one\
Line two
Payload CMS
If you are using Slate Rich Text in Payload CMS, pass payloadSlateToMarkdownConfig. It adds support for Payload upload elements: images become  and other files become links.
import { slateToMarkdown, payloadSlateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "h2",
"children": [
{
"text": "Uploads"
}
]
},
{
"type": "upload",
"relationTo": "media",
"value": {
"url": "/media/diagram.png",
"alt": "Architecture diagram",
"mimeType": "image/png"
},
"children": [
{
"text": ""
}
]
},
{
"type": "upload",
"relationTo": "media",
"value": {
"url": "/media/report.pdf",
"filename": "report.pdf",
"mimeType": "application/pdf"
},
"children": [
{
"text": ""
}
]
}
]
const markdown = slateToMarkdown(slate, payloadSlateToMarkdownConfig)
## Uploads

[report.pdf](/media/report.pdf)
Options
Spread slateToMarkdownConfig and override what you need. The config type is exported as SlateToMarkdownConfig.
elementMap
Map a Slate element type to a Markdown construct: paragraph, h1–h6, blockquote, ul, ol, li, task, link, image, line-break, hr, code-block, table, table-section, table-row or table-cell.
import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "title",
"children": [
{
"text": "Release notes"
}
]
},
{
"type": "p",
"children": [
{
"text": "A custom title element becomes a level 1 heading."
}
]
}
]
const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
elementMap: { ...slateToMarkdownConfig.elementMap, title: 'h1' },
})
# Release notes
A custom title element becomes a level 1 heading.
markMap
Map a leaf property such as bold to strong, emphasis, strikethrough or code, or to a pair of { open, close } strings. Earlier entries wrap later ones.
import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "p",
"children": [
{
"text": "Markdown has no syntax for "
},
{
"text": "highlighted",
"highlight": true
},
{
"text": " or "
},
{
"text": "underlined",
"underline": true
},
{
"text": " text, so HTML tags are used."
}
]
}
]
const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
markMap: {
...slateToMarkdownConfig.markMap,
highlight: { open: '<mark>', close: '</mark>' },
},
})
Markdown has no syntax for <mark>highlighted</mark> or <u>underlined</u> text, so HTML tags are used.
Custom output per element type. Each function receives the node and its children, already serialized to Markdown, and returns a string. Return undefined to fall back to elementMap.
import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "p",
"children": [
{
"text": "Thanks "
},
{
"type": "mention",
"username": "thompsonsj",
"children": [
{
"text": ""
}
]
},
{
"text": " for the review."
}
]
},
{
"type": "callout",
"kind": "Note",
"children": [
{
"text": "Callouts become block quotes."
}
]
}
]
const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
elementTransforms: {
mention: ({ node }) => `@${node.username}`,
callout: ({ node, children }) => `> **${node.kind}:** ${children}`,
},
})
Thanks @thompsonsj for the review.
> **Note:** Callouts become block quotes.
emphasisDelimiter and bulletMarker
emphasisDelimiter: '*' (default) or '_'. * also works inside words.bulletMarker: '-' (default), '*' or '+'.
import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "p",
"children": [
{
"text": "Some "
},
{
"text": "emphasis",
"italic": true
},
{
"text": " and a literal *asterisk*."
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "One"
}
]
},
{
"type": "li",
"children": [
{
"text": "Two"
}
]
}
]
}
]
const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
emphasisDelimiter: '_',
bulletMarker: '*',
})
Some _emphasis_ and a literal \*asterisk\*.
* One
* Two
escape
Escape text that Markdown would treat as syntax. Default: true. Set to false if your text already contains Markdown.
import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'
const slate = [
{
"type": "p",
"children": [
{
"text": "Text that already contains **Markdown**."
}
]
}
]
slateToMarkdown(slate)
// default: the asterisks are escaped
slateToMarkdown(slate, { ...slateToMarkdownConfig, escape: false })
// escape: false: the asterisks are written as given
Text that already contains \*\*Markdown\*\*.
output.md (escape: false)
Text that already contains **Markdown**.
Behaviour
- Blocks are separated by a blank line. Empty paragraphs are dropped, because Markdown cannot represent them.
- A
\n in text becomes a hard line break (\ at the end of the line). In table cells it becomes <br>. - The first table row is the header row. Content that tables cannot hold, such as lists, is joined with
<br>. - Elements that are not in
elementMap are treated as inline when they sit beside text, and as a wrapper around their children otherwise. - Link attributes that Markdown has no syntax for, such as
newTab, are ignored. Use elementTransforms to output HTML instead. - When a
*, ** or ~~ pair would not parse in its position (for example bold directly next to italic), the equivalent HTML tag is written instead. - Escaping keeps Markdown syntax in text literal. It is not HTML sanitization:
elementTransforms, { open, close } marks and URLs are written as given. Sanitize the result if you render untrusted documents as HTML.
Try the interactive demo.