Different rewritten dynamic slugs seen as duplicate

Viewed 338

I'm sorry if the post title is not too understandable...

I have a custom post type called houses and I rewrite dynamically the slug depending on the taxonomy term. The taxonomy is called sizes and the terms are big and small.

When I create 2 posts with same title, the output of the slugs is like this even though the rewrite function makes it to be different slugs:

  • houses/big/red-house
  • houses/small/red-house-2

I even attempted to change manually the slug on the edit option but it does not let me, it still leaves the -2 at the end of the slug. The same goes for the database, it redirects to the first post....

I don't understand why wordpress sees them as same slug... is not identifying the rewrite?

Here how I created the custom post type:

$labels = array(
        'name'              => 'Houses',
        'singular_name'     => 'House',
        'menu_name'         => 'Houses',
        'search_items'      => 'Houses'
    );
    $args = array(
        'labels'                => $labels,
        'public'                => true,        
        'hierarchical'          => false,
        'exclude_from_search'   => false,
        'publicly_queryable'    => true,
        'menu_icon'             => 'dashicons-admin-home',
        'supports'              => array('title', 'editor'),
        'taxonomies'            => array('sizes', 'post_tag'), // Term & Tag
        'has_archive'           => false,
        'rewrite'               => array( 'slug' => 'houses/%sizes%'),
        'query_var'             => true
    );
    register_post_type('houses', $args);

Here is my taxonomy code:

$labels = array(
        'name'              => 'Sizes',
        'singular_name'     => 'Size',
        'menu_name'         => 'Sizes',
    );
    $args = array(
        'labels'            => $labels, // display name
        'hierarchical'      => true,
        'public'            => true,
        'show_ui'           => true,
        'show_in_rest'      => true,
        'show_admin_column' => true,
        'query_var'         => true,
        'with_front'        => true
    );
    register_taxonomy( 'sizes', 'houses', $args); // Slug, CPT, array

And this is the code I'm using to dynamically change the slugs:

function houses_link( $post_link, $id = 0 )
{
    $post = get_post($id);

    if ( is_object( $post ) )
    {
        $terms = wp_get_object_terms( $post->ID, 'houses' );

        if( $terms )
        {
            return str_replace( '%sizes%' , $terms[0]->slug , $post_link );
        }
    }
    return $post_link;  
}
add_filter( 'post_type_link', 'houses_link', 1, 3 );

I hope that my explanation was clear :') thanks for the help!

1 Answers

First we need to explain a little bit about how rewrite_rules and page url generation in Wordpress works

Here is the simplest url

https://example.com/?p=1

When you go to it, Wordpress searches for a post with ID = 1 and outputs a page. And if it doesn't find it, it returns a 404.

Now a bit more complicated url

https://example.com/?name=hello-world

Instead of ID we have a slug in the url, so Wordpress works like with the first one, it looks for this slug in the database and gives us the page.

Now let's look at the url of this page with permalinks enabled

https://example.com/hello-world/

What happens when Wordpress sees such a url? It goes to its rewrite_rules list to see if there is a regex for that url.

With regex we extract key data from the url and send a request to the standard url

https://example.com/?name=hello-world

Now let's have a look at your URLs

https://example.com/houses/big/red-house

When Wordpress sees this url, it finds the appropriate regex for it in the list of rewrite_rules, extracts the 'red-house' slug, and then sends a request to

https://example.com/?houses=red-house

And that's why when creating a post in Wordpress there are checks to make sure the slug is unique to the post_type Because with these links

https://example.com/houses/small/red-house

https://example.com/houses/medium/red-house

https://example.com/houses/giga/red-house

Wordpress will do the same thing as the first link, it will send the request to

https://example.com/?houses=red-house.

Wordpress has no requests with slug+term for single pages, only slug.

What should we do to solve this problem?

It doesn't have to be a one-piece slug, we can assemble it from different parts of the url

What we need to do:

  1. Add custom_field 'house_base_slug', because after post_type_link the slug will no longer be editable.
  2. Make a function that will create a single unique 'TermSlug-and-house_base_slug' format slug from our custom_field and term slug
add_action( 'save_post_houses', 'house_save_new_slug' );
function house_save_new_slug($post_id) {
    
        //disable
        remove_action( 'save_post_houses', 'house_save_new_slug' );

        $house_base_slug = get_post_meta($post_id, "house_base_slug", true);
        if($house_base_slug =='') {
        $house_base_slug = $post_id;
        }
        $terms = wp_get_object_terms( $post_id, 'sizes' );
        
        $term_slug = $terms[0]->slug;
        if($term_slug == "") {
        $term_slug = 'default';    
        }
        $new_house_slug = $term_slug . '-and-' .  $house_base_slug;
        // update the post. At this time the save_post hook is triggered
        wp_update_post( array( 'ID' => $post_id, 'post_name' => $new_house_slug ) );
        clean_post_cache( $post_id );
        // Enable
        add_action( 'save_post_houses', 'house_save_new_slug' );
}

There is 1 important point here, we update post_name(slug) and this triggers the save_post hook To avoid a loop, we disable our function before the update

remove_action('save_post_houses', 'house_save_new_slug' );

And we turn it back on after

add_action('save_post_houses', 'house_save_new_slug' );

Now when you create or update a house, a slug of this format 'TermSlug-and-house_base_slug' will be automatically created. If our custom_field 'house_base_slug' is empty - post_id will be taken. If there is no term - slug 'default' will be used

  1. Ok, now we have a 'small-and-red-house' slug. We have to associate it with our url
add_action( 'init', 'rewrite_rule_my_house' );
function rewrite_rule_my_house(){
  add_rewrite_rule( '^houses/([^/]*)/([^/]*)/?', 'index.php?houses=$matches[1]-and-$matches[2]', 'top' );
}

Don't forget to disable/enable permalinks after this step, we need to reset the cache.

  1. Updating permalinks
function custom_post_permalink( $permalink, $post, $leavename, $sample ) 
{
    if ( $post->post_type == 'houses'  ) {

      $post_name = $post->post_name;
      $post_name_exploded = explode('-and-', $post_name);
      $current_slug_part1 = $post_name_exploded[0];
      $current_slug_part2= $post_name_exploded[1];
      $post_cur_id = $post->ID;
      

      
      $slug = '/houses/' . $current_slug_part1 .'/' . $current_slug_part2 . '/';

      
      $permalink = home_url($slug, 'https');
    }
    return $permalink;  
}
add_filter( 'post_type_link', 'custom_post_permalink', 10, 4 );
Related