Meta Box Generator

Fields in the editor, and a save handler that checks the nonce, the autosave, the revision and the capability — in that order, before it writes anything.

acme-event-details.php
Saved just now
The box
Post types
Fields
4 fields · keys prefixed _acme_
Start date_acme_start_date
Venue_acme_venue
Format_acme_format
Sold out_acme_sold_out
Save handler
Nonce check
wp_nonce_field() in the box and wp_verify_nonce() before saving.
Autosave and revision guard
Bails on autosaves and revisions, which otherwise blank your fields.
Capability check
current_user_can( edit_post, $post_id ) before writing.
Post type check
Skips the handler on saves of unrelated post types.
register_post_meta()
Registers the keys with show_in_rest so blocks and the editor can read them.
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io).
/**
 * Register the meta box.
 */
function acme_add_box() {
	add_meta_box(
		'acme_event_details',
		__( 'Event details', 'acme' ),
		'acme_render',
		array( 'post', 'event' ),
		'side',
		'default'
	);
}
add_action( 'add_meta_boxes', 'acme_add_box' );

/**
 * Print the box.
 *
 * @param WP_Post $post Post being edited.
 */
function acme_render( $post ) {
	wp_nonce_field( 'acme_save_meta', 'acme_meta_nonce' );

	$start_date = get_post_meta( $post->ID, '_acme_start_date', true );
	printf(
		'<p><label for="acme_start_date"><strong>%1$s</strong></label><br /><input type="date" id="acme_start_date" name="acme_start_date" value="%2$s" class="widefat" />%3$s</p>',
		esc_html( __( 'Start date', 'acme' ) ),
		esc_attr( $start_date ),
		'<span class="description">' . esc_html( __( 'When the event begins.', 'acme' ) ) . '</span>'
	);

	$venue = get_post_meta( $post->ID, '_acme_venue', true );
	printf(
		'<p><label for="acme_venue"><strong>%1$s</strong></label><br /><input type="text" id="acme_venue" name="acme_venue" value="%2$s" class="widefat" />%3$s</p>',
		esc_html( __( 'Venue', 'acme' ) ),
		esc_attr( $venue ),
		''
	);

	$format = get_post_meta( $post->ID, '_acme_format', true );
	echo '<p><label for="acme_format"><strong>' . esc_html( __( 'Format', 'acme' ) ) . '</strong></label><br />';
	echo '<select id="acme_format" name="acme_format">';

	foreach ( array(
		'online' => __( 'Online', 'acme' ),
		'venue'  => __( 'In person', 'acme' ),
		'hybrid' => __( 'Hybrid', 'acme' ),
	) as $value => $label ) {
		printf(
			'<option value="%1$s"%2$s>%3$s</option>',
			esc_attr( $value ),
			selected( $format, $value, false ),
			esc_html( $label )
		);
	}

	echo '</select></p>';

	$sold_out = get_post_meta( $post->ID, '_acme_sold_out', true );
	printf(
		'<p><label for="acme_sold_out"><input type="checkbox" id="acme_sold_out" name="acme_sold_out" value="1"%1$s /> %2$s</label>%3$s</p>',
		checked( (bool) $sold_out, true, false ),
		esc_html( __( 'Sold out', 'acme' ) ),
		'<span class="description">' . esc_html( __( 'Hides the booking button on the front end.', 'acme' ) ) . '</span>'
	);
}

/**
 * Save the fields.
 *
 * @param int $post_id Post being saved.
 */
