For a while now I've been applying and updating some custom metaboxes that my company re-uses on different sites. (Not the same metaboxes, but the same methodology.)

Essentially it allows for the 'association' of individual posts of different custom post-types to be assigned/associated with default WP pages and posts.

So, for example Post ID = 449 which is a of Post Type = CustomA can be assigned to any page or post.

What I have built, works. It's worked for a long time. And it still works.

What we've started to see however is that on the odd occasion, one of the Pages/Posts will see all of this custom post meta simply purged from the page.

I have never been able to replicate the error myself. I'm just given URLs to edit screens of posts or pages where the custom post meta was assigned, and now it's gone.

I have tried repeatedly to force the issue and try and trigger something that will make it happen while I'm monitoring the developer console, but I can't trigger it.

Error logs are empty. (There's the odd warning every couple of days, but nothing that pertains to any of the posts or functions in question.)

    function cpt_add_cpt_selection() {
        $screens = ['page', 'post'];
        foreach( $screens as $screen ) {
                'Assign A Custom Post Type',
                    '__block_editor_compatible_meta_box' => true,
                    '__back_compat_meta_box'             => false,
    add_action( 'add_meta_boxes', 'cpt_add_cpt_selection' );
    function cpt_assign_metabox( $post ) {
        wp_nonce_field( basename( __FILE__ ), 'cpt_nonce' );
        $cpt_position               = get_post_meta( $post->ID, 'cpt_position', true );
        $cpt_appended_cpts          = get_post_meta( $post->ID, 'cpt_cpts', false );
        <label for="cpt_position_sidebar" style="display:block;margin-bottom:0.5rem;">
            <input type="radio" id="cpt_position_sidebar" name="cpt_position" value="sidebar" <?php checked( 'sidebar' == $cpt_position ); ?>/> Display in Sidebar
        <label for="cpt_position_content" style="display:block;margin-bottom:0.5rem;">
            <input type="radio" id="cpt_position_content" name="cpt_position" value="content" <?php checked( 'content' == $cpt_position ); ?>/> Display below Content
        <select id="cpt_cpts" name="cpt_cpts[]" multiple="multiple" style="width:90%;">
        if( $cpt_appended_cpts ) {
            foreach( $cpt_appended_cpts as $cpt_ids ) {
                foreach( $cpt_ids as $cpt_id ) {
                    $cpt_title = get_the_title( $cpt_id );
                    $cpt_title = ( mb_strlen( $cpt_title ) > 50 ) ? mb_substr( $cpt_title, 0, 49 ) . '...' : $cpt_title;
                    echo '<option value="' . $cpt_id . '" selected="selected">' . $cpt_title . '</option>';
    <?php }
    function cpt_assign_save_postdata( $post_id ) {
        if( isset( $_POST['cpt_cpts'] ) ) {
            update_post_meta( $post_id, 'cpt_cpts', $_POST['cpt_cpts'] );
        } else {
            delete_post_meta( $post_id, 'cpt_cpts' );
        if( isset( $_POST['cpt_position'] ) ) {
            update_post_meta( $post_id, 'cpt_position', $_POST['cpt_position'] );
        } else {
            delete_post_meta( $post_id, 'cpt_position' );
    add_action( 'save_post', 'cpt_assign_save_postdata' );
    function cpt_get_cpts_ajax_callback() {
        $return = array();
        $search_results = new WP_Query( array(
            's'                     => $_GET['q'],
            'post_type'             => 'the_custom_cpt_in_question',
            'post_status'           => 'publish',
            'ignore_sticky_posts'   => 1,
            'posts_per_page'        => -1
        ) );
        if( $search_results->have_posts() ) :
            while( $search_results->have_posts() ) : $search_results->the_post();
                $title = ( mb_strlen( $search_results->post->post_title ) > 50 ) ? mb_substr( $search_results->post->post_title, 0, 49 ) . '...' : $search_results->post->post_title;
                $return[] = array( $search_results->post->ID, $title );
        echo json_encode( $return );
    add_action( 'wp_ajax_getcustompt', 'cpt_get_cpts_ajax_callback' );

Like I said, it's actually working. Every day across numerous large sites, people are adding and removing a number of Custom Post Types from being associated with different posts and pages. The CPTs themselves are stuff like 'FAQs' or 'Team Members' and the themes being used have template tags applied that will display the contents of the CPTs if any have been assigned to pages. It all works.

The one anomaly is that maybe once every 3-4 weeks, a page that HAD these fields populated, including the field that assigns whether the content should be applied to either the sidebar or below the content, is simply being purged from the postmeta table.

(Two different template tags are used that run a conditional check before retrieving any content to determine whether the assigned CPTs should be displayed in the sidebar or beneath the content.)

My two theories, neither of which I can replicate are:

People are navigating away from the page before the 'Update' process is finished. But I keep trying this and every time I do it, the page still has the custom post meta.

We use WPML and people will have multiple tabs open and often across different languages (all of our client sites are bilingual) and I've noticed that this sometimes causes things to get confused in the back end... ...you start editing in one language, go to check something in a new tab with a second language, go back to the first page you were editing and your searches no longer work, because the search is executing in language of the tab you most recently opened, which set a language cookie.

So, my question is, does anyone see something in my metaboxes and save process that could result in occasional purging of postmeta on posts/pages?

(Patiently waits for SallyCJ. ;-P)

P.S. I know there is a different way of building these metaboxes, and am planning on doing it, but with my current workload I do not have the time to stop working on new projects and go revisit something that ostensibly works. I also don't know if using the new React based metaboxes will address the issue we're intermittently experiencing.

1 Answer 1


The code uses the save_post hook to save the results of the form, however, at no point did it check which post it was saving.

This means your save function runs for that post, but it also runs for drafts, autosaves, etc

Then, on top of that there are checks like this:

        if( isset( $_POST['cpt_cpts'] ) ) {
            update_post_meta( $post_id, 'cpt_cpts', $_POST['cpt_cpts'] );
        } else {
            delete_post_meta( $post_id, 'cpt_cpts' );

If cpt_cpts is undefined it will delete the post meta, causing the problem you are experiencing. This is incorrect, cpt_cpts being undefined doesn't mean that cpt_cpts needs to be deleted. This may not be a submission of the post form but a minor update from an unrelated piece of code such as a plugin.

A quick way of reproducing this should be using the quick edit feature, this would update the post even though your select input is not on the page or a part of the update process.

Instead the save handler also needs to check for this, e.g. by checking if the nonce exists, or that another form input exists, as well as the post type of the post being saved, and other details.

  • Just did a quick test and it's the Quick Edit. I'm going to review some old version of this methodology and see if and where the divergence happened. Everything you highlighted is correct. So now I'm trying to figure out why it's not implemented here. (Edit: I checked my original implementation of this and there is a check for the post_id before executing the save. No clue why it's not being applied in later iterations. I don't even know if it was me or another dev. FFS! So obvious too. Thanks Tom.) Commented Jan 26, 2022 at 19:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.