API-Referenz

Seiten

Seiten bilden Bäume. Jede Root-Seite (parent_id: null, Pfad /) ist eine eigene Website; welche ausgeliefert wird, entscheidet die aufgerufene Domain (domains), sonst die Standard-Website (default_site). Root-Seiten lassen sich nicht verschieben; gelöscht werden kann jede, auch die letzte (samt ihrer Seiten). Eine Instanz ohne Website – so startet ein neues Binary – liefert auf jedem Pfad 404. Der Pfad einer Seite ergibt sich aus den Slugs ihrer Vorfahren: /produkte/schuhe/sneaker. Pfade sind nur innerhalb einer Website eindeutig – root_id sagt, zu welcher eine Seite gehört.

Seiten-Objekt

{
  "id": 2,
  "parent_id": 1,
  "slug": "blog",
  "title": "Blog",
  "html": "<main>{% for p in data(\"blog\") %}…{% endfor %}</main>",
  "head": "<meta property=\"og:title\" content=\"{{ page.title }}\">",
  "description": "Neuigkeiten",
  "meta_title": "{% if route.slug %}{{ item(\"blog\", \"slug\", route.slug).titel }} – {% endif %}Blog",
  "html_attrs": "",
  "lang": "de",
  "effective_lang": "de",
  "translation_group": "k3j9q2xw7m",
  "body_attrs": "class=\"blog\"",
  "published": true,
  "catch_all": true,
  "no_cache": false,
  "sitemap_exclude": false,
  "noindex": false,
  "nofollow": false,
  "hide_in_nav": false,
  "source_url": "https://www.firma.de/blog",
  "import_root": 1,
  "local_modified": false,
  "root_id": 1,
  "domains": "",
  "default_site": false,
  "notfound_page_id": 0,
  "favicon_file_id": 0,
  "og_image_file_id": 0,
  "robots_txt": "",
  "sort": 0,
  "created_at": 1791191665,
  "updated_at": 1791191700,
  "path": "/blog",
  "assets": [
    { "asset_id": 1, "inherit": true, "name": "base", "kind": "css" },
    { "asset_id": 4, "inherit": false, "name": "blog", "kind": "js" }
  ],
  "blocks": []
}
Feld Typ Beschreibung
parent_id int | null Elternseite; null nur bei der Startseite
slug string URL-Segment: a-z, 0-9, ., _, -; muss mit Buchstabe/Ziffer beginnen. Eindeutig unter Geschwistern. Startseite: immer ""
title string Pflichtfeld; Name der Seite im Seitenbaum, in nav()/children() und Standard für <title>
meta_title string Optionaler SEO-Titel für <title>, wird als Template ausgewertet (z. B. Artikeltitel bei Catch-All-Seiten). Leer = title. Im Template als page.meta_title verfügbar
html string Seiteninhalt als pongo2-Template. Wird in <body> eingesetzt. Enthält er selbst ein <html>-Element, wird er unverändert als ganzes Dokument genutzt und CSS/JS werden vor </head> bzw. </body> eingefügt
head string Zusätzliches Template für <head>
description string Wird als <meta name="description"> ausgegeben; Template-Syntax möglich
html_attrs string Attribute des <html>-Elements, roh ausgegeben (kein Template). Ohne lang= ergänzt IllusionCMS lang="<effective_lang>"; ein eigenes lang= gewinnt. Webflow-Importe setzen hier z. B. data-wf-page/data-wf-site
lang string Eigene Sprache der Seite (BCP 47, klein normalisiert, z. B. de, en, de-at); leer = erbt von der nächsten Seite darüber. PUT ohne Feld = unverändert. Ungültig: 400 translation.langInvalid. Siehe Sprachfassungen
effective_lang string Nur lesend: wirksame Sprache (eigene, geerbte oder de) – im Template page.lang
translation_group string Nur lesend: Übersetzungsgruppe (leer = keine); geändert über /pages/{id}/translations
body_attrs string Attribute des <body>-Elements, roh ausgegeben
published bool Unveröffentlichte Seiten (Entwürfe) liefern im Frontend 404 (außer für Admins mit ?preview=1). Ein Entwurf blendet nur sich selbst aus – seine Unterseiten bleiben erreichbar, z. B. ein Ordner /blog ohne eigene Seite über /blog/mein-artikel
catch_all bool Die Seite beantwortet auch nicht existierende Unterpfade. Der Rest steht im Template als route.slug / route.rest bereit – ideal für Detailseiten. Existiert der Eintrag nicht, liefert {{ not_found() }} die 404-Seite
no_cache bool Seite nie im Seiten-Cache halten, sondern bei jedem Aufruf neu rendern
sitemap_exclude bool Seite nicht in /sitemap.xml aufführen (z. B. Danke-Seiten, Landingpages)
noindex bool Gibt <meta name="robots" content="noindex"> aus; die Seite fehlt außerdem in /sitemap.xml
nofollow bool Gibt <meta name="robots" content="nofollow"> aus (zusammen mit noindex: noindex, nofollow)
sitemap_source object | null Nur Catch-All: Quelle der Detailseiten für /sitemap.xml und den statischen Export. null/fehlt = automatisch (Collections über item(…, route.slug)). IllusionDB: { "type": "illusiondb", "connection": "shop", "table": "landingpages", "column": "slug", "lastmod": "geaendert", "view": "", "rules": [{ "col": "aktiv", "op": "eq", "val": "true" }], "match": "and" } (rules/op wie im Workflow-Node, ohne Platzhalter; view = gespeicherte Ansicht als Filter). Workflow: { "type": "workflow", "path": "orte", "column": "slug", "lastmod": "" } – der GET-Eingang bekommt sitemap=1 und liefert eine Liste von Objekten oder Texten (oder { "records": [...] }). column Standard slug, Werte mit / ergeben mehrstufige Pfade; lastmod optional (ISO-Datum oder Unix-Zeit). Bei IllusionDB werden nur diese beiden Spalten abgefragt (?fields=). Ungültig: 400
hide_in_nav bool Seite fehlt in den Template-Funktionen nav()/children() (mit nav(true)/children(id, true) dabei), bleibt aber erreichbar und in der Sitemap
source_url string Nur lesend: Herkunft bei importierten Seiten
import_root int Nur lesend: Zielseite des Imports, aus dem die Seite stammt
root_id int Nur lesend: Website (Root-Seite), zu der die Seite gehört
renders_blocks bool Nur lesend, nur in der Antwort auf einzelne Seiten (GET/POST/PUT /pages/{id}): Gibt das Template der Seite samt extends/include einen Blockbereich ({% blocks page.blocks %}) aus? Statische Auswertung wie bei has_blocks der Seitenlayouts. Der Editor nutzt es für den Hinweis im Reiter Blöcke
domains string Nur Root-Seiten: Domains der Website, eine pro Zeile. Wird normalisiert (klein, ohne Schema/Port/Pfad); eine Domain gehört höchstens zu einer Website (409)
default_site bool Nur Root-Seiten: Standard-Website für alle Domains ohne eigene Website. Setzen mit true nimmt die Markierung anderen Websites weg. Ohne Markierung gilt die älteste Website
notfound_page_id int Nur Root-Seiten: Seite dieser Website, die als 404-Seite gerendert wird (0 = automatisch: Seite mit Slug 404 direkt unter der Startseite). Muss eine Unterseite derselben Website sein, sonst 400; bei Unterseiten ignoriert. Ist die Seite ein Entwurf oder gelöscht, greift die Automatik
og_image_file_id int Vorschaubild für Link-Vorschauen (og:image) – ID einer Bilddatei, 0 = das der nächsten Seite darüber. Jede Seite bekommt automatisch Open-Graph- und twitter:card-Tags (Titel, Beschreibung, absolute URL über die erste Domain der Website oder die Anfrage); Tags, die schon im Head-Feld/HTML stehen, werden nicht doppelt ausgegeben. Keine Bilddatei: 400
favicon_file_id int Nur Root-Seiten: Website-Icon – ID einer Bilddatei aus der Dateiverwaltung (SVG, PNG, ICO …; 0 = keins). Jede Seite der Website bekommt <link rel="icon"> (große PNG/JPEG ab 120 px zusätzlich apple-touch-icon), /favicon.ico liefert die Datei. Keine Bilddatei: 400; bei Unterseiten ignoriert
robots_txt string Nur Root-Seiten: eigener Inhalt für /robots.txt. Leer = automatisch (User-agent: * / Allow: /). Fehlt eine Sitemap:-Zeile, wird sie mit der aufgerufenen Domain ergänzt
local_modified bool Nur lesend: Seite wurde nach dem Import im CMS bearbeitet (Vergleich des Inhalts mit dem Import-Stand; bei nie importierten Seiten: überhaupt schon bearbeitet)
sort int Position unter den Geschwistern (wird über /move gesetzt)
path string Berechneter URL-Pfad (nur lesend)
assets array CSS/JS-Zuordnungen in Ladereihenfolge (siehe unten)
blocks array Seitenaufbau aus Blöcken (Blockbaum), im Template per {% blocks page.blocks %} ausgegeben. Ohne Blöcke []; fehlt in GET /pages (Liste ohne Inhalte). PUT ohne blocks lässt den Baum unverändert, sonst wird er geprüft und normalisiert (400 mit block.*-Codes). Versioniert und im Bundle

