Collections
Collections sind eigene, frei definierbare Datenstrukturen (Blog, Team, Produkte, FAQ …), die in Templates über data("slug") verwendet werden.
Überall, wo {ref} steht, kannst du ID oder Slug verwenden: /collections/3/items ≙ /collections/blog/items.
Collection-Schema
{
"id": 1,
"slug": "blog",
"name": "Blog",
"folder": "Website A",
"public_api": true,
"live": false,
"singleton": false,
"detail_page_id": 7,
"detail_field": "slug",
"lang": "de",
"fields": [
{ "name": "titel", "label": "Titel", "type": "text", "required": true },
{ "name": "slug", "label": "URL", "type": "text", "required": true },
{ "name": "datum", "label": "Datum", "type": "date", "required": false },
{ "name": "bild", "label": "Bild", "type": "image", "required": false },
{ "name": "text", "label": "Text", "type": "markdown", "required": false },
{ "name": "kategorie", "label": "Kategorie", "type": "select", "required": false, "options": ["News", "Technik"] },
{ "name": "autor", "label": "Autor", "type": "reference", "required": false, "collection": 2 },
{ "name": "tags", "label": "Tags", "type": "references", "required": false, "collection": 3 }
],
"created_at": 1791191665,
"updated_at": 1791191665,
"item_count": 12
}
| Feld | Beschreibung |
|---|---|
slug |
a-z0-9_-, beginnt mit Buchstabe. Eindeutig über Collections und REST-Quellen |
folder |
Ordner in der Admin-Liste, nur zur Übersicht (leer = ohne) |
public_api |
true = Lese-Endpunkte ohne Authentifizierung erreichbar (nur veröffentlichte Einträge), auch cross-origin (CORS: Access-Control-Allow-Origin: *) |
live |
true = Live-Daten: Die Einträge entstehen auf dem Live-Server (z. B. per Workflow aus einem Formular) und bleiben beim Einspielen eines Bundles erhalten |
singleton |
true = genau ein Eintrag (Website-Einstellungen, Farbcodes …): GET …/items, /data/{slug} und data() im Template liefern ihn als Objekt (bzw. null) statt als Liste. Ein zweiter Eintrag (auch per Batch) gibt 409, der CSV-Import 400; anlegen und ändern am einfachsten mit PUT …/item. Umschalten geht nur mit höchstens einem Eintrag (409) |
detail_page_id |
Detailseite: ID einer (Catch-All-)Seite, unter der die Einträge erscheinen – Pfad = Seite + / + Wert von detail_field. Das Admin zeigt beim Bearbeiten eines Eintrags die Vorschau dieser Seite. 0 = keine; unbekannte Seite: 400 |
detail_field |
Feld mit dem URL-Teil (Standard slug, muss ein Feld sein – sonst 400) |
lang |
Sprache aller Einträge (BCP 47, z. B. de, en; leer = ohne Sprache). Voraussetzung, um Einträge als Sprachfassungen zu verknüpfen |
fields[].name |
Feldname im Template (item.titel): [a-zA-Z_][a-zA-Z0-9_]*, eindeutig, nicht id |
fields[].label |
Anzeigename im Admin (Standard: name) |
fields[].required |
Pflichtfeld |
fields[].options |
Erlaubte Werte bei select |
fields[].collection |
Bei reference/references: ID der verknüpften Collection (muss existieren, auch die eigene) |
item_count |
Anzahl Einträge (nur lesend) |
Feldtypen
| Typ | Gespeichert als | Hinweise |
|---|---|---|
text |
string | |
textarea |
string | Mehrzeilig |
html |
string | Im Template mit |safe ausgeben |
markdown |
string | Markdown-Quelltext; im Template mit |markdown als HTML ausgeben. Die API liefert den Quelltext |
number |
number | Strings wie "3.5" werden konvertiert |
boolean |
bool | Fehlt der Wert, wird false gespeichert |
date |
string | Empfohlen: ISO YYYY-MM-DD (sortierbar) |
url |
string | |
select |
string | Muss einer der options sein (wenn welche definiert sind) |
image |
number | Datei-ID → im Template {{ image(item.bild) }} |
file |
number | Datei-ID → im Template file(item.datei).url |
reference |
number | Eintrags-ID der Collection collection. Auch als String |
references |
number[] | Liste von Eintrags-IDs, auch als String "1, 2". Doppelte fallen weg, die Reihenfolge bleibt |
blocks |
array | Blockbaum (auch als JSON-Text). Beim Anlegen und bei Teil-Updates, die das Feld enthalten, streng geprüft (400 mit block.*-Code und params.field). Im Template {% blocks item.feld %}. CSV-Export als JSON-Text; beim Import werden ungültige Bäume mit Warnung leer gelassen |
Die Items-Endpunkte liefern Verknüpfungen als IDs. Im Template (data(), item()) und über GET /data/{slug} sind sie aufgelöst: Statt der ID steht der verknüpfte Eintrag da, eine Ebene tief. Gelöschte und, ohne Entwürfe, unveröffentlichte Einträge fallen weg. Filter vergleichen die IDs; bei references bedeutet Gleichheit „enthält“ (data("blog", "tags", 3)), __ne „enthält nicht“. Der CSV-Import nimmt IDs oder den Text eines Ziel-Eintrags (bevorzugt slug), der Export schreibt IDs mit Komma.
Leere Werte ("", null, fehlend) werden als null gespeichert. Felder, die nicht im Schema stehen, werden verworfen.
| Endpunkt | Zugriff | Beschreibung |
|---|---|---|
GET /collections |
🔑 | Alle Collections |
POST /collections |
✏️ | Anlegen → 201 |
GET /collections/{ref} |
🔑 / 🌐 bei public_api |
Einzelne Collection |
PUT /collections/{ref} |
✏️ | Merge-Update. fields wird – wenn angegeben – vollständig ersetzt |
DELETE /collections/{ref} |
✏️ | Löscht Collection mit allen Einträgen → 204 |
Hinweis zum Ändern des Schemas: Vorhandene Einträge werden nicht migriert. Werte umbenannter oder entfernter Felder bleiben gespeichert, bis der Eintrag das nächste Mal gespeichert wird – dann werden sie verworfen.
Ohne Authentifizierung liefern nicht-öffentliche Collections 404 (nicht 401), damit ihre Existenz nicht erkennbar ist.
Einträge
Eintrag-Objekt
{
"id": 5,
"collection_id": 1,
"data": {
"titel": "Hallo Welt",
"slug": "hallo-welt",
"datum": "2026-10-05",
"bild": 7,
"text": "Erster **Beitrag**",
"kategorie": "News"
},
"sort": 0,
"published": true,
"translation_group": "",
"created_at": 1791191665,
"updated_at": 1791191665
}
GET /collections/{ref}/items 🔑 / 🌐
Alle Einträge in manueller Reihenfolge (sort). Mit Authentifizierung inklusive Entwürfen, öffentlich nur published: true. Bei einem Singleton der eine Eintrag als Objekt (oder null).
Für Filtern, Sortieren und Limitieren nutze GET /data/{slug}.
GET /collections/{ref}/items/{id} 🔑 / 🌐
Einzelner Eintrag. Öffentlich: Entwürfe liefern 404.
POST /collections/{ref}/items ✏️
{
"data": { "titel": "Hallo Welt", "slug": "hallo-welt", "bild": 7 },
"published": true
}
published ist optional (Standard true). Der Eintrag wird ans Ende gehängt. 201, 400 bei Validierungsfehlern, z. B. field "Titel" is required.
Mehrere Einträge auf einmal: Statt eines Objekts ein Array schicken (max. 1000 Einträge je Anfrage):
[
{ "data": { "titel": "Erster", "slug": "erster" } },
{ "data": { "titel": "Zweiter", "slug": "zweiter" }, "published": false }
]
Alles oder nichts: Ist ein Eintrag ungültig, wird keiner angelegt, die Meldung nennt den Index (ab 0), z. B. item 1: field "Titel" is required. Die Einträge landen in der Reihenfolge des Arrays am Ende. Antwort 201 mit dem Array der angelegten Einträge (inkl. id). Der Seiten-Cache wird einmal geleert, das Ereignis created kommt wie beim Einzel-POST je Eintrag. Ein leeres oder zu großes Array liefert 400.
PUT /collections/{ref}/items/{id} ✏️
Teil-Update: Nur die in data angegebenen Felder werden geändert, der Rest bleibt erhalten.
{ "data": { "titel": "Neuer Titel" }, "published": false }
GET /collections/{ref}/export 🔑
Alle Einträge (inkl. Entwürfe) als CSV, Content-Disposition: attachment; filename="<slug>-<datum>.csv". Spalten: id, published, dann die Felder in Schema-Reihenfolge (Kopfzeile = Feldnamen). UTF-8 mit BOM, Komma als Trennzeichen; Ja/Nein als true/false, Zahlen mit Punkt, Bilder/Dateien als Datei-ID.
POST /collections/{ref}/import ✏️
CSV-Import in eine bestehende Collection. multipart/form-data:
| Feld | |
|---|---|
file |
CSV-Datei (UTF-8, optional mit BOM, max. 32 MB / 50 000 Zeilen). Trennzeichen , ; oder Tab wird aus der Kopfzeile erkannt. |
mapping |
optional, JSON-Array mit einem Eintrag je Spalte: Feldname, "" (ignorieren), "_published" (ja/nein → veröffentlicht), "_draft" (ja/nein → Entwurf) oder "_id" (Eintrags-ID, nur für key=_id). Ohne: Zuordnung nach Feldname/Bezeichnung, Spalte id → _id, Webflow-Spalten Draft/Archived → _draft. |
key |
optional, Abgleich: ein zugeordnetes Feld (z. B. slug) oder _id. Zeilen, deren Wert schon ein Eintrag hat, aktualisieren ihn (nur die zugeordneten Felder; Status nur mit Status-Spalte), sonst neuer Eintrag. Leerer Wert → neuer Eintrag. Fehler: Wert doppelt in der Datei, Wert bei mehreren Einträgen, unbekannte _id. Ohne key werden alle Zeilen angehängt. |
Query: dry_run=1 (nichts schreiben – liefert Spalten, Zuordnung, Vorschau, Fehler und die Zähler), replace=1 (ohne key: alle bisherigen Einträge löschen; mit key: nur die, die in der Datei nicht vorkommen – die übrigen behalten ihre ID). Alles in einer Transaktion; neue Einträge werden vor dem Löschen angelegt, damit keine gelöschte ID neu vergeben wird.
{
"delimiter": ";", "rows": 2, "columns": [{ "name": "Titel", "samples": ["Yoga", "Pilates"] }],
"mapping": ["titel", "preis", "_published", ""], "key": "titel",
"preview": [{ "data": { "titel": "Yoga", "preis": 1234.5 }, "published": true, "action": "update", "id": 12 }],
"errors": [], "error_count": 0,
"warnings": [{ "row": 3, "column": "Bild", "message": "no file for \"https://…\"", "code": "csv.noFileFor", "params": { "value": "https://…" } }], "warning_count": 1,
"created": 1, "updated": 1, "deleted": 0, "imported": 2
}
Umwandlung je Feldtyp: Zahl (1234.5, 1.234,5, 1,234.5; nur Komma = Dezimalkomma), Ja/Nein (true/false, ja/nein, yes/no, 1/0, x), Datum → JJJJ-MM-TT (ISO, TT.MM.JJJJ, RFC 1123, Webflow-Format), Bild/Datei (Datei-ID oder Quell-URL einer importierten Datei – sonst leer mit Warnung), sonst Text. Danach gelten die üblichen Prüfungen (Pflichtfelder, Auswahl-Optionen). row ist die Zeile in der Datei (Kopfzeile = 1), leere Zeilen werden übersprungen.
Ohne dry_run: alles oder nichts in einer Transaktion – bei Fehlern 400 mit error (Code csv.rowError/csv.cellError bei einem Fehler, sonst csv.errors), errors und error_count. Einträge in errors/warnings tragen code und params, soweit vorhanden. Erfolg 200 mit created, updated, deleted und imported (= neu + aktualisiert). Löst keine created-Ereignisse aus (anders als POST …/items). Probeläufe leeren den Seiten-Cache nicht.
POST /collections/import ✏️
Neue Collection aus einer CSV-Datei, sonst wie oben. Formularfelder: file, name (nur für den Vorschlag), optional collection (JSON wie bei POST /collections: name, slug, fields) und mapping. Ohne collection schlägt der Server Slug, Feldnamen und Typen vor (Antwortfeld collection) – Typen aus allen Werten der Spalte: Ja/Nein, Zahl (ohne führende Null), Datum, URL, HTML, Mehrzeiliger Text, Text; Webflow-Verwaltungsspalten und eine Spalte id werden ignoriert. Mit dry_run=1 nur Vorschlag und Vorschau; ohne legt der Server Collection und Einträge an (201, collection enthält die neue id). key und replace gibt es hier nicht.
curl -X POST -H "Authorization: Bearer $TOKEN" -F file=@blog.csv -F name=Blog "https://example.com/api/v1/collections/import"
POST /collections/{ref}/items/reorder ✏️
Setzt die Reihenfolge. Body: Array der Eintrags-IDs in gewünschter Reihenfolge. Nicht aufgeführte Einträge behalten ihren sort-Wert. 204.
[5, 2, 9, 1]
DELETE /collections/{ref}/items/{id} ✏️
204.
PUT /collections/{ref}/item ✏️
Nur für Singletons: legt den einen Eintrag an oder ändert ihn (Teil-Update wie bei PUT …/items/{id}, mit base_updated_at Konfliktprüfung). Body { "data": { "primaerfarbe": "#7c5cff" }, "published": true }, Antwort der Eintrag. Bei anderen Collections 400.
Sprachfassungen von Einträgen
Wie bei Seiten: GET/POST/DELETE /collections/{ref}/items/{id}/translations, verknüpfen mit { "collection": "blog-en", "item_id": 12 } (collection = ID oder Slug, fehlt = dieselbe Collection; optional "move": true). Die Sprache eines Eintrags ist die seiner Collection (lang) – eine Collection hat eine Sprache, für mehrere Sprachen legst du getrennte Collections an (blog + blog-en). Einträge aus Collections ohne Sprache lassen sich nicht verknüpfen (400 translation.noLang). url/path zeigen auf die Detailseite der Collection (detail_page_id + Wert von detail_field), ohne Detailseite leer. Ändern der Collection-Sprache prüft die Gruppen ihrer Einträge (409 translation.langTaken, Entfernen bei verknüpften Einträgen 409 translation.collectionLinked).