WordPress
Content → Gutenberg blocks
Send Markdown, HTML or JSON and get native WordPress block markup back — the kind the block editor opens without complaining.
Reading this with an AI agent?
The entire documentation lives in one Markdown file, built to be fetched and read by an LLM. Copy the prompt below and paste it into ChatGPT, Claude, Cursor or any agent with web access — it asks the agent to read the file before helping you.
The problem this solves
Pushing AI-generated content into WordPress usually goes one of two ways, and both are bad.
Send raw HTML to the content field and WordPress wraps the whole thing in a single Classic block. It renders on the front end, but in the editor it is one opaque lump: no headings to click, no list to reorder, no way for an editor to fix a paragraph without touching HTML.
Send Markdown and it is worse — WordPress does not parse Markdown. You get literal ## and ** characters in the published post.
And hand-writing block markup by hand tends to produce the message every WordPress user recognises:
“This block contains unexpected or invalid content.” The editor compares the saved HTML against what the block says it should look like. One missing class, one attribute out of place, and the block is flagged as broken.
This endpoint produces the markup the editor expects, and verifies it before returning.
One request
curl -X POST https://api.searchops.io/v1/gutenberg/convert \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_YOUR_KEY" \
-d '{
"input_format": "markdown",
"content": "## How it works\n\nText with **bold** and a [link](https://example.com).\n\n- one\n- two"
}'The response carries the markup plus a report on what happened to your content:
{
"success": true,
"gutenberg": "<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">How it works</h2>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Text with <strong>bold</strong> and a <a href=\"https://example.com/\">link</a>.</p>\n<!-- /wp:paragraph -->",
"blocks": ["core/heading", "core/list", "core/list-item", "core/paragraph"],
"validation": { "syntax": "valid", "round_trip": "valid", "target": "not_checked" },
"warnings": [],
"losses": [],
"media": [],
"serializer": { "profile": "portable-article", "catalog": "2026-08-04", "api": "1.0.0" },
"generation_time_ms": 34
}Put gutenberg straight into the content field of the WordPress REST API. Nothing else to transform.
What the markup looks like
Gutenberg stores blocks as HTML wrapped in special comments. That is what makes each element editable in the editor rather than one frozen lump:
<!-- wp:heading --> <h2 class="wp-block-heading">How it works</h2> <!-- /wp:heading --> <!-- wp:paragraph --> <p>Text with <strong>bold</strong> and a <a href="https://example.com/">link</a>.</p> <!-- /wp:paragraph --> <!-- wp:list --> <ul class="wp-block-list"> <!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> </ul> <!-- /wp:list -->
The comment delimiters carry the block name and its attributes; the HTML between them is what the block saves. Both have to match what the block expects — classes like wp-block-heading are part of the validation, not decoration.
Blocks produced
The default profile is portable-article: only blocks that ship with WordPress and are available on virtually every install, so nothing depends on the theme or on plugins.
| Your content | Block |
|---|---|
| Paragraphs, bold, italic, inline code, links | core/paragraph |
| Headings | core/heading |
| Lists, including nested and ordered | core/list + core/list-item |
| Block quotes | core/quote |
| Fenced code | core/code |
| Preformatted text | core/preformatted |
| Tables | core/table |
| Images by URL | core/image |
| Horizontal rules | core/separator |
| Collapsible sections | core/details |
| HTML with no mapping | core/html |
Images are referenced by their original URL. Uploading them to the site’s media library, which is what gives you a wp-image-123 class and a real attachment, is not part of this release — see Publishing a post.
Three ways in
The default, and the best fit for LLM output.
{
"input_format": "markdown",
"content": "## Title\n\nText with **bold**."
}For content that already exists as markup — a CMS field or a rich-text editor. Structure is converted; CSS and scripts are discarded.
{
"input_format": "html",
"content": "<h2>Title</h2><p>Text with <strong>bold</strong>.</p>"
}For documents assembled by code. Same canonical tree used by the DOCX API, so one structure feeds both outputs.
{
"input_format": "json",
"content": "{\"children\":[{\"type\":\"heading\",\"level\":2,\"children\":[{\"type\":\"text\",\"value\":\"Report\"}]},{\"type\":\"list\",\"items\":[\"one\",\"two\"]}]}"
}Nothing is dropped silently
When something in your content cannot survive the conversion, the response says so. Every entry carries a stable code you can branch on, and the path that points at the offending node.
{
"warnings": [
{
"code": "HEADING_DEMOTED",
"severity": "warning",
"path": "/children/0",
"message": "H1 demoted to H2: the post title already provides the page H1.",
"action": "degraded"
}
],
"losses": [
{
"code": "SANITIZE_UNSAFE_URL",
"severity": "loss",
"path": "/children/3",
"message": "Link target uses a protocol that is not allowed and was removed.",
"action": "dropped"
}
]
}warnings means the content came through in a different shape. losses means something did not come through at all. A conversion can succeed with both — check them if fidelity matters to you.
The H1 rule
By default an h1 in your content is demoted to h2, with a warning. In WordPress the post title already renders as the page H1, so a second one hurts both SEO and screen-reader navigation. Change it with options.h1_policy: demote (default), keep or remove.
Validation before you get the markup
Every conversion is parsed back and re-serialised before the response is sent. If the result does not survive that round trip, you get a 422 instead of markup — and the request is not charged.
The reasoning is simple: broken block markup does not fail loudly. It publishes fine and only surfaces when a human opens the editor, which may be days later. An error at request time is cheaper than that.
Inspecting existing markup
POST /v1/gutenberg/parse takes block markup and returns its structure. Useful when a post is already broken and you need to find where. It does not consume credits.
curl -X POST https://api.searchops.io/v1/gutenberg/parse \
-H "Content-Type: application/json" -H "x-api-key: sk_live_YOUR_KEY" \
-d '{"content":"<!-- wp:paragraph -->\n<p>x</p>\n<!-- /wp:paragraph -->"}'Safety
- Link protocols are filtered. Only
http,https,mailto,teland relative URLs survive. Ajavascript:target is removed and the link text is kept, with aSANITIZE_UNSAFE_URLloss. - Your text cannot become markup. Content that happens to contain block delimiters is escaped, so it appears as text in the post instead of restructuring it.
- Scripts, iframes and inline handlers are removed from HTML input, along with their content.