Suche

↑↓ auswählenEnter öffnenErweiterte Suche

Anleitungen

ACF-Felder sauber nach EmDash bringen

Der Import aus WordPress bringt alle ACF-Werte mit, aber Repeater sind zerlegt, Bilder und Verweise zeigen auf alte WordPress-IDs. Ich zeige, wie ein kleines Skript daraus echte EmDash-Felder macht, und was dabei noch Handarbeit bleibt.

Von Publiziert am 9 Min. Lesezeitgeprüft mit 1.1 · aktuell 1.2

Geprüft mit EmDash 1.1.0, WordPress 7.1.2, Secure Custom Fields 6.9.5 und dem Exporter-Plugin 1.0.0 am 5. Oktober 2026.

Referenzen mit Kennzahlen, Teamseiten mit Porträts, Projekte mit Bildergalerien: Bei Kundenseiten stecken die Inhalte, die wirklich zählen, oft nicht im Editor, sondern in ACF-Feldern. Bei einem Umzug nach EmDash entscheiden deshalb diese Felder, wie viel Arbeit auf dich zukommt.

Als ich für den Leitfaden für Agenturen eine typische Agentur-Site importiert habe, war ich zuerst erleichtert. Alle ACF-Werte kamen an, nichts fehlte. Beim zweiten Blick kam die Ernüchterung: Zwei Repeater waren in 20 Einzelfelder zerlegt, Bilder waren nur noch alte WordPress-IDs, und das Launch-Datum stand als 20250115 da. Damit kann kein Frontend etwas anfangen.

Im Leitfaden habe ich dafür ein eigenes Import-Skript empfohlen. Weil ich selbst am WordPress-Import von EmDash mitarbeite, wollte ich wissen, wie viel Arbeit das wirklich ist und wo es hakt. Also habe ich das Skript für unsere Test-Site geschrieben. Es hat knapp 270 Zeilen, und am Ende sahen die Referenzen im Admin aus, als hätte es WordPress nie gegeben. Ein paar Dinge bleiben trotzdem Handarbeit, dazu am Schluss mehr.

Die Test-Site

Ich habe dieselbe Site genommen wie im Leitfaden: einen eigenen Beitragstyp referenz mit zwölf Einträgen und einer Feldgruppe, wie ich sie aus Kundenprojekten kenne. Darin stecken Kennzahlen und eine Bildergalerie als Repeater, ein Logo, verwandte Beiträge, ein Launch-Datum, ein WYSIWYG-Feld, ein Schalter für hervorgehobene Referenzen und ein Flexible-Content-Feld. Importiert habe ich mit dem Exporter-Plugin, denn nur dieser Weg nimmt ACF-Felder überhaupt mit.

Statt ACF Pro läuft auf der Test-Site Secure Custom Fields, der kostenlose Ableger auf WordPress.org, der Repeater und Flexible Content mitbringt. Beide speichern die Werte gleich, deshalb gilt alles hier auch für ACF Pro.

Was nach dem Import in EmDash steht

So sieht eine Referenz nach dem Import aus, gekürzt auf die ACF-Felder:

{
	"kennzahlen": 2,
	"kennzahlen_0_label": "Besucher",
	"kennzahlen_0_wert": "+40%",
	"kennzahlen_1_label": "Ladezeit",
	"kennzahlen_1_wert": "0,8 s",
	"galerie": 2,
	"galerie_0_bild": 1692,
	"galerie_0_text": "<p>Bild <strong>eins</strong></p>",
	"logo": 1687,
	"verwandt": [1, 163],
	"launch": 20250115,
	"featured": 1,
	"beschreibung": "<p>WYSIWYG <em>Text</em> 1</p>…"
}

Wer schon einmal in die Tabelle wp_postmeta geschaut hat, erkennt das sofort. Genau so legt ACF die Werte in WordPress ab, und der Importer übernimmt sie eins zu eins. Für jeden Schlüssel entsteht in EmDash ein eigenes Feld. Hat eine einzige Referenz fünf Kennzahlen, bekommt die ganze Collection zehn Felder dafür. Bei unserer Test-Site waren es am Ende 29.