Reservierte Slugs auf oberster Ebene (direkt unter der Startseite): cms-admin, api, media, _assets, sitemap.xml.

Asset-Zuordnungen

Feld Typ Beschreibung
asset_id int ID eines CSS/JS-Assets
inherit bool true = wird auch auf allen Unterseiten geladen
name, kind string Nur lesend, zur Anzeige

Beim Rendern werden zuerst die vererbten Assets der Vorfahren (von der Startseite abwärts) und dann die eigenen geladen; Duplikate werden entfernt. CSS landet im <head>, JS am Ende von <body>.

GET /pages 🔑

Alle Seiten als flache Liste, sortiert nach Eltern und sort – ohne html und head (für Baumansichten). assets ist enthalten. Den Baum baust du über parent_id auf.

Mit ?roots=1 kommen nur die Root-Seiten, also die Websites.

GET /pages/{id} 🔑

Eine Seite inklusive html, head und assets.

POST /pages ✏️

Legt eine Unterseite an (am Ende der Geschwisterliste).

{
  "parent_id": 1,
  "title": "Über uns",
  "slug": "ueber-uns",
  "html": "<main><h1>{{ page.title }}</h1></main>",
  "published": true,
  "assets": [{ "asset_id": 1, "inherit": true }]
}

Pflicht: parent_id, title, slug. Antwort 201 mit dem vollständigen Seiten-Objekt.

