How to replace custom post type with custom taxonmy in permalinks

WordPress is a query based system, you need to first understand that if using permalinks and when a link http://domain.com/hospital/hospitalname is submitted, WP analyses it internally as

  • post type = hospital
  • name = hospitalname

And then create a query to look up. It is likewise for other links such as http://domain.com/cityname/hospitalname

Assumptions

Before a custom rewrite is going to work. It is assumed that you know and understand post type and taxonomy setup. Because by the time of a post type and taxonomy setup. The arguments determines how rewrite path and query_var is going to be setup.

If, in case, the post type and taxonomy is built by plugins, by someone else instead by yourself. It is necessary to find out, it could not be guessed because when it is setup, custom arguments is probably changing the default name. It is true in your case.
It will waste time if you are trying to test by guessing and assuming the name is same as what it seems to be.

The following example is based on asker’s information in the discussion. It could benefit anyone struggling in understanding rewrite.

find out what taxonomy exists

add_action( 'init', 'ws361150_debug_test' );
function ws361150_debug_test() {
    // please comment/uncomment the debug output needed

    // get an array list of available taxonomies name
    // var_dump(get_taxonomies());

    // get an array list of available taxonomies name => object pair
    // var_dump(get_taxonomies( [], 'objects'));

    // get a specific object (include assoicated post type name)
    // var_dump(get_taxonomy( 'towns' ));
}

// ... reference result
// ["countries"]=> string(9) "countries" 
// ["cities"]=> string(6) "cities" 
// ...

find out what post type the taxonomy(default or custom) is associated to

  • what is the rewrite tag
  • what is the query_var
// taxonomy example
// object(WP_Taxonomy)[1329]
//   public 'name' => string 'towns' (length=5) // taxonomy slug
//   ...
//   public 'public' => boolean true
//   public 'publicly_queryable' => boolean true // http://example/?xxxxx= is allowed

//   // post type associated with this taxonomy
//   public 'object_type' => 
//     array (size=1)
//       0 => string 'post' (length=4)
//   public 'rewrite' => 
//     array (size=4)
//    ...
//       'slug' => string 'towns' (length=5) // rewrite tag is %towns%
//   public 'query_var' => string 'towns' (length=5) // http://example/?towns=

// some taxonomy's arguments, it is better view it in formatted form
// the taxonomy slug is missed in the discussion, so not sure which slug it means, but probably countries because the query_var is countries
// ["object_type"]=> array(1) { 
//  [0]=> string(13) "sp_categories" // linked to post type sp_categories
// } 
// ["rewrite"]=> array(4) { 
//  ["slug"]=> string(7) "country" // rewrite is /%country%/
// } 
// ["query_var"]=> string(9) "countries" // means http://example.com/?countries= is allowed

// taxonomy 'cities' arguments
// ["cities"]=> object(WP_Taxonomy)#2622 (26) { 
//  ["name"]=> string(6) "cities" // slug is cities
// ... missed in the discussion

find out what is the settings of a custom post type

  • what is the rewrite tag
  • what is the query_var
add_action( 'init', 'ws361150_debug_test_pt' );
function ws361150_debug_test_pt() {
    // please comment/uncomment the debug output needed
    // export an array list of all post types list
    var_dump( get_post_types() );

    // debug view one post type object by the following method
    $post_types = get_post_types( [], 'objects' );
    var_dump($post_types['post']); // put in the post type slug
}

// object(WP_Post_Type)[963]
//   public 'name' => string 'post' (length=4) // post type slug
//   ...
//   public 'publicly_queryable' => boolean true
//   public 'query_var' => boolean false
//   public 'rewrite' => boolean false // for post it is off, the settings is similar to taxonomy, if slug is set the rewrite is /%specified_slug%/, if true, it is by default /%post_slug%/, so that by default 'post' is http://example.com/post-name/ instead of http://example.com/post_slug/post-name/

There are many methods to accomplish, here shows 2 methods, one is rewrite, and the other one is manually modifying the query.
I have tested both methods with sample post types and worked perfectly by putting them into theme’s functions.php
Please don’t put method1 and method2 at the same time for only one method is recommended for the purpose of a successful test.
In method 1, add_permastruct and add_rewrite_rule are used.

  • add_permastruct – add an extra permalink structure in addition to the settings > Permalinks in WordPress, if the rewrite tag is already registered such as taxonomy and post type, it could be used to create custom URL structure
  • add_rewrite_rule – add custom rewrite regular expression to match query URL, in some cases, it is required to do custom rules to do the task.

Method 1: Rewrite

