Make permalinks based on an ACF-field

URLs made from CF

Well, you can do this in many ways… One of them would be to use parse_request filter and modify it’s behavior, but it can easily mess something up.

Another way would be to use save_post action and modify the posts post_name, so WordPress will work like normal, but the post_name will be generated based on your custom field and not based on title. This is the solution I would go for. Why? Because then you can use WP functions to assure that the links are unique…

So here’s the code:

function change_post_name_on_save($post_ID, $post, $update ) {
    global $wpdb;

    $post = get_post( $post_ID );
    $cf_post_name = wp_unique_post_slug( sanitize_title( 's' . get_post_field('foo', $post_ID), $post_ID ), $post_ID, $post->post_status, $post->post_type, $post->post_parent );

    if ( ! in_array( $post->post_status, array( 'publish', 'trash' ) ) ) {
        // no changes for post that is already published or trashed
        $wpdb->update( $wpdb->posts, array( 'post_name' => $cf_post_name ), array( 'ID' => $post_ID ) );
        clean_post_cache( $post_ID );
    } elseif ( 'publish' == $post->post_status ) {
        if ( $post->ID == $post->post_name ) {
            // it was published just now
            $wpdb->update( $wpdb->posts, array( 'post_name' => $cf_post_name ), array( 'ID' => $post_ID ) );
            clean_post_cache( $post_ID );
        }
    }
}
add_action( 'save_post', 'change_post_name_on_save', 20, 3 );

It’s not the prettiest one, because ACF fields are saved after the post, so you’ll have to overwrite the post_name after the post is already saved. But it should work just fine.

CPTs in root

And there’s the second part of your question: How to make CPTs URLs without CPT slug…

WordPress uses Rewrite Rules to parse the request. It means that if the request matches one of the rule, it will be parsed using that rule.

The rule for pages is one of the last rule made to catch most of the requests that didn’t match any earlier rules. So if you add your CPT without any slug, then pages rule won’t be fired up…

One way to fix this is to register CPT with a slug and then change their links and WPs behavior. Here’s the code:

function register_mycpt() {
    $arguments = array(
        'label' => 'MyCPT',
        'public' => true,
        'hierarchical' => false,
        ...
        'has_archive' => false,
        'rewrite' => true
    );
    register_post_type('mycpt', $arguments);
}
add_action( 'init', 'register_mycpt' );

Now the URLs for these posts will look like these:

http://example.com/mycpt/{post_name}/

But we can easily change this using post_type_link filter:

function change_mycpt_post_type_link( $url, $post, $leavename ) {
    if ( 'mycpt' == $post->post_type ) {
        $url = site_url("https://wordpress.stackexchange.com/") . $post->post_name . "https://wordpress.stackexchange.com/";
    }

    return $url;
}
add_filter( 'post_type_link', 'change_mycpt_post_type_link', 10, 3 );

Now the URLs will be correct, but… They won’t work. They will be parsed using rewrite rule for pages and will cause 404 error (since there is no page with such URL). But we can fix that too:

function try_mycpt_before_page_in_parse_request( $wp ) {
    global $wpdb;

    if ( is_admin() ) return;

    if ( array_key_exists( 'name', $wp->query_vars ) ) {
        $post_name = $wp->query_vars['name'];

        $id = $wpdb->get_var( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s ",
            'mycpt', $post_name
        ) );

        if ( $id ) {
            $wp->query_vars['mycpt'] = $post_name;
            $wp->query_vars['post_type'] = 'mycpt';
        }

    }
}
add_action( 'parse_request', 'try_mycpt_before_page_in_parse_request' );

Leave a Comment