Content-Blocks & KI-Bildkennzeichnung
Diese Seite dokumentiert das content_blocks-Format: die JSON-Struktur, die blog-maker an ein Publish-Ziel vom Typ "funnelexpert" sendet. Damit laufen unsere eigenen gehosteten Seiten und jedes bisher angebundene Drittanbieter-Backend (WordPress- und custom_rest-Ziele nutzen ein anderes, einfacheres Format, siehe die Links unten).
Wann sinnvoll
Beim Bau eines Receivers für eine Seite, die nicht WordPress ist und volle strukturelle Treue braucht (Überschriften, Listen, FAQ-Blöcke, Bilder) statt eines einzelnen flachen HTML-Strings. Reicht flaches HTML für dein Backend, nutze stattdessen das einfachere custom_rest-Format.
Struktur der obersten Ebene
Jede Veröffentlichung ist ein einzelner POST mit dieser Struktur:
| Feld | Typ | Beschreibung |
|---|---|---|
slug | string, required | URL-Slug für den Artikel. |
status | "draft" | "published", optional | Standard ist published. |
category | string, optional | Kategorie-Name; wird bei dir angelegt, falls noch nicht vorhanden. |
seo | object, required | focus_keyword, seo_title, meta_description, secondary_keywords[]. |
translations | object, required | Nach Sprachcode geschlüsselt ("en", "de", ...); jeder Wert hat title, excerpt, content_blocks[]. |
source | string, optional | Kennzeichnet den Absender, z.B. "seo_blog_generator". |
source_id | string, optional | Unsere eigene Artikel-ID, nützlich für idempotente Updates bei dir. |
Content-Block-Typen
Jeder Eintrag in translations[lang].content_blocks hat einen type und typ-spezifische Felder:
| Typ | Felder | Beschreibung |
|---|---|---|
heading-2 … heading-6 | content, level | Eine Überschrift. level wird bei uns auf 2 bis 6 begrenzt, damit ein übersetzter Body nie ein zweites H1 erzeugt. |
paragraph | content | Inline-HTML ist in content erlaubt (<strong>, <em>, <a>); als untrusted behandeln und bei dir sanitizen. |
list | items[], ordered | ordered true rendert <ol>, sonst <ul>. |
blockquote | content | Einfaches Zitat. |
callout | content | Eine "gut zu wissen"-Hinweisbox, optisch von einem einfachen Zitat unterschieden. |
table | content | Rohes Tabellen-HTML, unverändert durchgereicht. |
image | src, alt, caption, ai_origin | Siehe den Abschnitt zur KI-Bildkennzeichnung unten für ai_origin. |
divider | (none) | Eine horizontale Linie. |
iframe | src | Nur vertrauenswürdige Embeds (Allowlist-Hosts); alles andere rendert leer. |
faq | questions[] ({question, answer}) | Ein strukturierter FAQ-Block, gedacht auch für dein FAQPage-Schema, falls vorhanden. |
html | content | Ein Passthrough für einen selbstschließenden Block-Kommentar, den wir bei der Konvertierung nicht erkannt haben (z.B. ein SEO-Plugin-Marker). Kann gefahrlos ignoriert oder verworfen werden, nichts hängt vom Rendern ab. |
Vollständiges Beispiel
Ein kompletter Request und die erwartete Antwort:
POST https://example.com/api/blog
Authorization: Bearer <your-api-key>
Content-Type: application/json
{
"slug": "how-to-set-up-blog-automation",
"status": "published",
"category": "guides",
"seo": {
"focus_keyword": "blog automation",
"seo_title": "How to Set Up Blog Automation",
"meta_description": "Learn step by step how content automation works.",
"secondary_keywords": ["content pipeline", "SEO workflow"]
},
"translations": {
"en": {
"title": "How to Set Up Blog Automation",
"excerpt": "A step-by-step guide to automating your content pipeline.",
"content_blocks": [
{ "type": "heading-2", "content": "Getting started", "level": 2 },
{ "type": "paragraph", "content": "Blog automation starts with a clear <strong>content pipeline</strong>." },
{
"type": "image",
"src": "https://blog-maker.com/images/articles/pipeline-hero.webp",
"alt": "A content pipeline diagram",
"caption": "The pipeline, end to end.",
"ai_origin": "generated"
},
{ "type": "list", "items": ["Plan", "Write", "Review", "Publish"], "ordered": true },
{
"type": "faq",
"questions": [
{ "question": "How long does this take?", "answer": "Most brands are live within a day." }
]
}
]
}
},
"source": "seo_blog_generator",
"source_id": "8421"
}{
"ok": true,
"id": "post-8421",
"url": "https://example.com/blog/how-to-set-up-blog-automation"
}KI-Bildkennzeichnung (ai_origin)
Der EU AI Act (Art. 50(4)) verlangt eine klare Kennzeichnung, wenn eine Website ein KI-generiertes oder KI-bearbeitetes Bild zeigt. blog-maker verfolgt das an der Quelle: ein Bild-content_block trägt "ai_origin": "generated" nur, wenn wir das Bild generiert haben und keine menschliche Foto-Credit-Angabe angewendet wurde. Das Feld fehlt bei allem anderen (ein echtes Foto, ein Upload des Autors, eine externe URL). Unbekannte Herkunft behauptet nie KI, ein Receiver, der das Feld ignoriert, zeigt also einfach keine Kennzeichnung statt einer falschen.
Die einzige Aufgabe deines Receivers ist, auf genau diesem Bild ein sichtbares Badge zu rendern, wenn ai_origin gesetzt ist. Markup und CSS unten sind exakt die, die unsere sechs angebundenen Seiten nutzen; Klassennamen unverändert lassen, damit das Design konsistent bleibt und ein künftiges Badge-Update (ein neuer Sprach-String, ein Design-Refresh) ein Copy-Paste ist, kein Neubau.
<span class="ai-origin-frame"> <img src="..." alt="..." loading="lazy"> <span class="ai-origin-badge">AI-generated</span> </span> <!-- small thumbnails (listing cards, related grids): --> <span class="ai-origin-frame"> <img src="..." alt="..." loading="lazy"> <span class="ai-origin-badge ai-origin-badge--micro">AI</span> </span>
.ai-origin-frame { position: relative; display: block; line-height: 0; }
.ai-origin-frame img { display: block; width: 100%; height: auto; margin: 0; }
.ai-origin-badge {
position: absolute;
left: 10px;
bottom: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 9px 4px 5px;
border-radius: 999px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
color: #fff;
font-size: 10.5px;
font-weight: 500;
line-height: 1.3;
letter-spacing: 0.01em;
white-space: nowrap;
pointer-events: none;
}
.ai-origin-badge::before {
content: "";
width: 13px;
height: 13px;
flex: 0 0 auto;
background-image: url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 566.93 566.93'%3E%3Cdefs%3E%3Cstyle%3E .cls-1 { fill: %23fff; fill-rule: evenodd; } .cls-2 { fill: %231d1d1b; } %3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M272.03,100.72c100.92,0,182.74,81.82,182.74,182.75s-81.82,182.74-182.74,182.74-182.75-81.82-182.75-182.74,81.82-182.75,182.75-182.75'/%3E%3Cg%3E%3Cpath class='cls-2' d='M170.79,353.74c-1.08,0-2.05-.43-2.92-1.31-.88-.87-1.31-1.84-1.31-2.92,0-.67.07-1.27.2-1.81l47.34-129.32c.4-1.48,1.24-2.79,2.52-3.93,1.27-1.14,3.05-1.71,5.34-1.71h29.81c2.28,0,4.06.57,5.34,1.71,1.27,1.14,2.11,2.45,2.52,3.93l47.14,129.32c.27.54.4,1.14.4,1.81,0,1.08-.44,2.05-1.31,2.92s-1.91,1.31-3.12,1.31h-24.78c-2.01,0-3.52-.5-4.53-1.51-1.01-1.01-1.65-1.91-1.91-2.72l-7.86-20.55h-53.78l-7.65,20.55c-.27.81-.88,1.71-1.81,2.72-.94,1.01-2.55,1.51-4.83,1.51h-24.78ZM218.13,299.96h37.47l-18.93-53.18-18.53,53.18Z'/%3E%3Cpath class='cls-2' d='M328.11,353.74c-1.48,0-2.69-.47-3.63-1.41-.94-.94-1.41-2.15-1.41-3.63v-130.93c0-1.48.47-2.68,1.41-3.63s2.15-1.41,3.63-1.41h26.99c1.48,0,2.68.47,3.63,1.41.94.94,1.41,2.15,1.41,3.63v130.93c0,1.48-.47,2.69-1.41,3.63-.94.94-2.15,1.41-3.63,1.41h-26.99Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.ai-origin-badge--micro {
left: 6px;
bottom: 6px;
padding: 2px 6px 2px 5px;
gap: 4px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
opacity: 1;
font-size: 9px;
font-weight: 700;
border-radius: 999px;
}
.ai-origin-badge--micro::before {
width: 11px;
height: 11px;
}
/* Add the --right modifier class below when your layout needs the badge in
the bottom-right corner instead (e.g. a hero with a headline overlay on
the left). Icon, size and colors stay identical, only the corner flips. */
.ai-origin-badge.ai-origin-badge--right { left: auto; right: 10px; }Mit einem AI-Assistenten bauen
Einen der folgenden Prompts in Claude, ChatGPT oder deinen bevorzugten Assistenten kopieren. Jeder trägt den vollständigen Contract oben, damit der Assistent einen funktionierenden Receiver bauen kann, ohne diese Seite erneut zu lesen.
I'm connecting my website to blog-maker.com, an SEO content platform. It will
POST published articles to an endpoint I control, using its "content_blocks"
JSON format. Build me a minimal receiver in [YOUR LANGUAGE/FRAMEWORK] that:
1. Accepts POST requests, authenticated with a Bearer token I configure.
2. Parses this request shape (fields I don't recognize should be ignored,
never cause an error):
POST https://example.com/api/blog
Authorization: Bearer <your-api-key>
Content-Type: application/json
{
"slug": "how-to-set-up-blog-automation",
"status": "published",
"category": "guides",
"seo": {
"focus_keyword": "blog automation",
"seo_title": "How to Set Up Blog Automation",
"meta_description": "Learn step by step how content automation works.",
"secondary_keywords": ["content pipeline", "SEO workflow"]
},
"translations": {
"en": {
"title": "How to Set Up Blog Automation",
"excerpt": "A step-by-step guide to automating your content pipeline.",
"content_blocks": [
{ "type": "heading-2", "content": "Getting started", "level": 2 },
{ "type": "paragraph", "content": "Blog automation starts with a clear <strong>content pipeline</strong>." },
{
"type": "image",
"src": "https://blog-maker.com/images/articles/pipeline-hero.webp",
"alt": "A content pipeline diagram",
"caption": "The pipeline, end to end.",
"ai_origin": "generated"
},
{ "type": "list", "items": ["Plan", "Write", "Review", "Publish"], "ordered": true },
{
"type": "faq",
"questions": [
{ "question": "How long does this take?", "answer": "Most brands are live within a day." }
]
}
]
}
},
"source": "seo_blog_generator",
"source_id": "8421"
}
3. Renders each content_blocks entry to HTML for my site. Block types and
their fields:
- heading-2 … heading-6: content, level : A heading. level is clamped to 2 through 6 on our side, so a translated body never emits a second page H1.
- paragraph: content : Inline HTML is allowed inside content (<strong>, <em>, <a>); treat it as untrusted and sanitize on your side.
- list: items[], ordered : ordered true renders <ol>, otherwise <ul>.
- blockquote: content : Plain quote.
- callout: content : A "good to know" tip box, visually distinct from a plain blockquote.
- table: content : Raw table HTML, passed through as-is.
- image: src, alt, caption, ai_origin : See the AI image disclosure section below for ai_origin.
- divider: (none) : A horizontal rule.
- iframe: src : Trusted embeds only (allowlisted hosts); anything else renders as nothing.
- faq: questions[] ({question, answer}) : A structured FAQ block, meant to also drive your FAQPage schema if you have one.
- html: content : A passthrough for a self-closing block comment we didn't recognize while converting (e.g. an SEO plugin marker). Safe to ignore or drop; nothing depends on you rendering it.
4. For "image" blocks: when ai_origin is "generated", render the image
wrapped in this exact markup and include this exact CSS (do not change
the class names, they must stay ai-origin-frame / ai-origin-badge /
ai-origin-badge--micro), since this is an EU AI Act Art. 50(4) disclosure
requirement, not just decoration. When ai_origin is absent, render the
image with no badge at all.
Markup (standard tier for hero/inline images, micro tier for small
thumbnails/listing cards):
<span class="ai-origin-frame">
<img src="..." alt="..." loading="lazy">
<span class="ai-origin-badge">AI-generated</span>
</span>
<!-- small thumbnails (listing cards, related grids): -->
<span class="ai-origin-frame">
<img src="..." alt="..." loading="lazy">
<span class="ai-origin-badge ai-origin-badge--micro">AI</span>
</span>
CSS (one block, both tiers):
.ai-origin-frame { position: relative; display: block; line-height: 0; }
.ai-origin-frame img { display: block; width: 100%; height: auto; margin: 0; }
.ai-origin-badge {
position: absolute;
left: 10px;
bottom: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 9px 4px 5px;
border-radius: 999px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
color: #fff;
font-size: 10.5px;
font-weight: 500;
line-height: 1.3;
letter-spacing: 0.01em;
white-space: nowrap;
pointer-events: none;
}
.ai-origin-badge::before {
content: "";
width: 13px;
height: 13px;
flex: 0 0 auto;
background-image: url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 566.93 566.93'%3E%3Cdefs%3E%3Cstyle%3E .cls-1 { fill: %23fff; fill-rule: evenodd; } .cls-2 { fill: %231d1d1b; } %3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M272.03,100.72c100.92,0,182.74,81.82,182.74,182.75s-81.82,182.74-182.74,182.74-182.75-81.82-182.75-182.74,81.82-182.75,182.75-182.75'/%3E%3Cg%3E%3Cpath class='cls-2' d='M170.79,353.74c-1.08,0-2.05-.43-2.92-1.31-.88-.87-1.31-1.84-1.31-2.92,0-.67.07-1.27.2-1.81l47.34-129.32c.4-1.48,1.24-2.79,2.52-3.93,1.27-1.14,3.05-1.71,5.34-1.71h29.81c2.28,0,4.06.57,5.34,1.71,1.27,1.14,2.11,2.45,2.52,3.93l47.14,129.32c.27.54.4,1.14.4,1.81,0,1.08-.44,2.05-1.31,2.92s-1.91,1.31-3.12,1.31h-24.78c-2.01,0-3.52-.5-4.53-1.51-1.01-1.01-1.65-1.91-1.91-2.72l-7.86-20.55h-53.78l-7.65,20.55c-.27.81-.88,1.71-1.81,2.72-.94,1.01-2.55,1.51-4.83,1.51h-24.78ZM218.13,299.96h37.47l-18.93-53.18-18.53,53.18Z'/%3E%3Cpath class='cls-2' d='M328.11,353.74c-1.48,0-2.69-.47-3.63-1.41-.94-.94-1.41-2.15-1.41-3.63v-130.93c0-1.48.47-2.68,1.41-3.63s2.15-1.41,3.63-1.41h26.99c1.48,0,2.68.47,3.63,1.41.94.94,1.41,2.15,1.41,3.63v130.93c0,1.48-.47,2.69-1.41,3.63-.94.94-2.15,1.41-3.63,1.41h-26.99Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.ai-origin-badge--micro {
left: 6px;
bottom: 6px;
padding: 2px 6px 2px 5px;
gap: 4px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
opacity: 1;
font-size: 9px;
font-weight: 700;
border-radius: 999px;
}
.ai-origin-badge--micro::before {
width: 11px;
height: 11px;
}
/* Add the --right modifier class below when your layout needs the badge in
the bottom-right corner instead (e.g. a hero with a headline overlay on
the left). Icon, size and colors stay identical, only the corner flips. */
.ai-origin-badge.ai-origin-badge--right { left: auto; right: 10px; }
5. Store the article, then respond with:
{
"ok": true,
"id": "post-8421",
"url": "https://example.com/blog/how-to-set-up-blog-automation"
}
6. Return HTTP 401 for a missing/wrong Authorization header, HTTP 400 for a
malformed body, and 2xx only on real success.I need a receiver endpoint for blog-maker.com's article publishing API. It
POSTs JSON in a "content_blocks" format (full spec below) to my endpoint,
Bearer-authenticated. Please write it in [YOUR LANGUAGE/FRAMEWORK].
Request shape:
POST https://example.com/api/blog
Authorization: Bearer <your-api-key>
Content-Type: application/json
{
"slug": "how-to-set-up-blog-automation",
"status": "published",
"category": "guides",
"seo": {
"focus_keyword": "blog automation",
"seo_title": "How to Set Up Blog Automation",
"meta_description": "Learn step by step how content automation works.",
"secondary_keywords": ["content pipeline", "SEO workflow"]
},
"translations": {
"en": {
"title": "How to Set Up Blog Automation",
"excerpt": "A step-by-step guide to automating your content pipeline.",
"content_blocks": [
{ "type": "heading-2", "content": "Getting started", "level": 2 },
{ "type": "paragraph", "content": "Blog automation starts with a clear <strong>content pipeline</strong>." },
{
"type": "image",
"src": "https://blog-maker.com/images/articles/pipeline-hero.webp",
"alt": "A content pipeline diagram",
"caption": "The pipeline, end to end.",
"ai_origin": "generated"
},
{ "type": "list", "items": ["Plan", "Write", "Review", "Publish"], "ordered": true },
{
"type": "faq",
"questions": [
{ "question": "How long does this take?", "answer": "Most brands are live within a day." }
]
}
]
}
},
"source": "seo_blog_generator",
"source_id": "8421"
}
Block types to render: heading-2 … heading-6, paragraph, list, blockquote, callout, table, image, divider, iframe, faq, html. Full
field list per type:
heading-2 … heading-6: content, level
paragraph: content
list: items[], ordered
blockquote: content
callout: content
table: content
image: src, alt, caption, ai_origin
divider: (none)
iframe: src
faq: questions[] ({question, answer})
html: content
Special case: an "image" block with "ai_origin": "generated" is a legally
required EU AI Act Art. 50(4) disclosure and must render with this exact
badge markup and CSS (class names must stay exactly as given):
<span class="ai-origin-frame">
<img src="..." alt="..." loading="lazy">
<span class="ai-origin-badge">AI-generated</span>
</span>
<!-- small thumbnails (listing cards, related grids): -->
<span class="ai-origin-frame">
<img src="..." alt="..." loading="lazy">
<span class="ai-origin-badge ai-origin-badge--micro">AI</span>
</span>
.ai-origin-frame { position: relative; display: block; line-height: 0; }
.ai-origin-frame img { display: block; width: 100%; height: auto; margin: 0; }
.ai-origin-badge {
position: absolute;
left: 10px;
bottom: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 9px 4px 5px;
border-radius: 999px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
color: #fff;
font-size: 10.5px;
font-weight: 500;
line-height: 1.3;
letter-spacing: 0.01em;
white-space: nowrap;
pointer-events: none;
}
.ai-origin-badge::before {
content: "";
width: 13px;
height: 13px;
flex: 0 0 auto;
background-image: url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 566.93 566.93'%3E%3Cdefs%3E%3Cstyle%3E .cls-1 { fill: %23fff; fill-rule: evenodd; } .cls-2 { fill: %231d1d1b; } %3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M272.03,100.72c100.92,0,182.74,81.82,182.74,182.75s-81.82,182.74-182.74,182.74-182.75-81.82-182.75-182.74,81.82-182.75,182.75-182.75'/%3E%3Cg%3E%3Cpath class='cls-2' d='M170.79,353.74c-1.08,0-2.05-.43-2.92-1.31-.88-.87-1.31-1.84-1.31-2.92,0-.67.07-1.27.2-1.81l47.34-129.32c.4-1.48,1.24-2.79,2.52-3.93,1.27-1.14,3.05-1.71,5.34-1.71h29.81c2.28,0,4.06.57,5.34,1.71,1.27,1.14,2.11,2.45,2.52,3.93l47.14,129.32c.27.54.4,1.14.4,1.81,0,1.08-.44,2.05-1.31,2.92s-1.91,1.31-3.12,1.31h-24.78c-2.01,0-3.52-.5-4.53-1.51-1.01-1.01-1.65-1.91-1.91-2.72l-7.86-20.55h-53.78l-7.65,20.55c-.27.81-.88,1.71-1.81,2.72-.94,1.01-2.55,1.51-4.83,1.51h-24.78ZM218.13,299.96h37.47l-18.93-53.18-18.53,53.18Z'/%3E%3Cpath class='cls-2' d='M328.11,353.74c-1.48,0-2.69-.47-3.63-1.41-.94-.94-1.41-2.15-1.41-3.63v-130.93c0-1.48.47-2.68,1.41-3.63s2.15-1.41,3.63-1.41h26.99c1.48,0,2.68.47,3.63,1.41.94.94,1.41,2.15,1.41,3.63v130.93c0,1.48-.47,2.69-1.41,3.63-.94.94-2.15,1.41-3.63,1.41h-26.99Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.ai-origin-badge--micro {
left: 6px;
bottom: 6px;
padding: 2px 6px 2px 5px;
gap: 4px;
background: rgba(10, 14, 20, 0.55);
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
opacity: 1;
font-size: 9px;
font-weight: 700;
border-radius: 999px;
}
.ai-origin-badge--micro::before {
width: 11px;
height: 11px;
}
/* Add the --right modifier class below when your layout needs the badge in
the bottom-right corner instead (e.g. a hero with a headline overlay on
the left). Icon, size and colors stay identical, only the corner flips. */
.ai-origin-badge.ai-origin-badge--right { left: auto; right: 10px; }
Expected success response:
{
"ok": true,
"id": "post-8421",
"url": "https://example.com/blog/how-to-set-up-blog-automation"
}
Please also handle: wrong/missing Bearer token -> 401, malformed JSON body
-> 400, and make repeated POSTs with the same "slug" update the existing
article instead of creating duplicates.Maschinenlesbare Version
Eine reine Textkopie dieses Contracts liegt unter einer stabilen URL, gedacht zum direkten Abruf durch einen Agenten: https://blog-maker.com/docs/content-blocks.md