// objective: http://domain.com/cityname/hospitalname
// post type: 'hospital', assumed the rewrite tag is '%hospital%'
// taxonomy: 'cities', assumed the rewrite tag is '%cities%' and it is associated with hospital, otherwise, it will not work
add_action( 'init', 'ws365575_taxonomy_rewrite1' );
function ws365575_taxonomy_rewrite1() {
    // optional settings as the 3rd argument for add_permastruct
    // default
    $args = array(
        // 'with_front'  => true,
        // 'ep_mask'     => EP_NONE,
        // 'paged'       => true,
        // 'feed'        => true,
        // 'forcomments' => false,
        // 'walk_dirs'   => true,
        // 'endpoints'   => true,
    );

    // this will need to flush the permalink cache to be effective
    // add to $wp_rewrite->extra_permastructs
    add_permastruct( 'cities_with_hospitals', "%cities%/%hospital%", $args );
}
// objective: http://domain.com/hospitalname
// post_type: 'hospital', assumed the query vars is '%hospital%'
// the top option tells WP to match it in higher priority before it is being regarded as not found
add_action( 'init', 'ws365575_taxonomy_rewrite2' );
function ws365575_taxonomy_rewrite2() {
    // the rule is copied from originally registered post types 'hopsital' part and modify by following page post type's rewrite

    add_rewrite_rule('(.?.+?)/page/?([0-9]{1,})/?$', 'index.php?hospital=$matches[1]&paged=$matches[2]', 'top');
    add_rewrite_rule('(.?.+?)?(:/([0-9]+))?/?$', 'index.php?hospital=$matches[1]&page=$matches[2]', 'top');

    // because the above will override the priority, to avoid page cannot be loaded and become 404, manually add once more
    add_rewrite_rule('(.?.+?)/page/?([0-9]{1,})/?$', 'index.php?pagename=$matches[1]&paged=$matches[2]', 'top');
    add_rewrite_rule('(.?.+?)?(:/([0-9]+))?/?$', 'index.php?pagename=$matches[1]&page=$matches[2]', 'top');
}

**After setup the above code, the permalink cache is needed to flush. Please refer to the following notes. **

To confirm the registered tag is right or not, may var_dump the $wp_rewrite global variables.

// the following is for optional for debug
add_action( 'init', 'ws365575_confirm_wp_rewrite' );
function ws365575_confirm_wp_rewrite() {
    global $wp_rewrite;

    // if xdebug is not installed, it is recommended to view in source
    var_dump($wp_rewrite);

    exit(); // break the code to read
}

Method 2: Manually Modify Query (Advanced)

Now, if visiting the link http://domain.com/hospitalname and make it equivalent to http://domain.com/hospital/hospitalname with missing post type information, it needs some logic in order to analyse and work.

The filter request provides early control of any query and is suitable to do the job.
While it works, please note that it depends on how well educate WP to analyse. So, a lot of test and thorough thinking must be done to different situations such as similar name, guess wrong, exception handling.

*** It won’t blow the WP, at most, it returns 404 or wrong page.
The following is tested on a custom post type by placing in theme’s function.php

add_filter( 'request', 'ws361150_custom_request_query' );
function ws361150_custom_request_query( $query ) {
    if ( isset( $_SERVER['HTTP_HOST'] ) ) {
        // build the URL in the address bar
        $requested_url  = is_ssl() ? 'https://' : 'http://';
        $requested_url .= $_SERVER['HTTP_HOST'];
        $requested_url .= $_SERVER['REQUEST_URI'];
    }


    if( url_to_postid( $_SERVER['REQUEST_URI']) > 0 ) {
        // if something is found priorly, nothing to do, suppose something exists

        return $query;
    } else {
        // must think of a logic and test thoroughly before production

        // manual setup the post query by guess            
        // check if rewrite slugs match original settings (spelling...etc)
        if( $search_ID = url_to_postid( 'hospital' . $_SERVER['REQUEST_URI']) > 0 ) {
            // setup query based on source code, if match by test

            // check if post type name match settings
            $query['post_type'] = 'hospital';
            $query['hospital'] = $query['name'];

            // var_dump($query);
            // exit();
            return $query;
        }
    }

    return $query; // untouched
}

It looks like a simple code and less overhead, it depends heavily on the testing logic supplied.

Reset permalink (flush the cache)

Reminder, whenever changing a rewrite slug or rule, need to flush the cache. The simplest way is going to settings > permalinks and click save. If not updating the cache, it will probably stick to old settings and waste time to test.

Additional work required after setup the URL

And then it is necessary to use the post_link and post_type_link hook to change the links for get_permalinks() or get_post_permalink() to get links. Then the links will be reflected on the frontend or post edit page because WP does not do it automatically.