Mit layout_id statt html entsteht die Seite aus einem Seitenlayout: Ihr HTML ist dann {% extends "<name des layouts>" %} (auch im Batch unter data). layout_id und ein nicht leeres html zusammen lehnt der Server ab (400 page.layoutWithHTML), ein Snippet ohne Kennzeichen Seitenlayout ebenso (400 page.layoutUnknown).

Fehler: 400 ungültiger/reservierter Slug, 404 Elternseite fehlt, 409 Slug unter Geschwistern vergeben.

PUT /pages/{id} ✏️

Merge-Update. Ist assets im Body enthalten, werden die Asset-Zuordnungen vollständig ersetzt; fehlt das Feld, bleiben sie unverändert. parent_id und sort werden hier ignoriert – zum Verschieben dient /move.

{ "title": "Über uns", "html": "<main>…</main>", "published": true }

Antwort 200 mit dem aktualisierten Objekt.

DELETE /pages/{id} ✏️

Löscht die Seite inklusive aller Unterseiten. Bei einer Root-Seite wird die ganze Website gelöscht – auch die letzte. 204.

POST /sites ✏️

Legt eine neue Website (Root-Seite mit Standard-HTML, veröffentlicht) an.

{ "title": "Shop", "domains": "shop.firma.de\nwww.shop.firma.de" }

