

Ein eigenes Checkout-Feld ist nicht fertig, nur weil es auf der Seite steht. Es muss die Übermittlung überstehen, in der Bestellung landen und ausschließlich Daten ablehnen, die eine Erfüllung tatsächlich verhindern. Fehlt der Save-Hook, schließt der Kunde den Checkout ab, doch der Wert geht verloren. Ist die Validierung zu eng, scheitern gute Bestellungen im letzten Schritt.
Dieser Guide führt durch den gesamten Ablauf mit praxistauglichem Code. Dazu gehören die abweichenden Regeln für Checkout-Blöcke und Express-Zahlungsbuttons.
Im klassischen Checkout übergibt dir dieser Filter das vollständige Formular als verschachteltes Array, bevor WooCommerce es ausgibt. Hier passt du die eingebauten Felder an.
woocommerce_checkout_fields liefert vier Gruppen auf oberster Ebene. Jede Gruppe enthält Felder, die über ihren Eingabenamen angesprochen werden. Jedes Feld ist ein Array mit Eigenschaften wie label, placeholder, required, class, priority und validate.
Die vier Feldgruppen in $fields
| Gruppe | Enthält | Gerendert wenn |
|---|---|---|
billing | Rechnungsname, Adresse, Telefon und E-Mail | Immer |
shipping | Liefername und Lieferadresse | Versand nötig ist und eine andere Lieferadresse gewählt wird |
account | Felder für Benutzername und Passwort | Kontoerstellung beim Checkout ist aktiviert |
order | Bestellnotizen und üblicherweise eigene bestellbezogene Felder | Immer, sofern du es nicht entfernst |
Die Prioritäten der eingebauten Felder steigen meist in Zehnerschritten. Dadurch bleibt zwischen ihnen Platz für eigene Felder.
Zwei Grenzen überraschen auch erfahrene Entwickler. Der Filter betrifft ausschließlich den klassischen Checkout-Shortcode, nicht den Checkout-Block. Er steuert nur Ausgabe und Pflichtstatus. Ohne Save-Hook geht der Wert eines hier eingefügten Feldes nach dem Checkout verloren. Nach einem WooCommerce-Update prüfst du, ob das Feld noch gerendert wird und speichert, bevor du den Checkout als gesund annimmst.
Lege diesen Code in ein kleines projektspezifisches Plugin oder ein Child-Theme. Das Parent-Theme ist der falsche Ort:
add_filter( 'woocommerce_checkout_fields', 'sm_checkout_fields' );
function sm_checkout_fields( $fields ) {
$fields['billing']['billing_phone']['required'] = false;
$fields['billing']['billing_phone']['label'] = 'Mobile number for delivery updates';
$fields['order']['order_comments']['placeholder'] = 'Anything the courier should know';
return $fields;
}Gib immer $fields zurück. Ein Filter ohne Rückgabewert übergibt WooCommerce ein leeres Checkout-Formular und kann einen Live-Shop lahmlegen.
Adressfelder laufen zusätzlich durch woocommerce_billing_fields, woocommerce_shipping_fields und woocommerce_default_address_fields. Nutze diese Hooks, wenn dieselbe Änderung auch in den Adressformularen unter Mein Konto gelten soll.
Entferne einen Key mit unset und setze priority zum Verschieben:
add_filter( 'woocommerce_checkout_fields', 'sm_trim_checkout_fields' );
function sm_trim_checkout_fields( $fields ) {
// Drop fields this store genuinely does not use.
unset( $fields['billing']['billing_company'] );
unset( $fields['billing']['billing_address_2'] );
// Move contact details above the address block.
$fields['billing']['billing_email']['priority'] = 21;
$fields['billing']['billing_phone']['priority'] = 22;
// Widen the remaining rows.
$fields['billing']['billing_email']['class'] = array( 'form-row-wide' );
return $fields;
}Priority steuert die Reihenfolge innerhalb einer Gruppe, nicht auf der ganzen Seite. Eine Änderung bei billing betrifft shipping nicht. Die eingebauten Felder lassen oft Zehnerschritte frei. Mit 21 und 22 liegen beide Felder zwischen Nachname (20) und Firma (30), ohne andere Felder zu verschieben.
Umsortieren ist meist risikoarm. Für das Entfernen eines Feldes brauchst du einen klaren Grund.
Steuer- und Versandfelder
Diese Felder solltest du behalten, auch wenn sie verzichtbar wirken:
Im klassischen WooCommerce-Checkout gibst du ein eigenes Feld mit woocommerce_form_field() an einem Checkout-Action-Hook aus. woocommerce_after_order_notes ist ein gängiger Ort. woocommerce_before_order_notes, woocommerce_after_checkout_billing_form und woocommerce_review_order_before_submit folgen demselben Muster. Gib jedem Feldnamen ein Präfix, damit er nicht mit WooCommerce oder einem Plugin kollidiert.
add_action( 'woocommerce_after_order_notes', 'sm_text_checkout_fields' );
function sm_text_checkout_fields( $checkout ) {
woocommerce_form_field(
'sm_vat_number',
array(
'type' => 'text',
'label' => __( 'VAT number', 'my-store' ),
'placeholder' => 'DE123456789',
'class' => array( 'form-row-wide' ),
'required' => false,
),
$checkout->get_value( 'sm_vat_number' )
);
woocommerce_form_field(
'sm_delivery_notes',
array(
'type' => 'textarea',
'label' => __( 'Delivery notes', 'my-store' ),
'class' => array( 'form-row-wide' ),
),
$checkout->get_value( 'sm_delivery_notes' )
);
}$checkout->get_value() stellt die Eingabe wieder her, wenn die Validierung den Kunden zurück zum Formular schickt. Ohne diesen Wert löscht jede fehlgeschlagene Übermittlung die bisherige Eingabe.
Beide Feldtypen nutzen ein options-Array. Der Key ist der Wert, den du speichern möchtest:
woocommerce_form_field( 'sm_delivery_window', array( 'type' => 'select', 'label' => __( 'Preferred delivery window', 'my-store' ), 'class' => array( 'form-row-wide' ), 'options' => array( '' => __( 'No preference', 'my-store' ), 'morning' => __( 'Morning, 8:00 to 12:00', 'my-store' ), 'evening' => __( 'Evening, 16:00 to 20:00', 'my-store' ), ), ), $checkout->get_value( 'sm_delivery_window' ) );
Ersetze select durch radio und WooCommerce gibt dieselben Optionen als Radio-Inputs aus. Behandle übermittelte Werte als nicht vertrauenswürdig. Es sind Request-Strings, kein Beweis für eine gültige Auswahl. Vergleiche jeden Wert vor dem Speichern mit deiner eigenen Liste erlaubter Werte.
woocommerce_form_field( 'sm_gift_wrap', array( 'type' => 'checkbox', 'label' => __( 'Add gift wrapping', 'my-store' ), 'class' => array( 'form-row-wide' ), ), $checkout->get_value( 'sm_gift_wrap' ) );
Eine nicht gesetzte Checkbox übermittelt nichts. Prüfe mit isset(), statt einen Wert zu vergleichen, und speichere ausdrücklich yes oder no. Sonst kannst du eine abgelehnte Option nicht von einem Feld unterscheiden, das nie ausgegeben wurde.
Datei-Uploads sind die Ausnahme. Der klassische Checkout sendet ein serialisiertes Formular per AJAX. Solche Formulare enthalten kein $_FILES. Ein einfaches File-Input mit type => file kann also korrekt aussehen und auf dem Server trotzdem leer ankommen.
Nutze zwei Requests. Lade die Datei in deinen eigenen Endpoint, sobald der Kunde sie auswählt. Schreibe anschließend die zurückgegebene Attachment-ID in ein verborgenes Feld, das die Checkout-Übermittlung transportieren kann.
add_action( 'wp_ajax_sm_upload_checkout_file', 'sm_upload_checkout_file' );
add_action( 'wp_ajax_nopriv_sm_upload_checkout_file', 'sm_upload_checkout_file' );
function sm_upload_checkout_file() {
check_ajax_referer( 'sm_checkout_upload', 'nonce' );
if ( empty( $_FILES['sm_artwork']['name'] ) ) {
wp_send_json_error( array( 'message' => 'No file received.' ), 400 );
}
if ( $_FILES['sm_artwork']['size'] > 2 * MB_IN_BYTES ) {
wp_send_json_error( array( 'message' => 'Maximum file size is 2 MB.' ), 400 );
}
// Whitelist, not blacklist. No SVG, no archives, nothing executable.
$allowed = array(
'pdf' => 'application/pdf',
'png' => 'image/png',
'jpg|jpeg' => 'image/jpeg',
);
$checked = wp_check_filetype_and_ext(
$_FILES['sm_artwork']['tmp_name'],
sanitize_file_name( $_FILES['sm_artwork']['name'] ),
$allowed
);
if ( empty( $checked['type'] ) ) {
wp_send_json_error( array( 'message' => 'File type not allowed.' ), 400 );
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = media_handle_upload(
'sm_artwork',
0,
array(),
array(
'test_form' => false,
'mimes' => $allowed,
)
);
if ( is_wp_error( $attachment_id ) ) {
wp_send_json_error( array( 'message' => $attachment_id->get_error_message() ), 400 );
}
wp_send_json_success( array( 'attachment_id' => (int) $attachment_id ) );
}Dein JavaScript sendet Datei und Nonce an admin-ajax.php und schreibt attachment_id in das verborgene Feld sm_artwork_id. Für Uploads brauchst du zusätzliche Logik, die diese Attachment-ID validiert und in der Bestellung speichert.
Öffentlicher Upload-Endpoint
Ein ausgegebenes Feld ohne Save-Hook erfasst nichts. Der Checkout läuft trotzdem durch. Genau deshalb bleibt dieser Fehler leicht unbemerkt.
Speichere über das Bestellobjekt in woocommerce_checkout_create_order. Dieser Hook läuft, bevor WooCommerce die Bestellung schreibt:
add_action( 'woocommerce_checkout_create_order', 'sm_save_custom_checkout_fields', 10, 2 );
function sm_save_custom_checkout_fields( $order, $data ) {
// WooCommerce has already verified its own checkout nonce at this point.
if ( ! empty( $_POST['sm_vat_number'] ) ) {
$order->update_meta_data(
'_sm_vat_number',
sanitize_text_field( wp_unslash( $_POST['sm_vat_number'] ) )
);
}
if ( ! empty( $_POST['sm_delivery_notes'] ) ) {
$order->update_meta_data(
'_sm_delivery_notes',
sanitize_textarea_field( wp_unslash( $_POST['sm_delivery_notes'] ) )
);
}
// Never trust a posted option value. Compare it against your own list.
$window = isset( $_POST['sm_delivery_window'] ) ? sanitize_key( wp_unslash( $_POST['sm_delivery_window'] ) ) : '';
$allowed = array( 'morning', 'evening' );
if ( in_array( $window, $allowed, true ) ) {
$order->update_meta_data( '_sm_delivery_window', $window );
}
// An unchecked checkbox posts nothing, so write both states explicitly.
$order->update_meta_data( '_sm_gift_wrap', isset( $_POST['sm_gift_wrap'] ) ? 'yes' : 'no' );
}Rufe hier kein $order->save() auf. WooCommerce speichert die Bestellung direkt nach diesem Hook. Das Bestellobjekt statt update_post_meta() hält den Code kompatibel mit HPOS-Bestellspeicherung, bei der die posts-Tabelle nicht mehr die maßgebliche Ablage für Bestellungen ist.
Wenn du Code am älteren Hook woocommerce_checkout_update_order_meta übernimmst, musst du ihn nicht sofort ersetzen. Du musst aber die CRUD-Schicht verwenden:
add_action( 'woocommerce_checkout_update_order_meta', 'sm_legacy_save_custom_fields' );
function sm_legacy_save_custom_fields( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order || empty( $_POST['sm_vat_number'] ) ) {
return;
}
$order->update_meta_data( '_sm_vat_number', sanitize_text_field( wp_unslash( $_POST['sm_vat_number'] ) ) );
$order->save();
}Meta-Keys mit führendem Unterstrich bleiben im allgemeinen Custom-Fields-Panel verborgen. Gib sie daher gezielt aus:
add_action( 'woocommerce_admin_order_data_after_billing_address', 'sm_show_custom_fields_in_admin' );
function sm_show_custom_fields_in_admin( $order ) {
$vat = $order->get_meta( '_sm_vat_number' );
$window = $order->get_meta( '_sm_delivery_window' );
if ( $vat ) {
echo '<p><strong>' . esc_html__( 'VAT number', 'my-store' ) . ':</strong> ' . esc_html( $vat ) . '</p>';
}
if ( $window ) {
echo '<p><strong>' . esc_html__( 'Delivery window', 'my-store' ) . ':</strong> ' . esc_html( $window ) . '</p>';
}
}Nutze $order->get_meta() statt get_post_meta(), aus demselben HPOS-Grund wie beim Save-Hook.
Dieser Filter hängt deine Felder an die Metatabelle von WooCommerce-Bestell-E-Mails:
add_filter( 'woocommerce_email_order_meta_fields', 'sm_email_order_meta_fields', 10, 3 );
function sm_email_order_meta_fields( $fields, $sent_to_admin, $order ) {
$vat = $order->get_meta( '_sm_vat_number' );
if ( $vat ) {
$fields['sm_vat_number'] = array(
'label' => __( 'VAT number', 'my-store' ),
'value' => $vat,
);
}
return $fields;
}Prüfe $sent_to_admin, wenn ein Wert nur in die interne E-Mail gehört. Der Filter steuert die Template-Ausgabe. Künftig kann ShopMonitor die Zustellung transaktionaler E-Mails separat testen.
Bei der Validierung entscheidet sich, ob eine Formularänderung Bestellungen unterstützt oder ausbremst.
Nutze woocommerce_after_checkout_validation. Der Hook läuft nach der eingebauten Validierung, liefert geparste $data und ein WP_Error-Objekt und verbindet einen Fehler mit einem bestimmten Feld:
add_action( 'woocommerce_after_checkout_validation', 'sm_validate_custom_checkout_fields', 10, 2 );
function sm_validate_custom_checkout_fields( $data, $errors ) {
$vat = isset( $_POST['sm_vat_number'] ) ? sanitize_text_field( wp_unslash( $_POST['sm_vat_number'] ) ) : '';
if ( '' === $vat ) {
return;
}
// Normalize before judging: people paste spaces and dots.
$vat = strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', $vat ) );
if ( ! preg_match( '/^[A-Z]{2}[A-Z0-9]{2,12}$/', $vat ) ) {
$errors->add(
'sm_vat_number',
__( 'Enter your VAT number with the country prefix, for example DE123456789. Leave it empty if you do not have one.', 'my-store' )
);
}
}Der ältere Hook woocommerce_checkout_process mit wc_add_notice() funktioniert weiterhin. Er kann aber keinen Fehler an ein Feld binden. Der Kunde sieht dann nur eine Meldung am Seitenanfang und weiß nicht, welches Eingabefeld Aufmerksamkeit braucht.
Zu strenge Validierung ist teuer, weil sie selten ein Error-Log oder Support-Ticket erzeugt. Der Kunde sieht eine Ablehnung, die er nicht auflösen kann, und bricht ab.
Stille Validierungsfehler
Bevor du ein eigenes Feld zur Pflicht machst, prüfe vier Punkte:
Halte das Feld optional, solange die Bestellung ohne diesen Wert erfüllt werden kann.
Code gibt dir präzise Kontrolle. Ein Plugin gibt Teams ohne technisches Fachwissen eine Admin-Oberfläche. Dieser Unterschied zählt mehr, als viele Teams erwarten.
Die offizielle Erweiterung Checkout Field Editor bietet eine Tabelle für Rechnungs-, Versand- und Zusatzfelder. Du kannst Felder hinzufügen, entfernen, sortieren, umbenennen und Typen wie Select, Radio, Checkbox oder Datum ändern. ThemeHighs Checkout Field Editor bietet ähnliche Funktionen. Die kostenlose Variante ist eingeschränkt, die kostenpflichtige enthält bedingte Logik. Beide speichern Felddefinitionen in der Options-Tabelle außerhalb deiner Versionskontrolle. Wird das Plugin deaktiviert, greifen diese Definitionen nicht mehr.
Entscheidung Code vs. Plugin
| Situation | Bessere Wahl |
|---|---|
| Ein oder zwei stabile Felder mit einem Speicherpfad | Code |
| Nichttechnisches Team ändert Felder regelmäßig | Field-Editor-Plugin |
| Bedingte Felder ohne Deployment ändern | Field-Editor-Plugin |
| Eigene Validierung oder externer Lookup | Code |
| Feldwerte müssen in ein anderes System | Code |
Die Plugin-Route ist sinnvoll, wenn:
Eigene Felder hängen an Filtern und der Bestellspeicherung. Dadurch können auch Updates, die du nicht selbst geschrieben hast, etwas kaputtmachen.
Validierung blockiert echte Bestellungen
Felder speichern nach einem WooCommerce-Update nicht mehr
Inkompatibilität mit dem Checkout-Block
Konflikte mit Express-Checkout
Ein Checkout kann sauber aussehen, obwohl ein Feld nicht mehr speichert. ShopMonitor durchläuft den Checkout- und Thank-you-Pfad im Browser nach deinem Zeitplan. Scheitert dieser Pfad, ist das im nächsten Testlauf sichtbar. Künftig kann ShopMonitor zusätzlich die Zustellung transaktionaler E-Mails testen.
Plane nach jedem WooCommerce-, Theme- und Plugin-Update einen wiederholbaren Checkout-Durchlauf ein. Sende jedes eigene Feld ab und prüfe, ob der Wert in der Bestellung gelandet ist. Unser Guide zum Checkout testen zeigt den manuellen Ablauf. Auch die weiteren Änderungen zur Checkout-Seite anpassen brauchen dieselbe Sorgfalt.
ShopMonitor führt Browser-Tests für Checkout und Formulare nach deinem Zeitplan aus und alarmiert dich, wenn ein Pfad scheitert. Kein Plugin nötig.
Keine Kreditkarte nötig.

Über den Autor
Verfasst vom Team bei Winning Solutions, der Agentur hinter ShopMonitor. Wir helfen WooCommerce-Teams, Checkout-Felder zuverlässig zu speichern und zu validieren, ohne echte Bestellungen zu blockieren.