2026年11月13日

2026年11月13日

WordPress Meta Box Tutorial: add_meta_box and Safe Saving with Nonces

This article is a localized edition of the Japanese original. Read the Japanese original

A meta box is a group of input fields shown on the post edit screen. With the core function add_meta_box(), you can add your own fields and save them to post meta (custom fields) without a plugin such as ACF.

In this article we build a small plugin that adds a meta box with three fields to posts: "Price", "SKU" and "In stock". The code was tested in November 2026 on WordPress 7.1 (PHP 8.3), confirming that registration, saving and nonce verification work.

The finished plugin

Save the following file as wp-content/plugins/myplugin-product-info/myplugin-product-info.php and activate it on the Plugins screen.

<?php
/**
 * Plugin Name: Product Info Meta Box
 * Description: Adds a "Product info" meta box to the post edit screen.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// 1. Register the meta box
add_action( 'add_meta_boxes', 'myplugin_add_product_box' );
function myplugin_add_product_box() {
	add_meta_box(
		'myplugin_product_info',       // ID (also used as the HTML id; pick a name nothing else uses)
		'Product info',                // Heading
		'myplugin_product_box_html',   // Function that prints the contents
		array( 'post' ),               // Post types to show it on (for several: array( 'post', 'page' ))
		'normal',                      // Position: normal / side / advanced
		'high'                         // Priority: high / core / default / low
	);
}

// 2. Print the meta box contents (input fields)
function myplugin_product_box_html( $post ) {
	wp_nonce_field( 'myplugin_save_product_info', 'myplugin_product_nonce' );

	$price    = get_post_meta( $post->ID, '_myplugin_price', true );
	$sku      = get_post_meta( $post->ID, '_myplugin_sku', true );
	$in_stock = get_post_meta( $post->ID, '_myplugin_in_stock', true );
	?>
	<p>
		<label for="myplugin_price">Price (JPY)</label><br>
		<input type="number" id="myplugin_price" name="myplugin_price"
			value="<?php echo esc_attr( $price ); ?>" min="0" step="1">
	</p>
	<p>
		<label for="myplugin_sku">SKU</label><br>
		<input type="text" id="myplugin_sku" name="myplugin_sku"
			value="<?php echo esc_attr( $sku ); ?>" class="regular-text">
	</p>
	<p>
		<label>
			<input type="checkbox" name="myplugin_in_stock" value="1" <?php checked( $in_stock, '1' ); ?>>
			In stock
		</label>
	</p>
	<?php
}

// 3. Save (runs only when a post of type "post" is saved)
add_action( 'save_post_post', 'myplugin_save_product_info' );
function myplugin_save_product_info( $post_id ) {
	// Only handle saves that come from this meta box's form
	if ( ! isset( $_POST['myplugin_product_nonce'] ) ) {
		return;
	}
	$nonce = sanitize_text_field( wp_unslash( $_POST['myplugin_product_nonce'] ) );
	if ( ! wp_verify_nonce( $nonce, 'myplugin_save_product_info' ) ) {
		return;
	}
	// Do nothing for autosaves and revisions
	if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) ) {
		return;
	}
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	// Price: delete the meta if empty; otherwise store a non-negative integer
	$price = isset( $_POST['myplugin_price'] ) ? trim( wp_unslash( $_POST['myplugin_price'] ) ) : '';
	if ( '' === $price ) {
		delete_post_meta( $post_id, '_myplugin_price' );
	} else {
		update_post_meta( $post_id, '_myplugin_price', absint( $price ) );
	}

	// SKU: sanitize as a single line of text
	$sku = isset( $_POST['myplugin_sku'] ) ? sanitize_text_field( wp_unslash( $_POST['myplugin_sku'] ) ) : '';
	update_post_meta( $post_id, '_myplugin_sku', $sku );

	// Checkbox: an unchecked box is not sent, so store '0' when it is missing
	$in_stock = isset( $_POST['myplugin_in_stock'] ) ? '1' : '0';
	update_post_meta( $post_id, '_myplugin_in_stock', $in_stock );
}

After activation, open the post edit screen and a "Product info" meta box appears below the content. The code also works in your theme's functions.php, but the fields disappear if you switch themes, so a plugin is the better home for it.

The add_meta_box arguments

add_meta_box( $id, $title, $callback, $screen, $context, $priority, $callback_args );
Argument What it is
$id The meta box ID. It is also used as the HTML id attribute, so prefix it with your plugin name to avoid collisions
$title The meta box heading
$callback The function that prints the contents. It receives the post (WP_Post)
$screen Where to show it. A post type name (post, page or a custom post type), as a string or an array
$context Position: normal (below the content), side (sidebar) or advanced
$priority Order within the same position: high, core, default or low
$callback_args Extra values (an array) passed to $callback. Optional

To act on one post type only, you can also use the add_meta_boxes_post hook (replace post with the post type name) instead of add_meta_boxes.

What the save routine checks

The save_post family of hooks runs every time a post is saved. It also runs for saves that do not come from the meta box form (Quick Edit, the REST API, wp_update_post() called from code and so on), so check the following, in this order, before saving:

  1. Is there a valid nonce? Verify the value printed by wp_nonce_field() with wp_verify_nonce(). If there is no nonce, the save did not come from this meta box, so do nothing. This prevents values being wiped by saves from other screens, and forged submissions from other sites (CSRF).
  2. Is it an autosave or a revision? Autosaves may not send the field values, so skip them.
  3. Does the user have permission? Use current_user_can( 'edit_post', $post_id ) to confirm the user can edit that post.

After taking a value from $_POST, remove the slashes WordPress adds with wp_unslash(), then sanitize it with a function suited to the field: absint() for numbers, sanitize_text_field() for a single line of text, sanitize_textarea_field() for multi-line text and esc_url_raw() for URLs.

The hook is save_post_post so that it runs only when posts of type post are saved. To use the meta box on pages too, also hook the same function to save_post_page, or use save_post and check the post type inside the function.

Handling checkboxes

An unchecked checkbox is not included in the submitted form. If you only save "when it is sent", a post that was checked once can never be unchecked. As in the code above, save '0' when it is not sent.

How it works in the block editor

Meta boxes added with add_meta_box() also appear below the content in the block editor. When you save a post, the content is saved first (through the REST API), and then the meta box form is submitted separately. So save_post_post runs twice for one save, but only the second run carries the meta box values. The first run has no nonce and returns without doing anything.

The leading underscore in meta keys

When a meta key starts with _, as in _myplugin_price, WordPress treats it as "protected meta" and does not show it in the "Custom Fields" box on the edit screen. This keeps values entered in your meta box from being edited a second time through the Custom Fields box. If you want them visible there too, use a key without the _.

Display the values in a template

Read saved values with get_post_meta(), and escape them with esc_html() or similar when printing.

<?php
// Inside the loop of a template (single.php and so on)
$price = get_post_meta( get_the_ID(), '_myplugin_price', true );
if ( '' !== $price ) {
	echo '<p>Price: ' . esc_html( number_format( (int) $price ) ) . ' yen</p>';
}

With true as the third argument, the value comes back as a single string (an empty string if nothing is saved).

Showing meta boxes conditionally and removing them

For a meta box only administrators should see, check the capability before registering it. Remove unwanted meta boxes with remove_meta_box().

<?php
// Show a small meta box in the sidebar to administrators only
add_action( 'add_meta_boxes_post', function ( $post ) {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}
	add_meta_box( 'myplugin_admin_note', 'Admin note', 'myplugin_admin_note_html', 'post', 'side' );
} );

// Remove the Classic Editor's "Custom Fields" box
add_action( 'add_meta_boxes', function () {
	remove_meta_box( 'postcustom', 'post', 'normal' );
}, 20 );

For the first example, write a separate output function named myplugin_admin_note_html.

remove_meta_box() only works on things registered as meta boxes. Items such as "Discussion" and "Excerpt" in the block editor sidebar are panels of the editor itself, and WordPress core does not show those meta boxes in the block editor. So remove_meta_box() does not remove block editor panels.

Check saved values with WP-CLI

# Show all meta for post 123 (keys starting with _ are listed too)
wp post meta list 123 --format=table

# Get a single value
wp post meta get 123 _myplugin_price

# Change a value for testing
wp post meta update 123 _myplugin_price 9800

# Find posts with a given value
wp post list --meta_key=_myplugin_sku --meta_value=AB-1 --fields=ID,post_title

When values do not save

  • Values are not saved: confirm that wp_nonce_field() and wp_verify_nonce() use the same action name (the first argument). Also check that the nonce field name (the second argument of wp_nonce_field()) matches the $_POST key.
  • Saving fails for some post types: save_post_post is for post only. Add hooks for the post types you registered the box on.
  • The meta box does not appear on a custom post type: make sure that post type name is in the fourth argument of add_meta_box().

For choosing between this and plugins such as ACF, and for custom field saving problems, see also the following articles.

Related articles

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