Antwort 201 mit dem Seiten-Objekt der neuen Root-Seite. 409, wenn eine Domain schon zu einer anderen Website gehört.

GET /sites/{id}/static 🔑

Exportiert die Website {id} (Root-Seite) als statische Website: fertige HTML-Dateien mit CSS, JS und Medien als ZIP – zum Entpacken ins Wurzelverzeichnis eines Webservers wie Apache.

  • Seiten: Gerendert werden alle veröffentlichten Seiten des Baums. Dazu kommen die Detailseiten hinter Catch-All-Seiten, die auch in der Sitemap stehen (Collections bzw. sitemap_source, z. B. Tausende Landingpages aus IllusionDB). Zusätzlich folgt der Export allen internen Links (<a href>), so kommen auch andere verlinkte Detailseiten mit (z. B. /blog/mein-artikel). Jede Seite wird zu <pfad>/index.html (/ → index.html); Pfade mit Dateiendung wie /feed.xml bleiben Dateien. Apache leitet /pfad dann auf /pfad/ um.
  • Weitere Dateien: 404.html (die 404-Seite der Website), .htaccess (ErrorDocument 404 /404.html, keine Verzeichnislisten, die Weiterleitungen der Website als RedirectMatch – mod_alias genügt), robots.txt, alle CSS/JS-Assets unter _assets/…, sitemap.xml (wenn eine Basis-URL bekannt ist; bei mehr als 50 000 Adressen als Index mit sitemap-1.xml, sitemap-2.xml, …) und die Medien unter media/….
  • URLs bleiben unverändert (absolute Pfade ab /) – der Export gehört ins Wurzelverzeichnis der Domain, nicht in einen Unterordner.
  • Query-Varianten (?seite=2), Formulare an das CMS, /api/v1/…-Aufrufe aus JavaScript und REST-Daten nach dem Export funktionieren statisch nicht; REST-Daten werden mit dem Stand beim Export eingefroren.
Query Bedeutung
dry=1 Nur prüfen: liefert den Bericht als JSON statt des ZIPs
media=all Alle Dateien der Medienverwaltung; Standard: nur die, die in den gerenderten Seiten oder in CSS/JS vorkommen (inklusive srcset-Varianten)
base Basis-URL für sitemap.xml, z. B. https://www.firma.de. Leer = erste Domain der Website mit https://, ohne Domain keine Sitemap

Bericht (dry=1):

{
  "pages": ["/", "/blog", "/blog/hallo", "/kontakt"],
  "errors": {},
  "broken_links": { "/kaputt": "/" },
  "media_files": 29, "media_bytes": 7900000, "missing_media": [],
  "asset_files": 11, "base": "https://www.firma.de", "too_many": false,
  "private_media": 0
}

errors: Seiten, die sich nicht rendern lassen – dann liefert der Export ohne dry 422 mit { "error", "report" } statt eines unvollständigen ZIPs. broken_links: interne Links ohne Ziel (Pfad → Seite mit dem Link). missing_media: verwendete, aber nicht vorhandene Dateien. private_media: weggelassene Dateien privater Medien (Original, Varianten, WebP) – private Dateien fehlen immer, auch mit media=all. Über 20.000 Seiten bricht das Verfolgen ab (too_many).

PUT /pages/{id}/assets ✏️

Ersetzt nur die Asset-Zuordnungen. Body ist ein Array in Ladereihenfolge:

[
  { "asset_id": 1, "inherit": true },
  { "asset_id": 4, "inherit": false }
]

Antwort 200 mit dem Seiten-Objekt.

POST /pages/{id}/move ✏️

Verschiebt eine Seite im Baum.

{ "parent_id": 5, "index": 0 }
Feld Beschreibung
parent_id Neue Elternseite
index Position unter den neuen Geschwistern (0 = erste). Ungültige Werte (< 0 oder zu groß) hängen ans Ende

Antwort 200: die komplette Seitenliste wie bei GET /pages.

Fehler: 400 Startseite verschieben, Verschieben in den eigenen Teilbaum, reservierter Slug auf oberster Ebene; 409 Slug unter den neuen Geschwistern vergeben.

