The short answer: you need to modify the query for your single event post to include the metadata, so only posts with matching name, year, and month will be returned.
I’ll show you the complete code I used to test this. I made a few changes to your code and simplified a few things, so be careful that all functions are updated.
First, the rewrite tags used in permalinks. No need to modify globals to add these, there’s a function.
function wpd_event_rewrite_tags() {
add_rewrite_tag( '%bf_events_year%', '([0-9]{4})' );
add_rewrite_tag( '%bf_events_month%', '([0-9]{2})' );
}
add_action( 'init', 'wpd_event_rewrite_tags' );
Next is registering the post type. This is where we can set our permalink structure without needing to add rules manually. Note the use of rewrite tags in the event slug. I’ve otherwise simplified this down to the basics for a working example.
function wpd_event_post_type() {
$args = array(
'public' => true,
'label' => 'Events',
'rewrite' => array( 'slug' => 'events/%bf_events_year%/%bf_events_month%' ),
'supports' => array( 'title', 'editor', 'custom-fields' )
);
register_post_type( 'bf_events', $args );
}
add_action( 'init', 'wpd_event_post_type' );
Your post_type_link
function remains largely the same, just with updated rewrite tags:
function wpd_event_permalink($permalink, $post, $leavename) {
if ( get_post_type( $post ) === "bf_events" ) {
$sd = get_post_meta( $post->ID, 'bf_events_startdate', true);
$year = date('Y', $sd + get_option( 'gmt_offset' ) * 3600);
$month = date('m', $sd + get_option( 'gmt_offset' ) * 3600);
$rewritecode = array(
'%bf_events_year%',
'%bf_events_month%',
$leavename ? '' : '%postname%',
);
$rewritereplace = array(
$year,
$month,
$post->post_name
);
$permalink = str_replace($rewritecode, $rewritereplace, $permalink);
}
return $permalink;
}
add_filter( 'post_type_link', 'wpd_event_permalink', 10, 4 );
The final piece of the puzzle is where the magic happens. Here we add the month and year query vars into a meta query. Adding this also kicks in the canonical redirect, so it redirects to the correct year/month instead of returning a 404.
function wpd_single_event_queries( $query ){
if( $query->is_singular()
&& $query->is_main_query()
&& isset( $query->query_vars['bf_events'] ) ){
$meta_query = array(
array(
'key' => 'bf_events_year',
'value' => $query->query_vars['bf_events_year'],
'compare' => '=',
'type' => 'numeric',
),
array(
'key' => 'bf_events_month',
'value' => $query->query_vars['bf_events_month'],
'compare' => '=',
'type' => 'numeric',
),
);
$query->set( 'meta_query', $meta_query );
}
}
add_action( 'pre_get_posts', 'wpd_single_event_queries' );