Product Fields Generator
Fields in an existing Product Data tab or a new one, plus the save handler that writes them — through the product's own CRUD methods, not a raw postmeta write.
<?php // Generated with WP CodeKit — powered by GrowQuest (https://growquest.io). /** * Plugin Name: Product Details * Description: Adds custom fields to WooCommerce products and saves their values. * Version: 1.0.0 * Requires PHP: 7.4 * Requires Plugins: woocommerce * Text Domain: acme */ defined( 'ABSPATH' ) || exit; /** * Add the Product Details tab. * * @param array $tabs Existing tabs. * @return array */ function acme_add_product_tab( $tabs ) { $tabs['product_details'] = array( 'label' => __( 'Product Details', 'acme' ), 'target' => 'product_details_data', 'class' => array( 'show_if_simple', 'show_if_variable' ), 'priority' => 21, ); return $tabs; } add_filter( 'woocommerce_product_data_tabs', 'acme_add_product_tab' ); /** * Render the panel. */ function acme_product_tab_panel() { echo '<div id="product_details_data" class="panel woocommerce_options_panel">'; echo '<div class="options_group">'; woocommerce_wp_text_input( array( 'id' => '_acme_manufacturer_part_no', 'label' => __( 'Manufacturer part number', 'acme' ), 'desc_tip' => true, 'description' => __( 'Printed on the packing slip.', 'acme' ), ) ); woocommerce_wp_checkbox( array( 'id' => '_acme_assembly_required', 'label' => __( 'Assembly required', 'acme' ), 'description' => __( 'Shows an assembly notice on the product page.', 'acme' ), ) ); woocommerce_wp_select( array( 'id' => '_acme_difficulty', 'label' => __( 'Assembly difficulty', 'acme' ), 'options' => array( 'easy' => __( 'Easy', 'acme' ), 'moderate' => __( 'Moderate', 'acme' ), 'advanced' => __( 'Advanced', 'acme' ), ), ) ); woocommerce_wp_textarea_input( array( 'id' => '_acme_care_instructions', 'label' => __( 'Care instructions', 'acme' ), ) ); echo '</div>'; echo '</div>'; } add_action( 'woocommerce_product_data_panels', 'acme_product_tab_panel' ); /** * Save the fields. * * @param int $post_id Product ID. */ function acme_save_product_fields( $post_id ) { $product = wc_get_product( $post_id ); if ( ! $product ) { return; } if ( ! in_array( $product->get_type(), array( 'simple', 'variable' ), true ) ) { return; } if ( isset( $_POST['_acme_manufacturer_part_no'] ) ) { $product->update_meta_data( '_acme_manufacturer_part_no', wc_clean( wp_unslash( $_POST['_acme_manufacturer_part_no'] ) ) ); } $product->update_meta_data( '_acme_assembly_required', isset( $_POST['_acme_assembly_required'] ) ? 'yes' : 'no' ); if ( isset( $_POST['_acme_difficulty'] ) && in_array( sanitize_key( $_POST['_acme_difficulty'] ), array( 'easy', 'moderate', 'advanced' ), true ) ) { $product->update_meta_data( '_acme_difficulty', sanitize_key( $_POST['_acme_difficulty'] ) ); } if ( isset( $_POST['_acme_care_instructions'] ) ) { $product->update_meta_data( '_acme_care_instructions', sanitize_textarea_field( wp_unslash( $_POST['_acme_care_instructions'] ) ) ); } $product->save(); } add_action( 'woocommerce_process_product_meta', 'acme_save_product_fields' ); /** * Show the values on the single product page. */ function acme_display_product_fields() { global $product; if ( ! $product ) { return; } $manufacturer_part_no = $product->get_meta( '_acme_manufacturer_part_no', true ); if ( '' !== $manufacturer_part_no ) { printf( '<span class="acme-manufacturer-part-no"><strong>%1$s:</strong> %2$s</span>', esc_html__( 'Manufacturer part number', 'acme' ), esc_html( $manufacturer_part_no ) ); } if ( 'yes' === $product->get_meta( '_acme_assembly_required', true ) ) { printf( '<span class="acme-assembly-required">%s</span>', esc_html__( 'Assembly required', 'acme' ) ); } $difficulty = $product->get_meta( '_acme_difficulty', true ); if ( '' !== $difficulty ) { printf( '<span class="acme-difficulty"><strong>%1$s:</strong> %2$s</span>', esc_html__( 'Assembly difficulty', 'acme' ), esc_html( $difficulty ) ); } $care_instructions = $product->get_meta( '_acme_care_instructions', true ); if ( '' !== $care_instructions ) { printf( '<span class="acme-care-instructions"><strong>%1$s:</strong> %2$s</span>', esc_html__( 'Care instructions', 'acme' ), esc_html( $care_instructions ) ); } } add_action( 'woocommerce_product_meta_end', 'acme_display_product_fields' ); /** * Expose the fields to the REST API and the block editor. */ function acme_register_product_meta() { register_post_meta( 'product', '_acme_manufacturer_part_no', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'wc_clean', 'auth_callback' => static function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_product', $post_id ); }, ) ); register_post_meta( 'product', '_acme_assembly_required', array( 'type' => 'boolean', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => static function ( $value ) { return $value ? 'yes' : 'no'; }, 'auth_callback' => static function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_product', $post_id ); }, ) ); register_post_meta( 'product', '_acme_difficulty', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'sanitize_key', 'auth_callback' => static function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_product', $post_id ); }, ) ); register_post_meta( 'product', '_acme_care_instructions', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'sanitize_textarea_field', 'auth_callback' => static function ( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_product', $post_id ); }, ) ); } add_action( 'init', 'acme_register_product_meta' );
About this generator
Product Fields Generator Online
Build a WooCommerce custom product field — a manufacturer part number, an assembly-difficulty select, a care-instructions textarea — and get the markup, the sanitiser and the save handler as one coherent block of PHP. Fields go into the General, Inventory or Shipping tab, or into a brand-new Product Data tab registered through woocommerce_product_data_tabs and woocommerce_product_data_panels.
The Preview tab draws the actual Product Data metabox — the tab rail, the label column, the input WooCommerce would render for each field type — so you can see the panel before pasting anything into a live store. Free to use, no account, and nothing you type leaves the browser.
Reviewed Output tested on WordPress 6.8 · PHP 8.3
Why the Product Fields Generator beats a copy-pasted woocommerce_wp_text_input snippet
The usual snippet is one woocommerce_wp_text_input() call plus an update_post_meta() line, and it works right up until the meta key collides with something WooCommerce already owns, or the select saves a value that was never in its own options list. This generator writes the render side and the save side together, picks the right sanitiser per field type, and audits the meta keys before you paste anything near a catalogue.
Core meta keys are blocked outright
Twenty-eight of WooCommerce’s own keys — _price, _regular_price, _sale_price, _sku, _stock, _weight, _tax_class and the rest — raise a hard error, with the message naming what you would corrupt: pricing, stock levels or product data.
A sanitiser chosen per field type
Text emits wc_clean(), textarea sanitize_textarea_field(), number absint(), price wc_format_decimal(), URL esc_url_raw(), and a select is checked against in_array() on its own option values before it is allowed to save.
CRUD writes, not raw postmeta
The save handler uses $product->update_meta_data() followed by one $product->save(). The legacy update_post_meta() path is still offered, but it is flagged as a recommendation with a one-click switch, because it bypasses the product object WooCommerce may already hold in memory for that save.
Reserved tab ids caught before they overwrite core
A new Product Data tab whose id resolves to general, inventory, shipping, linked_product, attribute, variations or advanced is rejected, and a tab with no label is an error rather than a blank pill in the rail.
A real Product Data metabox preview
The Preview tab renders the tab rail with your new tab in place, the label column, tooltips as the grey ? icon, and the exact input control WooCommerce draws for each type — text, textarea, checkbox or select with your parsed choices.
Optional REST and block-editor exposure
Switch it on and every field also gets a register_post_meta() call on the product post type, typed as string, number or boolean, with an auth_callback that checks current_user_can( 'edit_product', $post_id ) for that specific product.
How does the Product Fields Generator work?
Four steps. Say where the fields live, describe them, choose how they are written and shown, then export.
- 01
Choose the placement
Pick an existing tab (General, Inventory, Shipping) or a new one, set the tab label and id if it is new, then set the meta key prefix, function prefix and text domain. Optionally limit the fields to Simple, Variable, Grouped or External products.
- 02
Add the fields
For each field give an id, a label and a type. Add a description, turn it into a tooltip, and for a select list the choices as
value:Labelpairs. The full meta key is shown as you type, prefix included. - 03
Set the save and display behaviour
Keep the CRUD save method, decide whether the values print on the single product page (after the SKU row or after the short description), whether empty values are hidden, and whether the keys are cleaned up on uninstall.
- 04
Clear the checks, then export
Fix anything flagged in the Checks tab — colliding keys, an empty select, a reserved tab id — then copy the snippet or download it as a
functions.phpblock or a standalone plugin file.
Worked example — a manufacturer part number on the General tab
One text field in the General tab, limited to Simple and Variable products, saved through the product object. Doc blocks trimmed; everything else is exactly what the tool emits.
function acme_add_product_fields() {
echo '<div class="options_group show_if_simple show_if_variable">';
woocommerce_wp_text_input( array(
'id' => '_acme_manufacturer_part_no',
'label' => __( 'Manufacturer part number', 'acme' ),
'desc_tip' => true,
'description' => __( 'Printed on the packing slip.', 'acme' ),
) );
echo '</div>';
}
add_action( 'woocommerce_product_options_general_product_data', 'acme_add_product_fields' );
function acme_save_product_fields( $post_id ) {
$product = wc_get_product( $post_id );
if ( ! $product ) {
return;
}
if ( isset( $_POST['_acme_manufacturer_part_no'] ) ) {
$product->update_meta_data( '_acme_manufacturer_part_no', wc_clean( wp_unslash( $_POST['_acme_manufacturer_part_no'] ) ) );
}
$product->save();
}
add_action( 'woocommerce_process_product_meta', 'acme_save_product_fields' );There is no nonce check and no capability check in the save handler, and that is deliberate. WC_Meta_Box_Product_Data::save() runs first, verifies the woocommerce_meta_nonce field, bails on autosaves and revisions, and checks edit_post for that product — only then does it fire woocommerce_process_product_meta. The Reference tab spells out that sequence.
WooCommerce custom product fields — frequently asked questions
The questions that come up most often when adding fields to the Product Data metabox.
Why is my custom product field not saving?
Almost always because the input’s id and the $_POST key you read do not match, or because the handler is not on woocommerce_process_product_meta. woocommerce_wp_text_input() uses the id argument as the field name, so the save handler must read $_POST with that same string. Checkboxes are the other common cause: an unchecked box posts nothing at all, so it must be handled with isset() rather than an equality test.
Do I need a nonce check when saving on woocommerce_process_product_meta?
No. WooCommerce’s own product data metabox handler runs first: it verifies the woocommerce_meta_nonce field, skips autosaves and revisions, and checks that the current user can edit that specific product. Only after all of that does it fire woocommerce_process_product_meta. Adding your own nonce check there is harmless but redundant.
Should product meta keys start with an underscore?
It is the convention WooCommerce itself follows, and this generator defaults to it. An underscore marks the key as protected, which hides it from the generic Custom Fields panel. On products that panel is disabled anyway, so the underscore is about consistency with core rather than about hiding data. What matters far more is that the key is namespaced with your own prefix so it can never collide with _price, _sku or _stock.
Can I add a custom field to a specific product type only?
Yes, with two caveats. Selecting product types adds show_if_simple, show_if_variable and similar classes to the wrapper, which is CSS-level hiding, and the generated save handler additionally checks $product->get_type() before writing anything. The tab itself still opens for every product type — only the fields inside it are hidden and skipped.
How do I add a custom field to a product variation instead?
A field added to the Product Data tabs lives on the parent product and is shared by every variation. A per-variation field is a different box entirely: render it on woocommerce_variation_options_pricing or woocommerce_product_after_variable_attributes, and save it on woocommerce_save_product_variation, which passes both the variation id and its loop index.
What value does a WooCommerce checkbox field actually store?
The string yes or no, never 1 or 0 — that is woocommerce_wp_checkbox()'s own convention, and the generated save handler follows it. Read it back with a strict comparison such as 'yes' === $product->get_meta( $key, true ). A loose truthy test is wrong here, because the string '0' is falsy in PHP while the string 'no' is not.
Where do I paste the code from the Product Fields generator?
You have three good options and one bad one. Best is a small site-specific plugin — the generator can output one for you, and it keeps working when you change theme. A code-snippets plugin is fine too. A child theme's functions.php works but ties the code to that theme. Never edit the parent theme: the next update overwrites it and your code is gone.
Can I use the generated code in client work or a commercial plugin?
Yes. The output is ordinary WordPress API calls that you configured — there is no licence attached to it and no attribution required. Use it in client sites, commercial plugins and themes, or products you sell. The generated file carries a short credit comment you are free to delete.
The other WooCommerce generators in the library, plus the WordPress tools most often used alongside this one.