content-json
npx skills add https://github.com/crystallizeapi/ai --skill content-jsonCrystallize Rich Text Content JSON
You are generating rich text content in the Crystallize Content Transformer JSON format — a tree of nodes representing structured rich text (similar to an AST).
The output must be a JSON array of block nodes (or a single node object when used inline).
Node Structure
Every node has these properties:
| Property | Type | Required | Description |
|---|---|---|---|
kind |
"block" or "inline" |
yes | Block-level or inline |
type |
string or null |
yes | Semantic content type (see tables below) |
textContent |
string |
no | Raw text (leaf nodes only — mutually exclusive with children) |
children |
node[] |
no | Child nodes (mutually exclusive with textContent) |
metadata |
object |
no | Extra attributes (e.g. { "href": "..." } for links, { "id": "..." } for any element) |
A node is either a leaf (has textContent) or a branch (has children). Never set both.
Block Node Types (kind: "block")
type |
HTML equivalent | Notes |
|---|---|---|
"paragraph" |
<p> |
Most common block type |
"heading1" – "heading6" |
<h1> – <h6> |
|
"unordered-list" |
<ul> |
Children must be list-item |
"ordered-list" |
<ol> |
Children must be list-item |
"list-item" |
<li> |
|
"quote" |
<blockquote> |
Block quote |
"code" |
<code> |
Code block |
"preformatted" |
<pre> |
Preformatted text |
"table" |
<table> |
Contains table-row directly |
"table-row" |
<tr> |
Contains table-cell or table-head-cell |
"table-cell" |
<td> |
Contains block content (paragraph) |
"table-head-cell" |
<th> |
Contains block content (paragraph) |
"horizontal-line" |
<hr> |
Self-closing, no children/textContent needed |
"image" |
<img> |
Self-closing, use metadata for src, alt etc. |
"container" |
<div> |
Generic block container |
"section" |
<section> |
|
"article" |
<article> |
|
"address" |
<address> |
|
"figure" |
<figure> |
|
"figcaption" |
<figcaption> |
|
"details" |
<details> |
|
"deleted" |
<del> |
|
"picture" |
<picture> |
|
"title-of-a-work" |
<cite> |
Inline Node Types (kind: "inline")
type |
HTML equivalent | Notes |
|---|---|---|
null |
(none) | Plain text wrapper — use textContent for raw text |
"strong" |
<strong> |
Bold |
"emphasized" |
<em> |
Italic |
"underlined" |
<u> |
|
"link" |
<a> |
Requires metadata.href. Optionally metadata.target |
"highlight" |
<mark> |
|
"subscripted" |
<sub> |
|
"superscripted" |
<sup> |
|
"line-break" |
<br> |
Self-closing |
"abbrevition" |
<abbr> |
Note: this is the actual spelling used in the format |
"aside" |
<aside> |
|
"container" |
<span> |
Generic inline container |
"time" |
<time> |
Metadata
- Links (
type: "link"):{ "href": "https://...", "target": "_blank" }—hrefandtargetare supported. - All elements:
{ "id": "some-id" }— theidattribute is valid on every node. - No other HTML attributes are supported in metadata.
Key Rules
- Text lives in leaf nodes only. A paragraph contains inline children; those inline children hold the
textContent. - Plain text uses inline nodes with
type: null:{ "kind": "inline", "type": null, "textContent": "Hello" }. - Lists must follow:
unordered-list/ordered-list→list-item→ inline/block content. - Tables must follow:
table→table-row→table-cell/table-head-cell→paragraph→ inline content. Cells usekind: "block". A header is a first row oftable-head-cell.- Never emit
table-head,table-body,table-footerortable-caption: the Crystallize App editor drops them along with every row inside. When converting HTML, put the rows of<thead>/<tbody>/<tfoot>directly undertable, and turn a<caption>into a paragraph before the table.
- Never emit
- Nested formatting is done via
children: e.g. bold + italic =strongnode containing anemphasizedchild. - Top-level output is always an array of block nodes.
Strict Formatting Enforcement
These rules are mandatory — every generated or modified node must comply. Validate the output before returning it.
-
typeis always required. Every node must have an explicittypefield. For plain text inlines, use"type": null. Never omit it.- ❌
{ "kind": "inline", "textContent": "hello" } - ✅
{ "kind": "inline", "type": null, "textContent": "hello" }
- ❌
-
Formatting nodes (
strong,emphasized,underlined,link,highlight, etc.) must usechildren, nevertextContentdirectly. Only plain text nodes (type: null) and self-closing nodes may usetextContentas a leaf.- ❌
{ "kind": "inline", "type": "strong", "textContent": "bold text" } - ✅
{ "kind": "inline", "type": "strong", "children": [{ "kind": "inline", "type": null, "textContent": "bold text" }] }
- ❌
-
No empty block nodes. Every block node must have either
children(with at least one child) ortextContent. Do not emit blocks like{ "kind": "block", "type": "paragraph" }with neither. -
Preserve existing structure. When the input already contains Crystallize JSON, preserve its formatting and structure. Do not restructure, re-wrap, or flatten nodes that are already valid.
- Tables saved by the Crystallize App editor have
kind: "inline"cells. Leave them as they are, but give new cellskind: "block": only block cells render as<td>/<th>in the API’s HTML output.
- Tables saved by the Crystallize App editor have
-
codeblocks insidepreformattedusekind: "block"and may holdtextContentdirectly (they are leaf blocks). This is the one exception where a typed block node carriestextContent.
Examples
Simple paragraph with bold text
[ { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "This is " }, { "kind": "inline", "type": "strong", "children": [ { "kind": "inline", "type": null, "textContent": "bold" } ] }, { "kind": "inline", "type": null, "textContent": " text." } ] }]Heading + paragraph with a link
[ { "kind": "block", "type": "heading2", "children": [ { "kind": "inline", "type": null, "textContent": "About Us" } ] }, { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "Visit " }, { "kind": "inline", "type": "link", "metadata": { "href": "https://example.com", "target": "_blank" }, "children": [ { "kind": "inline", "type": null, "textContent": "our site" } ] }, { "kind": "inline", "type": null, "textContent": " for more." } ] }]Unordered list
[ { "kind": "block", "type": "unordered-list", "children": [ { "kind": "block", "type": "list-item", "children": [ { "kind": "inline", "type": null, "textContent": "First item" } ] }, { "kind": "block", "type": "list-item", "children": [ { "kind": "inline", "type": null, "textContent": "Second item" } ] } ] }]Table with a header row
[ { "kind": "block", "type": "table", "children": [ { "kind": "block", "type": "table-row", "children": [ { "kind": "block", "type": "table-head-cell", "children": [ { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "Name" } ] } ] }, { "kind": "block", "type": "table-head-cell", "children": [ { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "Price" } ] } ] } ] }, { "kind": "block", "type": "table-row", "children": [ { "kind": "block", "type": "table-cell", "children": [ { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "Shoe" } ] } ] }, { "kind": "block", "type": "table-cell", "children": [ { "kind": "block", "type": "paragraph", "children": [ { "kind": "inline", "type": null, "textContent": "42" } ] } ] } ] } ] }]Bold + italic (nested inline formatting)
{ "kind": "inline", "type": "strong", "children": [ { "kind": "inline", "type": "emphasized", "children": [ { "kind": "inline", "type": null, "textContent": "bold and italic" } ] } ]}
Crystallize AI