Oben das ACF-Repeater-Feld „Kennzahlen“ einer Referenz in WordPress mit zwei Zeilen, Besucher +40 % und Ladezeit 0,8 s. Unten dieselbe Referenz in EmDash mit Flexible Content als JSON, dem Logo als Zahl 1687, verwandten Einträgen als [1, 163], der Beschreibung als HTML-Code und dem Repeater zerlegt in einzelne Felder wie „Kennzahlen 0 Label“.
Dieselbe Referenz in WordPress und direkt nach dem Import in EmDash. WordPress 7.1.2 und EmDash 1.1.0, Oktober 2026.

Für jeden ACF-Typ gibt es in EmDash ein passendes Feld, es wird beim Import nur nicht verwendet:

ACF-Feld

Nach dem Import

Passendes EmDash-Feld

Repeater

Anzahl plus ein Feld pro Zeile und Spalte

repeater

Bild

WordPress-ID als Zahl

image

Beziehung

Liste von WordPress-IDs als JSON

reference

Datum

Zahl wie 20250115

string mit 2025-01-15

Wahr / Falsch

Zahl 0 oder 1

boolean

WYSIWYG

HTML als Text

portableText

Flexible Content

JSON und zusätzlich zerlegt

blocks

Zwei Dinge sind mir erst beim Bauen aufgefallen, und beide haben mit ACF nichts zu tun. Die Titel und Beschreibungen aus Yoast landen als normale Felder seo_title und seo_description statt im SEO-Bereich von EmDash. Und das Beitragsbild zeigt zwar auf eine Datei in der eigenen Mediathek, ist aber als externes Bild gespeichert, ohne Verknüpfung.

Beim Datum habe ich mich gegen den Feldtyp datetime entschieden. EmDash rechnet dort jeden Wert in UTC um, und aus einem Launch am 15. Januar kann so schnell der 14. werden. Die Doku zu den Feldtypen empfiehlt für ein reines Kalenderdatum deshalb ein Textfeld. Im Format JJJJ-MM-TT lässt es sich trotzdem sortieren.

Mein Skript in drei Schritten

Ich wollte etwas, das ich bei einem echten Umzug beliebig oft laufen lassen kann, und habe das Skript deshalb in drei Befehle geteilt. schema legt die neuen Felder an und schaltet den SEO-Bereich für die Collection ein. data liest jede Referenz, übersetzt die Werte und veröffentlicht sie wieder. cleanup löscht die alten Felder, aber erst, wenn ich mir das Ergebnis angeschaut habe. Das ganze Skript findest du als Gist auf GitHub.

Die neuen Felder brauchen eigene Namen, weil es kennzahlen und logo schon gibt, nur mit dem falschen Typ. Ich habe sie kennzahlen_liste, galerie_bilder, logo_bild, verwandte, launch_datum, beschreibung_text und hervorgehoben genannt. Schön ist das nicht. Wenn dein Frontend noch nicht steht, ist es aber egal, und sonst planst du die neuen Namen am besten gleich mit ein.

Das Skript redet mit der REST-API von EmDash. Dafür brauchst du einen API-Token aus dem Admin mit den Rechten content:read, content:write, schema:read, schema:write und media:read. Für die alten IDs fragt es bei WordPress nach, die alte Site muss also noch laufen. Und weil es Felder anlegt und löscht, lass es zuerst gegen eine Kopie laufen. Ich habe vorher die Datenbank der Test-Site gesichert und war froh darum, wie du gleich siehst.

Repeater zurückbauen

Das war der einfachste Teil. Aus der Anzahl und den nummerierten Feldern werden wieder Zeilen:

// ACF stores a repeater as a row count plus one key per cell: galerie = 2,
// galerie_0_bild, galerie_0_text, galerie_1_bild, ...
function rows(data, name, cells) {
	const count = Number(data[name] ?? 0);
	return Array.from({ length: count }, (_, i) =>
		Object.fromEntries(cells.map((cell) => [cell, data[`${name}_${i}_${cell}`] ?? null])),
	);
}

Das neue Feld legt das Skript so an:

{
	slug: "kennzahlen_liste",
	label: "Kennzahlen",
	type: "repeater",
	validation: {
		subFields: [
			{ slug: "label", type: "string", label: "Label" },
			{ slug: "wert", type: "string", label: "Wert" },
		],
	},
}

Bei der Galerie bin ich an die erste Grenze gestossen. In einem EmDash-Repeater gibt es nur einfache Felder wie Text, Zahl, Ja/Nein, Datum, Auswahl, URL und Bild, aber keinen formatierten Text. Die Bildtexte waren in ACF kleine WYSIWYG-Felder, im Repeater bleibt davon nur der reine Text. Aus „Bild eins“ wird „Bild eins“. Bei Bildunterschriften kann ich damit leben, bei längeren Texten würde ich die Felder anders schneiden.

