Permalinks using event date (year & month) instead of publication date

The site http://www.bikefun.org is live and working, but the permalinks are not quite right. It is an event calendar site. Events are stored as a custom post type “bf_events”, with dates stored as integer postmeta values – integer as produced by strtotime().

I’m using a permalink structure /events/2014/09/cyclewise-confidence-training-intermediate-2/ where the year and month are those of the event date, not the publication date of the post. This is sort of working, but in practice only the event title is being used by WP, and the number “2” at the end has been added by WP to keep the permalink unique. If you alter the year or month, the same page loads, whereas you should get a 404.

Here is the code:

function event_rewrite_rules() {
    global $wp_rewrite;

    $wp_rewrite->add_rewrite_tag( '%eventyear%', '([0-9]{4})', 'bf_events_year=');
    $wp_rewrite->add_rewrite_tag( '%eventmonth%', '([0-9]{2})', 'bf_events_month=');
    // $wp_rewrite->add_permastruct('bf_events', '/events/%bf_events_year%/%bf_events_month%/%postname%', false);
    add_rewrite_rule(  '^events/([0-9]{4})/([0-9]{2})/([^/]+)?', 'index.php??post_type=bf_events&pagename=$matches[2]', 'top' );
}
add_action('init', 'event_rewrite_rules');

I commented the add_permastruct call because it broke everything.

add_filter('post_type_link', 'event_permalink', 10, 4);

function 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(
            '%eventyear%',
            '%eventmonth%',
            $leavename? '' : '%postname%',
        );

        $rewritereplace = array(
            $year,
            $month,
            $post->post_name
        );

        $permalink = str_replace($rewritecode, $rewritereplace, $permalink);
    }
    return $permalink;
}

To make it possible for WP to use year and month in the permalink, these are stored as additional postmeta values in the meta-save function:

update_post_meta($post->ID, "bf_events_startdate", $updatestartd );
update_post_meta($post->ID, "bf_events_year", date("Y", $updatestartd ) );
update_post_meta($post->ID, "bf_events_month", date("m", $updatestartd ) );

edit:
I’ve been comparing my code to the plugin “The Events Calendar”. This plugin inserts a date (different format to me) as part of the last component of the permalink, hyphenated with the event title: cultural-center-art-exhibition-2014-11-08 and notice that the same thing occurs there if you create a 2nd post with the same name, WP will append -2 to the title part of the permalink. However their code is better than mine in that if you change the date part of the permalink you get a 404, whereas mine will load the event so long as the title is correct, and you have anything matching /([0-9]{4})/([0-9]{2})/ in the date part of the permalink. I’ll try and figure out how they are getting that 404 and might learn more.

Solutions Collecting From Web of "Permalinks using event date (year & month) instead of publication date"

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' );