Meta Box Generator

Generate a WordPress meta box: the fields, the nonce, a save handler with its autosave and capability checks, and the meta registration REST needs.

Live output

Enable JavaScript to customise; default output below.

The heading on the box in the editor.

Must be unique. It is also the key users' screen layout preferences are stored against.

The save handler hooks to save_post_<type>, so it never runs for anything else.

Position

side is the narrow column. normal is under the editor, advanced is below that.

More options Show
Priority

Ordering against other boxes in the same position. Users can drag boxes anyway.

Live preview meta-box.php
<?php
/**
 * The "Article Details" meta box.
 */

/**
 * Adds the box to the editor screen.
 */
function my_plugin_add_meta_box() {
	add_meta_box(
		'my_plugin_details',
		__( 'Article Details', 'my-plugin' ),
		'my_plugin_render_meta_box',
		'post',
		'side',
		'default'
	);
}
add_action( 'add_meta_boxes_post', 'my_plugin_add_meta_box' );

/**
 * Registers the meta keys.
 *
 * Doing this means the value is sanitised and permission checked wherever it
 * is written from, not only through this box.
 */
function my_plugin_register_meta() {
	register_post_meta(
		'post',
		'_my_plugin_subtitle',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => false,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => 'my_plugin_can_edit_meta',
		)
	);
	register_post_meta(
		'post',
		'_my_plugin_featured',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => false,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => 'my_plugin_can_edit_meta',
		)
	);
}
add_action( 'init', 'my_plugin_register_meta' );

/**
 * Decides who may write these keys outside the editor.
 *
 * @param bool   $allowed Current decision.
 * @param string $key     Meta key.
 * @param int    $post_id Post ID.
 * @return bool
 */
function my_plugin_can_edit_meta( $allowed, $key, $post_id ) {
	return current_user_can( 'edit_post', $post_id );
}

/**
 * Prints the fields.
 *
 * @param WP_Post $post The post being edited.
 */
function my_plugin_render_meta_box( $post ) {
	wp_nonce_field( 'my_plugin_save_meta', 'my_plugin_nonce' );

	$value = get_post_meta( $post->ID, '_my_plugin_subtitle', true );

	echo '<p>';
	printf(
		'<label class="components-base-control__label" for="%1$s">%2$s</label>'
			. '<input type="text" class="widefat" id="%1$s" name="%1$s" value="%3$s" />',
		esc_attr( '_my_plugin_subtitle' ),
		esc_html__( 'Subtitle', 'my-plugin' ),
		esc_attr( $value )
	);
	echo '<span class="description">' . esc_html__( 'Shown under the title in the archive.', 'my-plugin' ) . '</span>';
	echo '</p>';

	$value = get_post_meta( $post->ID, '_my_plugin_featured', true );

	echo '<p>';
	printf(
		'<label><input type="checkbox" name="%1$s" value="1"%2$s /> %3$s</label>',
		esc_attr( '_my_plugin_featured' ),
		checked( $value, '1', false ),
		esc_html__( 'Feature this article', 'my-plugin' )
	);
	echo '</p>';
}

/**
 * Saves the fields.
 *
 * Every check here has a reason: the nonce proves the request came from this
 * form, the autosave and revision checks stop empty values overwriting real
 * ones, and the capability check stops someone editing a post they cannot.
 *
 * @param int $post_id The post being saved.
 */
function my_plugin_save_meta( $post_id ) {
	if ( ! isset( $_POST['my_plugin_nonce'] ) ) {
		return;
	}

	$nonce = sanitize_text_field( wp_unslash( $_POST['my_plugin_nonce'] ) );

	if ( ! wp_verify_nonce( $nonce, 'my_plugin_save_meta' ) ) {
		return;
	}

	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	if ( wp_is_post_revision( $post_id ) ) {
		return;
	}

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	if ( empty( $_POST['_my_plugin_subtitle'] ) ) {
		delete_post_meta( $post_id, '_my_plugin_subtitle' );
	} else {
		update_post_meta(
			$post_id,
			'_my_plugin_subtitle',
			sanitize_text_field( wp_unslash( $_POST['_my_plugin_subtitle'] ) )
		);
	}

	if ( empty( $_POST['_my_plugin_featured'] ) ) {
		delete_post_meta( $post_id, '_my_plugin_featured' );
	} else {
		update_post_meta( $post_id, '_my_plugin_featured', '1' );
	}
}
add_action( 'save_post_post', 'my_plugin_save_meta' );

