Adding Wordpress Meta boxes I am adding two custom metaboxes to the WordPress post page from a plugin. I am currently doing it with two separate instances of add_meta_boxes but this feels very bulky, especially as I go to add more.

Since the plugin is fairly small I dont want to include a framework and was wondering if there was an easier to add more than one metabox. Preferably under one heading, unlike currently if you add this to your functions.php or look at the screenshot below, they are under separate headings.

I was considering posting this on http://wordpress.stackexchange.com, but as it is working fully I posted it here.

function wp_cat_map_add_meta_box() {

$screens = array( 'post' );

foreach ( $screens as $screen ) {
    add_meta_box( 'map_lat', __( 'Add Latitude', 'wp_cat_map' ), 'wp_cat_map_lat_callback', $screen );
    add_meta_box( 'map_long', __( 'Add Longitude', 'wp_cat_map' ), 'wp_cat_map_long_callback', $screen );

}
}
add_action( 'add_meta_boxes', 'wp_cat_map_add_meta_box' );

function wp_cat_map_lat_callback( $post ) {

wp_nonce_field( 'wp_cat_map_lat_data', 'wp_cat_map_lat_nonce' );

$value = get_post_meta( $post-&gt;ID, '_wp_cat_map_lat', true );

echo '&lt;label for="wp_cat_map_lat"&gt;';
_e( 'Latitude', 'wp_cat_map' );
echo '&lt;/label&gt; ';
echo '&lt;input type="text" id="wp_cat_map_lat" name="wp_cat_map_lat" value="' . esc_attr( $value ) . '"/&gt;';
}

