Dynamic CPT permalink structure based on ACF field value

First step is to register the post type. The slug doesn’t matter here, we won’t be using any of the rules generated when the post type is registered.

Within this same init function, we add a rewrite tag and permastruct with our permalink pattern. This will make it easy to construct the permalink in the filter we’ll add later.

The last bit is the rewrite rule that maps incoming requests to the right query vars. To query a custom post type by post ID, you need to set post_type and p query vars.

function wpd_test_cpt() {

    $args = array(
        'label' => 'My CPT',
        'public' => true,
        'rewrite' => [
            'slug' => 'my_cpt',
            'with_front' => false,
        ],
    );
    register_post_type( 'my_cpt', $args );

    add_rewrite_tag( '%foo_or_bar%', '([^/]+)' );
    add_permastruct( 'my_cpt', 'fixed-word/%foo_or_bar%/%post_id%/' );
    add_rewrite_rule(
        'fixed-word/([^/]+)/([0-9]+)/?$',
        'index.php?post_type=my_cpt&foo_or_bar=$matches[1]&p=$matches[2]',
        'top'
    );

}
add_action( 'init', 'wpd_test_cpt' );

The next step is the post_type_link filter where we swap in values for the placeholders:

function wpd_test_post_type_link( $permalink, $post ) {
    if ( 'my_cpt' === $post->post_type ) {
        if( $choice = get_field( 'foo_or_bar', $post->ID ) ) {
            $permalink = str_replace( ['%foo_or_bar%', '%post_id%'], [$choice, $post->ID], $permalink );
        }
    }
    return $permalink;
}
add_filter( 'post_type_link', 'wpd_test_post_type_link', 10, 2 );

Now this should all work as-is, but you might notice one little quirk- you can change foo or bar to whatever you want and the query still succeeds. You can’t refine a singular view by meta data, WordPress sees a post as existing or not solely by ID or slug.

To change this, we can add a bit of code to check if foo_or_bar is set, and make sure it matches the requested post ID. If it doesn’t match, we redirect to the correct URL:

function wpd_test_pre_get( $query ) {
    if ( isset( $query->query['foo_or_bar'] ) && isset( $query->query['p'] ) ) {
        if( $choice = get_field( 'foo_or_bar', $query->query['p'] ) ){
            if( $choice != $query->query['foo_or_bar'] ){
                wp_redirect( get_permalink( $query->query['p'] ) );
            }
        }
    }
}
add_action( 'pre_get_posts', 'wpd_test_pre_get' );