Bilder über ihren Inhalt finden

Hier musste ich am längsten überlegen. Der Importer hat jedes Bild in die Mediathek von EmDash kopiert, im Feld steht aber weiter die WordPress-ID. Eine Liste, welche alte ID zu welchem neuen Bild gehört, gibt es nicht. Über den Dateinamen zu gehen, war mir zu riskant, denn in jeder gewachsenen Kundenseite liegt irgendwo ein zweites logo.png.

Eindeutig ist der Inhalt der Datei. EmDash speichert zu jedem Bild eine SHA-1-Prüfsumme. Das Skript lädt das Original über die WordPress-API, rechnet dieselbe Prüfsumme aus und sucht damit das passende Bild:

const attachment = await wp(`/media/${wpId}`);
const file = Buffer.from(await (await fetch(attachment.source_url)).arrayBuffer());
const hash = `sha1:${createHash("sha1").update(file).digest("hex")}`;
const item = media.byHash.get(hash);

Bei den zwölf Referenzen hat das für jedes Logo und jedes Galeriebild auf Anhieb gepasst. Findet das Skript kein Bild, schreibt es eine Warnung und lässt das Feld leer. Lieber ein leeres Feld, das jemand bemerkt, als das falsche Logo auf der Referenz eines Kunden.

Beim Beitragsbild war es einfacher, weil dort schon der Pfad in die eigene Mediathek steht. Das Skript sucht das Bild dazu und verknüpft es richtig.

Verwandte Beiträge

Im Beziehungsfeld stehen WordPress-IDs wie [1, 163]. Die Suche der WordPress-API verrät zu jeder ID den Beitragstyp und die Adresse, und darüber kommt das Skript an den Slug. Unter diesem Slug liegt der Beitrag nach dem Import auch in EmDash. In EmDash wird daraus ein Verweisfeld:

{
	slug: "verwandte",
	label: "Verwandte Beiträge",
	type: "reference",
	validation: { targetCollection: "posts", multiple: true },
}

Ein Verweisfeld zeigt allerdings immer auf genau eine Collection. In ACF kann eine Beziehung Beiträge, Seiten und eigene Typen mischen. In unseren Testdaten waren es nur Beiträge. Wenn deine Felder gemischt sind, brauchst du ein Verweisfeld pro Ziel oder entscheidest dich für eines. Das Skript meldet jeden Verweis, den es nicht zuordnen kann.

Verweise schreibst du übrigens nicht in die Daten des Eintrags, sondern daneben, als Liste von EmDash-IDs pro Feld (REST-API). EmDash speichert sie in derselben Transaktion wie den Eintrag.

Speichern, und dann noch einmal veröffentlichen

Der Rest war Fleissarbeit. Aus 20250115 wird 2025-01-15, aus 1 wird true, und das HTML aus dem WYSIWYG-Feld wandelt htmlToPortableText aus dem offiziellen Paket `@emdash-cms/gutenberg-to-portable-text` um. Das ist derselbe Konverter, den EmDash beim Import für den Inhalt verwendet.

Beim ersten Lauf dachte ich, ich sei fertig. Im Admin waren alle neuen Felder gefüllt, alles sah richtig aus. Dann habe ich mir die veröffentlichte Fassung angeschaut, und dort standen noch die alten Daten. Bei einem veröffentlichten Eintrag landet eine Änderung über die API zuerst in einem Entwurf, genau wie wenn eine Redakteurin im Admin etwas ändert und noch nicht auf Veröffentlichen geklickt hat. Das ist sinnvoll, nur hatte ich nicht daran gedacht.

Seitdem veröffentlicht das Skript jede Referenz wieder, die vorher veröffentlicht war:

const updated = await emdash(`/content/${COLLECTION}/${item.id}`, {
	method: "PUT",
	body: JSON.stringify({ data, references, seo, _rev }),
});
// On a published entry the update lands in a draft revision. Publish it
// again, but leave drafts as drafts.
if (item.status === "published") {
	await emdash(`/content/${COLLECTION}/${item.id}/publish`, {
		method: "POST",
		body: JSON.stringify({ _rev: updated._rev }),
	});
}

