Stop post submission without losing data?

I had to do the exact same thing with a custom post type that would be useless and cause theme problems without metadata.

You’ll have to either

A.

Check the post for metadata AFTER it has been published using the transition_post_status hook, and if the metadata isn’t there, change the status to “draft” and throw an error message, prompting them with a link to go back an add it. This way involves a separate error page that can be styled at will with multiple messages for multiple post requirements. My code:

// Validation - make sure teams and team news have the required fields before publishing
    // https://wordpress.stackexchange.com/a/187999/127459
    // https://developer.wordpress.org/reference/hooks/transition_post_status/#comment-244

    // the only issue with this is that it sometimes failed to register changes unless refreshed, so errors that are subsequently fixed would still throw an error until the page is refreshed.
    add_action( 'transition_post_status', 'team_post_changed', 10, 3 );

    function team_post_changed( $new_status, $old_status, $post ) {
        
            // only check Teams or Team News Posts
            if ( $post->post_type === 'team' || $post->post_type === 'team-news' ) {
                
                // only if it is being published or saved
                if ($new_status === 'publish') {
                    
                    $count = 0;
                    $sports = wp_get_object_terms($post->ID, 'sport');
                    $gender = wp_get_object_terms($post->ID, 'gender');
                    $meta = get_post_meta($post->ID);
                    $thisCoach = $meta['_coach_name'][0];

                    if ( $post->post_type === 'team') {$thisPostType="Team";
                    } else {$thisPostType="Team News Post";}

                    $die_message="<em>The ".$thisPostType.' was not published because of the following errors:</em><hr>';
                    $die_title="Error - Missing ";
                    
                    // Team / Team News must have Sport
                    if (!$sports[1]) {
                        
                        $die_message .= '<br><h3>Error - Please Specify a Sport</h3>';
                        $die_message .= '<p>'.$thisPostType.'s must be assigned a Season and Sport before they can be published. If the sport for this team is not avaialable, create a new sport <a href="'.admin_url('edit-tags.php?taxonomy=sport&post_type=team').'">here</a>, under the correct season.</p>';
                        $die_title .= 'Sport';
                        $count += 1;

                    } 
                    // Teams must also have Gender
                    if ( $post->post_type === 'team') {
                        if (!$gender[0]){

                            if ($count === 1){ $die_title .= ' and ';}
                            $die_message .= '<br><h3>Error - Please Specify a Team Gender</h3>';
                            $die_message .= '<p>'.$thisPostType.'s must be assigned a Gender before they can be published.</p>';
                            $die_title .= 'Gender';
                            $count += 1;

                        }
                    }
                    if ($count > 0) {
                        // keep post from publishing
                        $post->post_status="draft";
                        wp_update_post($post);
                        
                        // provide error message
                        $die_message .= '<br><hr><p><a href="' . admin_url('post.php?post=" . $post->ID . "&action=edit') . '">Go back and edit the post</a></p>';
                        wp_die($die_message, $die_title);
                    }
            
                }
            }
    }

or

B.

Check the post content via Ajax BEFORE it has been published (immediately when the Publish button is pressed) and alert the user on the same screen unless the post is completely valid. This method requires Javascript and Jquery. It alerts the user with a browser modal-type alert which prevents further actions on the page until you press “OK”. See the original answer where I found this solution, and another like it. Here’s my code that works for me:

        // Validate Teams and Team News Posts Before Publishing
    // modified from https://wordpress.stackexchange.com/a/42709/127459

    add_action('admin_head-post.php','ep_publish_admin_hook');
    add_action('admin_head-post-new.php','ep_publish_admin_hook');
    function ep_publish_admin_hook(){
        global $post;
        if ( is_admin() && ($post->post_type == 'team' || $post->post_type == 'team-news') ){
            ?>
            <script language="javascript" type="text/javascript">
                
                jQuery(document).ready(function() {
                    console.log('running this now');
                    jQuery('#publish').click(function() {
                        if(jQuery(this).data("valid")) {
                            return true;
                        }
                        var form_data = jQuery('#post').serializeArray();
                        var data = {
                            action: 'ep_pre_submit_validation',
                            security: '<?php echo wp_create_nonce( 'pre_publish_validation' ); ?>',
                            form_data: jQuery.param(form_data),
                        };
                        jQuery.post(ajaxurl, data, function(response) {
                            if (response.indexOf('true') > -1 || response == true) {
                                jQuery("#post").data("valid", true).submit();
                            } else {
                                alert("Error: " + response);
                                jQuery("#post").data("valid", false);

                            }
                            //hide loading icon, return Publish button to normal
                            jQuery('#ajax-loading').hide();
                            jQuery('#publish').removeClass('button-primary-disabled');
                            jQuery('#save-post').removeClass('button-disabled');
                        });
                        return false;
                    });
                });
            </script>
            <?php
        }
    }

    add_action('wp_ajax_ep_pre_submit_validation', 'ep_pre_submit_validation');
    function ep_pre_submit_validation() {
        //simple Security check (checks for post-type team or team-news, etc, via scripts above in ep_publish_admin_hook)
        check_ajax_referer( 'pre_publish_validation', 'security' );

        //convert the string of data received to an array
        //from https://wordpress.stackexchange.com/a/26536/10406
        parse_str( $_POST['form_data'], $vars );
        // _e(print_r($vars));
    
        //check that are actually trying to publish a post
        if ( $vars['post_status'] == 'publish' || 
            (isset( $vars['original_publish'] ) && 
            in_array( $vars['original_publish'], array('Publish', 'Schedule', 'Update') ) ) ) {
                
            // Check that post title is set
            if (!isset($vars['post_title'])) {
                // _e(print_r($vars)); // uncomment to see what is included in this array
                _e('Please provide a post title.');
                die();
            }
            // Check that gender is set
            if (!isset($vars['gender'])) {
                // _e(print_r($vars)); // uncomment to see what is included in this array
                _e('Please specify a gender.');
                die();
            }
            
        }

        //everything ok, allow submission
        echo 'true';
        die();
    }

Note: I got both solutions working for me, except that solution ‘A’, in my implementation, required a browser reload after changes were made, to update the error message (for example, if you go back and fix all errors, the publish buttom still triggers the error page, but when you reload the page, it publishes the post). So then I found solution B, and am customizing it for my needs, with multiple validations on multiple post types.