function acme_save( $post_id ) {
	if ( ! isset( $_POST['acme_meta_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['acme_meta_nonce'] ), 'acme_save_meta' ) ) {
		return;
	}

	if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
		return;
	}

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

	if ( ! in_array( get_post_type( $post_id ), array( 'post', 'event' ), true ) ) {
		return;
	}

	if ( isset( $_POST['acme_start_date'] ) ) {
		$value = sanitize_text_field( wp_unslash( $_POST['acme_start_date'] ) );

		if ( '' === $value ) {
			delete_post_meta( $post_id, '_acme_start_date' );
		} else {
			update_post_meta( $post_id, '_acme_start_date', $value );
		}
	}

	if ( isset( $_POST['acme_venue'] ) ) {
		$value = sanitize_text_field( wp_unslash( $_POST['acme_venue'] ) );

		if ( '' === $value ) {
			delete_post_meta( $post_id, '_acme_venue' );
		} else {
			update_post_meta( $post_id, '_acme_venue', $value );
		}
	}

	if ( isset( $_POST['acme_format'] ) && in_array( sanitize_key( $_POST['acme_format'] ), array( 'online', 'venue', 'hybrid' ), true ) ) {
		update_post_meta( $post_id, '_acme_format', sanitize_key( $_POST['acme_format'] ) );
	}

	if ( ! empty( $_POST['acme_sold_out'] ) ) {
		update_post_meta( $post_id, '_acme_sold_out', 1 );
	} else {
		delete_post_meta( $post_id, '_acme_sold_out' );
	}
}
add_action( 'save_post', 'acme_save' );

/**
 * Expose the meta to the REST API so blocks and the editor can read it.
 */
function acme_register_meta() {
	register_post_meta(
		'post',
		'_acme_start_date',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);

	register_post_meta(
		'post',
		'_acme_venue',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);

	register_post_meta(
		'post',
		'_acme_format',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'sanitize_key',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);

	register_post_meta(
		'post',
		'_acme_sold_out',
		array(
			'type'              => 'boolean',
			'single'            => true,
			'show_in_rest'      => true,
			'sanitize_callback' => 'rest_sanitize_boolean',
			'auth_callback'     => function () {
				return current_user_can( 'edit_posts' );
			},
		)
	);
}
add_action( 'init', 'acme_register_meta' );

About this generator

Meta Box Generator Online

A WordPress meta box generator that writes all three halves of the job: the add_meta_box() registration, the render callback that prints escaped, labelled inputs, and a save_post handler with a nonce check, an autosave guard, a capability check and a post type check. Optionally it also emits the matching register_post_meta() calls so the values reach the REST API and the block editor.

A Preview tab renders the panel roughly as it will look in the editor sidebar, so you can see the field order and labels before pasting anything. Procedural functions or a single class — your choice. Free, no account, nothing uploaded.

Reviewed Output tested on WordPress 6.8 · PHP 8.3

Why the meta box generator beats a copied add_meta_box() tutorial

The registration is the easy part. What sinks hand-written meta boxes is the save handler: save_post fires for autosaves, for every revision, for quick edits and for other post types, and it fires whether or not the request came from your form. Miss one guard and the field silently empties itself thirty seconds after the author types into it. This generator treats those four guards as errors, not as optional extras.

Four save guards, each an error if missing

The nonce check, the autosave and revision guard, current_user_can( 'edit_post', $post_id ) and the post type check are all reported in the Checks tab with a one-click fix. The autosave guard tests wp_is_post_autosave(), wp_is_post_revision() and the DOING_AUTOSAVE constant, because they do not all catch the same request.

A sanitiser and a REST type per field

Text uses sanitize_text_field(), textarea sanitize_textarea_field(), number absint(), checkbox rest_sanitize_boolean(), select sanitize_key(), URL esc_url_raw(), email sanitize_email() — and each maps to the right JSON Schema type when the register_post_meta() output is on.

Selects saved against their own whitelist

A select field is written as an in_array() check against the exact choices you entered, so a forged request cannot store a value the dropdown never offered. A select with no choices is an error with an Add two choices fix.

Empty values deleted, not stored

Every text-like field is saved with update_post_meta() when it has a value and delete_post_meta() when it does not, so an emptied field leaves no orphan row behind for get_post_meta() to return.

Protected keys handled deliberately

Meta keys are prefixed, and a prefix starting with an underscore keeps the values out of the Custom Fields panel. Leave the underscore off and the generator says so — with an Add the underscore fix — rather than letting you discover it on a client site.

Procedural or a class, same guards

Switch the code style and the identical logic is emitted either as prefixed functions with their own add_action() calls, or as a final class with a hooks() method and array( $this, ... ) callbacks. Nothing is dropped in the translation.

How does the meta box generator work?

Describe the panel, list the fields, choose which guards to write, then export.

  1. 01

    Describe the box

    Set the title shown in the panel header, the box id used by user screen preferences and remove_meta_box(), the context (normal, side or advanced) and the priority, plus the function prefix, text domain and meta key prefix.

  2. 02

    Choose the post types

    Pick from post, page, product and event, or type any custom post type slug. The list feeds both the add_meta_box() screen argument and the get_post_type() check in the save handler.

  3. 03

    Add the fields

    Each field gets a key, a label, a type, an optional description shown under the input, and a comma-separated choice list for selects using value:Label pairs. Drag to reorder; the render and save code follow the order.

  4. 04

    Confirm the save handler, then export

    Leave all four guards on, decide whether to emit register_post_meta(), clear the Checks tab, then copy the snippet or download it as a plugin file.

Worked example — the save handler for a one-field ISBN box

A Book details box on the book post type with a single text field. This is the save_post handler exactly as it is generated; the add_meta_box() call and the render function sit above it in the same file.

/**
 * Save the fields.
 *
 * @param int $post_id Post being saved.
 */
function acme_save( $post_id ) {
	if ( ! isset( $_POST['acme_meta_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['acme_meta_nonce'] ), 'acme_save_meta' ) ) {
		return;
	}

	if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
		return;
	}

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

	if ( ! in_array( get_post_type( $post_id ), array( 'book' ), true ) ) {
		return;
	}

	if ( isset( $_POST['acme_isbn'] ) ) {
		$value = sanitize_text_field( wp_unslash( $_POST['acme_isbn'] ) );

		if ( '' === $value ) {
			delete_post_meta( $post_id, '_acme_isbn' );
		} else {
			update_post_meta( $post_id, '_acme_isbn', $value );
		}
	}
}
add_action( 'save_post', 'acme_save' );

The order of the guards matters. The nonce proves the request came from your form; the capability check proves this user may edit this post. Neither substitutes for the other, and both run before anything is written.

Meta boxes — frequently asked questions

The questions that follow every first meta box, usually in this order.

Why do my meta box values disappear when the editor autosaves?

Because save_post fires for the autosave too, and the autosave request does not include your form fields. Without a guard the handler reads nothing, sanitises to an empty string, and deletes the value. Test wp_is_post_autosave(), wp_is_post_revision() and the DOING_AUTOSAVE constant and return early — the generator writes all three and treats a missing guard as an error.

Do I really need a nonce if I already check user capabilities?

Yes, they answer different questions. The nonce proves the request came from your form rather than from a crafted link on another site; the capability check proves that this particular user is allowed to edit this particular post. A handler with only one of the two is exploitable in one of the two ways.

Do meta boxes still work in the block editor?

Yes. Boxes registered with add_meta_box() render in a compatibility area below the content, and the save handler runs as normal. They cannot appear in the block editor sidebar though. If you need that, register the value with register_post_meta() and build a small sidebar plugin against it — the generator can emit the registration calls for you.

Why do my custom fields not appear in the Custom Fields panel?

Because the key starts with an underscore. WordPress treats an underscore-prefixed meta key as protected and hides it from that panel and from get_post_custom() output for display. That is usually what you want for values a box owns, and it is the generator default; drop the underscore if editors should be able to change the raw value by hand.

What is the difference between the normal, side and advanced context?

normal places the box under the editor, side puts it in the right-hand column beside Publish, and advanced puts it below the normal boxes. On pages in particular, a normal box sits far enough down the screen that clients scroll straight past it, which is why one or two fields usually belong in side.

How do I remove a meta box added by a plugin or by core?

Call remove_meta_box( $id, $screen, $context ) on the add_meta_boxes hook at a late priority. That is why the box id matters: it is the handle for removal, and it is also the key WordPress stores when a user hides the panel from Screen Options.

Where do I paste the code from the Meta Box 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 Content generators in the library, plus the WordPress tools most often used alongside this one.