Output is valid and updates as you type.

Describe the box and its fields, and the generator writes the whole thing: the registration, the inputs, the nonce, and a save handler with the four checks that stop a meta box quietly destroying data.

How to use

  1. Name the meta keys in lowercase with a prefix. The prefix is what keeps your subtitle apart from the three other plugins that also wanted subtitle.
  2. Keep the leading underscore unless you want the value editable in the Custom Fields panel. Underscore means protected, which hides it from that panel and from REST by default.
  3. Leave the meta registration on. It gives the key a type and a permission check everywhere, not only in this form.
  4. Turn on REST only if the block editor needs to read the value. A protected key needs the auth callback that comes with it.
  5. Paste the file into your plugin. The box, the meta and the save handler all register themselves.

Example

The save handler, which is the part worth reading:

function acme_save_meta( $post_id ) {
	if ( ! isset( $_POST['acme_nonce'] ) ) {
		return;
	}

	$nonce = sanitize_text_field( wp_unslash( $_POST['acme_nonce'] ) );

	if ( ! wp_verify_nonce( $nonce, 'acme_save_meta' ) ) {
		return;
	}

	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	if ( empty( $_POST['_acme_subtitle'] ) ) {
		delete_post_meta( $post_id, '_acme_subtitle' );
	} else {
		update_post_meta( $post_id, '_acme_subtitle', sanitize_text_field( wp_unslash( $_POST['_acme_subtitle'] ) ) );
	}
}
add_action( 'save_post_post', 'acme_save_meta' );

Every early return there is a bug someone has already shipped: no nonce, an autosave with an empty form, or a contributor saving a post they cannot edit.

Pitfalls

  • An autosave posts without your fields. Save without the DOING_AUTOSAVE check and WordPress erases every value a few seconds after the editor opens.
  • save_post fires for every post type. Hooking the generic action instead of save_post_<type> runs your handler on menu items and revisions too.
  • Without a capability check, anyone who can reach the save request can write your meta, including on posts they do not own.
  • An unchecked checkbox sends nothing. Treating a missing key as unchanged means the box can be ticked but never unticked.
  • $_POST values are slashed. Skipping wp_unslash() stores literal backslashes that grow on every save.
  • The block editor does not run meta boxes the way the classic editor did. A box relying on jQuery in the editor screen may load but never fire.
  • A meta key starting with an underscore is protected: it will not appear in the Custom Fields panel and is not exposed through REST without an auth callback.
  • update_post_meta() with an empty string stores an empty row. Deleting instead keeps meta_query and EXISTS checks honest.

Compatibility

add_meta_box(), wp_nonce_field() and the save_post_<type> action have been stable since WordPress 3.0. register_post_meta() needs WordPress 4.9.8, and the REST exposure it enables needs 4.7 or later. The generated code targets PHP 7.0 and up, and the tool runs entirely in your browser.

Frequently asked questions

Why do my values disappear on their own?
Almost always the missing autosave check. WordPress autosaves without your fields, and your handler writes the empty values over the real ones.
Do I still need meta boxes with the block editor?
They work, but a sidebar panel in JavaScript is the native option. A meta box is the quicker path when the value is simple and you are not shipping a build step.
Underscore prefix or not?
Underscore for anything your code owns. Without it, the value shows in the Custom Fields panel and anyone can edit it there by hand.
How do I query by one of these values?
meta_query on the prefixed key. Store numbers as numbers so NUMERIC comparisons behave.
Does register_post_meta() replace the save handler?
No. It types and guards the key; the handler is what reads your form. They cover different paths into the same value.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.