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.
<?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.
Fix the highlighted fields to update the output.
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
- Name the meta keys in lowercase with a prefix. The prefix is what keeps your
subtitleapart from the three other plugins that also wantedsubtitle. - 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.
- Leave the meta registration on. It gives the key a type and a permission check everywhere, not only in this form.
- Turn on REST only if the block editor needs to read the value. A protected key needs the auth callback that comes with it.
- 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_AUTOSAVEcheck and WordPress erases every value a few seconds after the editor opens. save_postfires for every post type. Hooking the generic action instead ofsave_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.
$_POSTvalues are slashed. Skippingwp_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 keepsmeta_queryandEXISTSchecks 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?
Do I still need meta boxes with the block editor?
Underscore prefix or not?
How do I query by one of these values?
meta_query on the prefixed key. Store numbers as numbers so NUMERIC comparisons behave.