POST /pages/{id}/preview 🔑

Rendert eine Seite mit ungespeicherten Inhalten – genutzt für die Live-Vorschau im Admin. Alle Felder sind optional; fehlende werden aus der gespeicherten Seite genommen.

{
  "html": "<h1>Entwurf</h1>",
  "head": "",
  "title": "Neuer Titel",
  "description": "",
  "assets": [{ "asset_id": 1, "inherit": true }],
  "overrides": { "1": "body { background: hotpink }" },
  "query": { "seite": "2" },
  "rest": ["mein-artikel"],
  "blocks": [{ "id": "b1", "type": "hero", "params": { "titel": "Ungespeichert" } }],
  "item": { "collection": "blog", "id": 12, "data": { "titel": "Ungespeichert", "slug": "mein-artikel" } }
}
Feld Beschreibung
assets Ungespeicherte Asset-Zuordnungen dieser Seite
overrides Ungespeicherter Inhalt von Assets (asset_id → Inhalt)
query Simulierte Query-Parameter (route.query)
rest Simulierter Unterpfad bei Catch-All-Seiten (route.rest)
blocks Ungespeicherter Blockbaum der Seite (page.blocks) – ungeprüft; unbekannte Bausteine erscheinen als Hinweis-Kasten
item Ungespeicherter Collection-Eintrag: ersetzt den Eintrag id (bzw. kommt mit id: 0 neu dazu) in data()/item() – für die Vorschau beim Bearbeiten eines Eintrags auf seiner Detailseite. Blöcke-Felder darin werden wie gespeicherte gerendert

In der Vorschau werden alle CSS/JS-Assets inline ausgegeben und auch unveröffentlichte Collection-Einträge berücksichtigt; Blöcke tragen Vorschau-Markierungen. Adressen privater Dateien aus image()/file() (auch Website-Icon und og:image) sind signiert (?exp=…&sig=…, ein bis zwei Stunden gültig), damit sie im iframe ohne Cookies laden – ebenso in Vorschau-Links und bei ?preview=1, nie auf Live-Seiten.

Antwort: 200 text/html mit dem fertigen Dokument, bei Template-Fehlern 422 text/html mit einer Fehlerseite. Ruft das Template not_found() auf, kommt 200 mit einem Hinweis (bei Catch-All-Seiten mit vorhandenen Unterpfaden).

POST /pages/{id}/sitemap-preview ✏️

Prüft eine (auch ungespeicherte) Sitemap-Quelle der Seite: Body { "sitemap_source": { … } }, Antwort { "count": 3920, "sample": [{ "path": "/leistungen/berlin", "lastmod": "2026-09-02" }, …], "duration_ms": 46 } (die ersten 10). Fehler der Quelle (IllusionDB nicht erreichbar, Workflow fehlt): 502. Leert den Seiten-Cache nicht.

POST /pages/{id}/template-data 🔑

Liefert, was ein Template der Seite sieht – für das Panel Template-Daten im Editor. Optional mit ungespeichertem title, meta_title, description sowie query und rest wie bei der Vorschau; gerechnet wird wie in der Vorschau (Entwürfe sichtbar). Leert den Seiten-Cache nicht.

{
  "variables": { "page": { "id": 2, "title": "Blog", "meta_title": "Blog | Firma", … }, "route": { … }, "site": { … }, "today": "2026-10-08", "now": "2026-10-08T14:03:00+02:00", "preview": true },
  "nav": [{ "id": 2, "title": "Blog", "path": "/blog", … }],
  "children": [],
  "collections": [{ "slug": "blog", "name": "Blog", "fields": [{ "name": "titel", "type": "text" }], "count": 42, "items": [{ "id": 1, "titel": "…", "_created": 1791457748, "_updated": 1791457748 }] }],
  "sources": [{ "slug": "wetter", "name": "Wetter", "url": "https://…" }]
}