function wp_cat_map_lat_data( $post_id ) {

if ( ! isset( $_POST['wp_cat_map_lat_nonce'] ) ) {
    return;
}

if ( ! wp_verify_nonce( $_POST['wp_cat_map_lat_nonce'], 'wp_cat_map_lat_data' ) ) {
    return;
}

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

if ( isset( $_POST['post_type'] ) &amp;&amp; 'page' == $_POST['post_type'] ) {

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

} else {

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

if ( ! isset( $_POST['wp_cat_map_lat'] ) ) {
    return;
}

$my_data = sanitize_text_field( $_POST['wp_cat_map_lat'] );

update_post_meta( $post_id, '_wp_cat_map_lat', $my_data );
}

function wp_cat_map_long_callback( $post ) {

wp_nonce_field( 'wp_cat_map_long_data', 'wp_cat_map_long_nonce' );

$value = get_post_meta( $post-&gt;ID, '_wp_cat_map_long', true );

echo '&lt;label for="wp_cat_map_long"&gt;';
_e( 'Longitude', 'wp_cat_map' );
echo '&lt;/label&gt; ';
echo '&lt;input type="text" id="wp_cat_map_long" name="wp_cat_map_long" value="' . esc_attr( $value ) . '"/&gt;';
}

function wp_cat_map_long_data( $post_id ) {

if ( ! isset( $_POST['wp_cat_map_long_nonce'] ) ) {
    return;
}

if ( ! wp_verify_nonce( $_POST['wp_cat_map_long_nonce'], 'wp_cat_map_long_data' ) ) {
    return;
}

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

if ( isset( $_POST['post_type'] ) &amp;&amp; 'page' == $_POST['post_type'] ) {

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

} else {

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

if ( ! isset( $_POST['wp_cat_map_long'] ) ) {
    return;
}

$my_data = sanitize_text_field( $_POST['wp_cat_map_long'] );

update_post_meta( $post_id, '_wp_cat_map_long', $my_data );
}
add_action( 'save_post', 'wp_cat_map_lat_data' );
add_action( 'save_post', 'wp_cat_map_long_data' );
First thing that pops in the eye is the unindented code, most IDEs do this automatically for us. That's fundamental for everyone's sanity, yours and whomever reads your code.
And you are missing two things from add_meta_box() parameters that will make the code reusable:

add_meta_box( $id, $title,$callback, $screen, $context, $priority, $callback_args );

One is that $screen can be an array, so the foreach $screens is unnecessary. And the other is the normally forgotten $callback_args, with it we can prepare an array that will build all our metaboxes using the same $callback function.
Notes:

$metaboxes is the reusable source, just add another item and a new metabox is created.
save_post receives 2 arguments, $post_id and $post_object.
I prefer to put all hook declarations at the beginning, as it gives a general panorama of all functions below.
Use of printf() to build complex HTML blocks.


add_action( 'add_meta_boxes', 'wp_cat_map_add_meta_box' );
add_action( 'save_post', 'wp_cat_map_data', 10, 2 );

function wp_cat_map_add_meta_box() {
    /* Our configuration vars */
    $screens = array( 'post', 'page' );
    $metaboxes = array(
        'map_lat' =&gt; array(
            'label' =&gt; 'wp_cat_map_lat',
            'title' =&gt; __( 'Add Latitude', 'wp_cat_map' ),
            'description' =&gt; __( 'Latitude', 'wp_cat_map' ),
            'name' =&gt; 'wp_cat_map_lat',
            'post_meta' =&gt; '_wp_cat_map_lat'
        ),
        'map_long' =&gt; array(
            'label' =&gt; 'wp_cat_map_long',
            'title' =&gt; __( 'Add Longitude', 'wp_cat_map' ),
            'description' =&gt; __( 'Longitude', 'wp_cat_map' ),
            'name' =&gt; 'wp_cat_map_long' ,
            'post_meta' =&gt; '_wp_cat_map_long'           
        )
    );
    foreach ( $metaboxes as $meta_id =&gt; $meta_cont ) {
        add_meta_box( 
            $meta_id, 
            $meta_cont['title'], 
            'wp_cat_map_callback', 
            $screens, 
            'advanced', 
            'default', 
            $meta_cont 
        );
    }
}

function wp_cat_map_callback( $post, $post_object ) {
    /* Inspect the object */
    // printf( '&lt;pre&gt;&lt;code&gt;%s&lt;/code&gt;&lt;/pre&gt;', print_r( $post_object, true ) );
    wp_nonce_field( 'wp_cat_map_data', 'wp_cat_map_nonce' );
    $value = get_post_meta( $post-&gt;ID, $post_object['args']['post_meta'], true );
    printf( 
        '&lt;label for=&quot;%s&quot;&gt;%s&lt;/label&gt;',
        $post_object['args']['label'],
        $post_object['args']['description'];
    );
    printf(
        '&lt;input type=&quot;text&quot; id=&quot;%s&quot; name=&quot;%s&quot; value=&quot;%s&quot;/&gt;',
        $post_object['args']['label'],
        $post_object['args']['post_meta'],
        esc_attr( $value )
    );
}

function wp_cat_map_data( $post_id, $post_object ) { /* code */ }


Issue:
We need those meta names to use in the save_post callback. Instead of creating a global variable, it's preferable to create a class to encapsulate everything. This way we can send Screens and Meta Boxes when instantiating the class and have post_meta names available inside the save_post callback.
Class Code_Review_111019 {
    /* Class properties, available to all methods */
    private $screens;
    private $metaboxes;
    
    public function __construct( $screens, $metaboxes ) {
        /* Set properties when instantiating */
        $this-&gt;screens = $screens;
        $this-&gt;metaboxes = $metaboxes;
        /* Start our hooks at a safer point */
        add_action( 'plugins_loaded', array( $this, 'start_up' ) );
    }
    
    public function start_up() {
        /* Inside classes we don't need unique prefixes or names for the callbacks, better yet, they can be literal */
        add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
        add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );        
    }
    
    public function add_meta_boxes() {
        foreach ( $this-&gt;metaboxes as $meta_id =&gt; $meta_cont ) {
            add_meta_box( 
                $meta_id, 
                $meta_cont['title'], 
                array( $this, 'build_metabox' ), 
                $this-&gt;screens, 
                'advanced', 
                'default', 
                $meta_cont 
            );
        }
    }

    public function build_metabox( $post, $post_object ) {
        wp_nonce_field( 'wp_cat_map_data', 'wp_cat_map_nonce' );
        $value = get_post_meta( $post-&gt;ID, $post_object['args']['post_meta'], true );
        printf( 
            '&lt;label for=&quot;%s&quot;&gt;%s&lt;/label&gt;',
            $post_object['args']['label'],
            $post_object['args']['description']
        );
        printf(
            '&lt;input type=&quot;text&quot; id=&quot;%s&quot; name=&quot;%s&quot; value=&quot;%s&quot;/&gt;',
            $post_object['args']['label'],
            $post_object['args']['post_meta'],
            esc_attr( $value )
        );
    }

    /* NO CHECKS OR SECURITY WHATSOEVER, FOR DEBUGGING PURPOSES ONLY */
    public function save_post( $post_id, $post_object ) { 
        foreach( $this-&gt;metaboxes as $mbox ){
            if( isset( $_POST[ $mbox['post_meta'] ] ) ) {
                update_post_meta( $post_id, $mbox['post_meta'], $_POST[ $mbox['post_meta'] ] );
            }
        }
    }
}

$Code_Review_111019 = new Code_Review_111019(
    /* Screens */
    array( 'post', 'page', 'portfolio' ),
    /* Metaboxes */
    array(
        'map_lat' =&gt; array(
            'label' =&gt; 'wp_cat_map_lat',
            'title' =&gt; __( 'Add Latitude', 'wp_cat_map' ),
            'description' =&gt; __( 'Latitude', 'wp_cat_map' ),
            'name' =&gt; 'wp_cat_map_lat',
            'post_meta' =&gt; '_wp_cat_map_lat'
        ),
        'map_long' =&gt; array(
            'label' =&gt; 'wp_cat_map_long',
            'title' =&gt; __( 'Add Longitude', 'wp_cat_map' ),
            'description' =&gt; __( 'Longitude', 'wp_cat_map' ),
            'name' =&gt; 'wp_cat_map_long' ,
            'post_meta' =&gt; '_wp_cat_map_long'           
        ),
        'map_zoom' =&gt; array(
            'label' =&gt; 'wp_cat_map_zoom',
            'title' =&gt; __( 'Add Zoom', 'wp_cat_map' ),
            'description' =&gt; __( 'Zoom', 'wp_cat_map' ),
            'name' =&gt; 'wp_cat_map_zoom' ,
            'post_meta' =&gt; '_wp_cat_map_zoom'           
        )           
    )
);


Suggestion:
Instead of one Meta Box for each piece of information, I think it's better UX to have a single box for your plugin/feature and add the form fields there.