Entwürfe bleiben Entwürfe, und das ursprüngliche Veröffentlichungsdatum aus WordPress bleibt erhalten. _rev ist die Revision, die du beim Lesen bekommst. Hat jemand den Eintrag in der Zwischenzeit geändert, lehnt EmDash das Speichern ab, statt seine Änderung zu überschreiben.

Der zweite Stolperstein kam beim Aufräumen. Nach cleanup fehlen die alten Felder, und ein weiterer Lauf von data hätte die neuen Felder mit leeren Werten überschrieben. Bei einem Umzug, den man mehrmals probt, passiert so etwas schnell. Jetzt erkennt das Skript bereits umgebaute Referenzen und lässt sie in Ruhe. Damit ich das sauber testen konnte, habe ich die gesicherte Datenbank zurückgespielt und alles noch einmal von vorne laufen lassen.

So sieht es jetzt aus

Nach dem Lauf sind die Kennzahlen ein Repeater mit zwei Zeilen, das Logo ist ein Bild aus der Mediathek, und die verwandten Beiträge lassen sich anklicken und umsortieren. Titel und Beschreibung aus Yoast stehen im SEO-Bereich, wo sie hingehören. Mit cleanup habe ich danach 20 alte Felder gelöscht, die Collection hat jetzt 18 statt 29.

Links dieselbe Referenz in EmDash nach dem Skript: die Kennzahlen als Repeater mit den Zeilen Besucher +40 % und Ladezeit 0,8 s, darunter das Logo als Bild aus der Mediathek und die verwandten Beiträge „Hello world!“ und „WP 6.1 Font size scale“ als Verweise. Rechts der SEO-Bereich mit SEO-Titel und Meta-Beschreibung aus Yoast.
Dieselbe Referenz, nachdem das Skript gelaufen ist. EmDash 1.1.0, Oktober 2026.

Überrascht hat mich, wie wenig vom Skript mit EmDash selbst zu tun hat. Die REST-API hat alles hergegeben, was ich brauchte. Die meiste Arbeit steckt darin, die alten WordPress-IDs aufzulösen und zu entscheiden, wie die Felder in EmDash heissen und aussehen sollen.

Was noch Handarbeit bleibt

Das Flexible-Content-Feld sections habe ich bewusst nicht angefasst. Es kommt als sauberes JSON mit dem Layout pro Abschnitt an, nur die zerlegten Kopien daneben löscht das Skript. EmDash hat dafür den Feldtyp blocks, der eigene Blocktypen braucht, und das ist ein Thema für einen eigenen Artikel. Die Bilder darin sind bis dahin noch WordPress-IDs.

Bedingte Felder und Options-Pages kennt EmDash nicht. Werte aus einer Options-Page kannst du zum Beispiel in eine eigene Collection mit einem einzigen Eintrag legen. Und dann ist da noch das Frontend: Die neuen Feldnamen musst du im Template verwenden, Bilder renderst du mit <Image image={...} /> aus emdash/ui, und Verweise holst du mit getEmDashReferences().

Fazit

Der Import aus WordPress verliert keine ACF-Daten, er legt sie nur so ab, wie WordPress sie speichert. Das ist ärgerlich, lässt sich aber reparieren, und zwar mit überschaubarem Aufwand und ohne einen einzigen Wert von Hand abzutippen. Eigentlich gehört das in den Importer selbst. Bis es dort ist, würde ich bei jedem Umzug mit ACF so ein Skript einplanen und es von Anfang an mitlaufen lassen: zuerst gegen eine Kopie, dann bei jedem Probelauf, und am Tag der Umstellung ein letztes Mal.

Was es sonst bei einem Umzug von WordPress zu beachten gibt, steht im Leitfaden für Agenturen. Und wenn du ACF-Felder hast, bei denen dieser Ansatz nicht aufgeht, schreib es mir in die Kommentare. Mich interessiert vor allem, wie ihr Flexible Content gelöst habt.

Über den Autor

Kevin Kyburz

Kevin Kyburz

Seit 20 Jahren im Netz und noch immer nicht fertig damit. Kevin Kyburz führt die Webagentur this:matters, baut Websites mit WordPress und EmDash und arbeitet als Maintainer an EmDash mit. Als Schweizer schreibt er „gross“ statt „groß“, und zwar mit Absicht.

Kommentare

No comments yet

Erstkommentare erscheinen nach unserer Freigabe. Was mit deinen Angaben passiert