variables enthält nur Werte, keine Funktionen. items zeigt die ersten 20 Einträge so, wie data("slug") sie liefert, count die Gesamtzahl. REST-Quellen werden nicht abgerufen (das Admin holt sie auf Klick über GET /data/{slug}). Antwort 422 bei Fehlern im SEO-Titel oder in der Beschreibung.

POST /pages/{id}/reimport ✏️

„Aus Quelle aktualisieren“: lädt eine importierte Seite erneut von source_url – mit den Download-Hosts des letzten Imports dieser Quelle in ihre Website (ohne gespeichertes Profil: abgeleitet aus den Hosts der bereits importierten Dateien der Quelle). Die Seite wird auch dann geschrieben, wenn sich die Quelle nicht geändert hat; Dateien und CSS/JS werden wie beim Re-Import wiederverwendet.

{ "overwrite_modified": true }

overwrite_modified ist nötig, wenn die Seite im CMS bearbeitet wurde (local_modified: true) – sonst bleibt sie unverändert. Antwort 202 mit einem Import-Job.

GET /pages/{id}/render 🔑

Rendert die gespeicherte Seite und liefert das HTML als JSON – praktisch für Headless-Szenarien oder statische Exporte. Query-Parameter der Anfrage werden als route.query durchgereicht.

{ "html": "<!doctype html>…", "path": "/blog" }

422 mit { "error": "HTML: … line 3 …" } bei Template-Fehlern.

GET /pages/{id}/preview-links – gültige Links der Seite. POST /pages/{id}/preview-links mit { "days": 7 } (1–90, Standard 7) legt einen an (201). DELETE /preview-links/{id} widerruft ihn (204).

{ "id": 3, "page_id": 12, "token": "9faf061b27c6…", "author": "erik", "created_at": 1791368754, "expires_at": 1791973554 }

Aufruf: <Seitenpfad>?ic_preview=<token> – rendert die Seite des Links oder eine darunter (auch Catch-All-Details) mit Entwürfen (Seiten und Collection-Einträge), ohne Anmeldung, Cache-Control: no-store, X-Robots-Tag: noindex, nofollow, ohne Statistik. Die Website ergibt sich aus dem Link, nicht aus der Domain. Ungültig, abgelaufen oder fremde Seite: 404. Links gehören zur Instanz (nicht im Bundle).

GET /pages/{id}/mhtml 🔑

