Why store JSON instead of HTML#
When picking an editor for this site, the thing that settled it for Editor.js was that it does not hand back one lump of HTML — it hands back a list of blocks as JSON. That sounds like a small difference. It changes everything downstream.
- Content stays data rather than markup, so building a table of contents from the
headerblocks or computing reading time from the prose is direct work. - The presentation can change without touching a single stored row, because the styling lives in components rather than inside the content.
- The surface where raw HTML has to be trusted shrinks to inline formatting instead of a whole article.
- The same content can be reused for RSS, meta descriptions and search indexing without stripping tags first.
The shape you actually get#
json
{
"time": 1754006400000,
"blocks": [
{ "id": "a1", "type": "header", "data": { "text": "Overview", "level": 2 } },
{ "id": "a2", "type": "paragraph", "data": { "text": "Text can be <b>bold</b>." } }
],
"version": "2.31.0"
}Mapping block types to components#
The whole renderer hinges on one lookup table from block.type to a component. Enabling a new tool in the editor means adding one line here, and nowhere else.
javascript
const RENDERERS: Record<string, ComponentType<BlockProps>> = {
header: HeaderBlock,
paragraph: ParagraphBlock,
list: ListBlock,
checklist: ChecklistBlock,
quote: QuoteBlock,
code: CodeBlock,
table: TableBlock,
delimiter: DelimiterBlock,
}
export function Blocks({ doc }: { doc: EditorDocument }) {
return doc.blocks.map((block) => {
const Block = RENDERERS[block.type]
if (!Block) return null // unsupported tool: skip it, never crash the page
return <Block key={block.id} data={block.data} />
})
}An unrecognised block should disappear quietly. It should never take the page down with it.
The parts that catch people out#
- Block text is always inline HTML (
<b>,<i>, links). Strip the tags before reusing it in a table of contents or an excerpt. - Never assume
datahas the shape you expect — it comes out of a JSON column, so narrow it before you read it. - @editorjs/list v2 changed
itemsfrom an array of strings to an array of objects carrying content, meta and nested items. - The
checklistblock keys its items on text, not on content the way list does. This one bites almost everybody once.
- Write a component for every block type enabled in the editor
- Fail soft on unknown blocks and missing fields
- Strip inline tags before using text in the TOC and meta tags
- Add image and embed blocks once there are real media files
| Block | Key fields | Watch out for |
|---|---|---|
| header | text, level | level can be 1 to 6; clamp it to h2/h3 before building a TOC |
| list | style, items[] | v2 items are objects, no longer plain strings |
| checklist | items[].text, items[].checked | Uses text, unlike list which uses content |
| table | withHeadings, content[][] | The first row is a header row only when withHeadings is true |