Die gespeicherte Seite als MHTML-Archiv (multipart/related, Download als <slug>-<datum>.mhtml) – Seite plus Stylesheets, Bilder, Schriften und Icons in einer Datei, auch von fremden Hosts. Skripte werden nicht eingepackt (Browser führen sie in MHTML nicht aus), srcset/sizes/loading und <picture><source> werden entfernt. Die Verweise im HTML bleiben unverändert; aufgelöst wird über die Content-Location der Teile – Basis ist die erste Domain der Website (https://…), sonst die Adresse der Anfrage.

Grenzen: 25 MB je Datei, 250 MB insgesamt, 1.000 Teile. Was nicht passt oder nicht abrufbar ist, fehlt im Archiv. Die Antwort-Header X-IC-MHTML-Parts und X-IC-MHTML-Skipped nennen die Anzahl eingepackter und ausgelassener Dateien.

curl -OJ "https://example.com/api/v1/pages/12/mhtml?token=$TOKEN"

422 bei Template-Fehlern.

Sprachfassungen

Mehrsprachige Websites bestehen aus eigenen Unterbäumen (/en/…) oder eigenen Websites (firma.de ↔ firma.com). Damit Suchmaschinen und Sprachumschalter wissen, welche Seiten dieselben Inhalte in anderer Sprache sind, verknüpfst du sie zu einer Übersetzungsgruppe.

  • Sprache einer Seite: Feld lang (eigene Sprache, BCP 47, z. B. de, en, de-at; wird klein geschrieben, ungültig → 400 translation.langInvalid). Die wirksame Sprache effective_lang ist die eigene oder die der nächsten Seite darüber mit lang, sonst de. Sie steht im <html lang="…"> (außer html_attrs setzt selbst lang=) und im Template als page.lang.
  • Gruppenregeln: Eine Gruppe enthält nur Seiten oder nur Einträge, je Sprache höchstens ein Mitglied (bei Seiten die wirksame Sprache). Seiten verschiedener Websites dürfen in eine Gruppe. Ändern der Sprache oder Verschieben einer Seite, das in einer Gruppe (der Seite oder einer Unterseite) eine Sprache doppelt belegen würde, gibt 409 translation.langTaken; ebenso das Wiederherstellen einer Version mit anderer Sprache.
  • Ausgabe: Hat die gerenderte Seite eine Gruppe mit mindestens zwei veröffentlichten Mitgliedern, schreibt IllusionCMS je Mitglied <link rel="alternate" hreflang="<sprache>" href="<absolute URL>"> in den <head> (inklusive der Seite selbst) und hreflang="x-default" für das Mitglied in der Sprache der eigenen Website, wenn alle Mitglieder in derselben Website liegen (Sprach-Unterbäume), sonst in der Sprache der Standard-Website. Absolute URLs über die erste Domain der jeweiligen Website, für die eigene Website ersatzweise über die Anfrage (wie bei Open Graph); Mitglieder ohne absolute URL fallen weg. Steht im Head-Feld oder HTML schon ein <link … hreflang>, entfällt die automatische Ausgabe. Catch-All-Detailseiten nutzen die Gruppe des angezeigten Eintrags (erkannt über item(…, route.slug) oder die Detailseite der Collection). /sitemap.xml ergänzt dieselben Angaben als <xhtml:link> (auch im Sitemap-Index und im statischen Export).
  • translation_group steht nur lesend an Seiten und Einträgen; PUT ignoriert es. Neue Seiten/Einträge (auch per CSV-Import) haben keine Gruppe. Gruppen kommen im Bundle mit. Löschen räumt auf: Gruppen mit nur noch einem Mitglied werden geleert.

Sprachfassungs-Objekt (Antwort aller Endpunkte, sortiert nach Sprachcode, inklusive des angefragten Objekts mit current: true):

[
  { "id": 3, "lang": "de", "title": "Kontakt", "path": "/kontakt", "site_id": 1, "url": "/kontakt", "published": true, "current": true },
  { "id": 9, "lang": "en", "title": "Contact", "path": "/contact", "site_id": 4, "url": "https://www.company.com/contact", "published": true, "current": false }
]
Feld Beschreibung
lang Wirksame Sprache der Seite bzw. Sprache der Collection
path, site_id Pfad innerhalb der Website und Website (Root-Seite); bei Einträgen die Detailseite der Collection, ohne Detailseite ""/0
url Absolut über die erste Domain der Website, ohne Domain = path
collection Nur bei Einträgen: Slug der Collection
Endpunkt Zugriff Beschreibung
GET /pages/{id}/translations 🔑 Sprachfassungen der Seite (ohne Gruppe nur die Seite selbst)
POST /pages/{id}/translations ✏️ Body { "page_id": 9 } – nimmt Seite 9 in die Gruppe von {id} auf (legt sie bei Bedarf an). Mit "move": true wechselt Seite 9 aus ihrer bisherigen Gruppe. Antwort 200 mit der Liste
DELETE /pages/{id}/translations ✏️ Löst die Seite aus ihrer Gruppe (bleibt nur ein Mitglied, wird auch dessen Gruppe geleert). Antwort 200 mit der Liste
Code Status Bedeutung
translation.langInvalid 400 Ungültiger Sprachcode (lang)
translation.targetRequired 400 page_id bzw. item_id fehlt
translation.sameLang 400 Gleiche Sprache wie das Objekt selbst (lang)
translation.alreadyLinked 409 Ziel ist schon in einer anderen Gruppe (title) – mit "move": true verschieben
translation.langTaken 409 Die Gruppe hat schon ein Mitglied in dieser Sprache (lang)
translation.noLang 400 Einträge: Collection ohne Sprache (collection)
translation.collectionLinked 409 Sprache einer Collection entfernen, deren Einträge verknüpft sind (count = Gruppen)