2

I have a hierarchical Custom Post Type called project, registered as follows:

register_post_type( 'project',
    array(
        'public'            => true,
        'hierarchical'      => true,
        'rewrite' => array(
            'with_front' => false
        )
    )
);

The URLs currently look like this:

I would like the URLs to look like this:

How can I most efficiently accomplish this such that other URLs are not interfered with (e.g. pages, blog posts, other custom post types)?

All the solutions that are currently out there either cause 404s on other pages on the site or do not work correctly with nested URLs.

2 Answers 2

3

This will allow you to use the post name without the slug. Essentially anytime the link is requested it can be altered to exclude the base post type. And any time a query runs with just a name, the available post types used in the search are altered to include your post type.

function wpse_remove_cpt_slug( $post_link, $post, $leavename ) {

    // leave these CPT alone
    $whitelist = array ('project');

    if ( ! in_array( $post->post_type, $whitelist ) || 'publish' != $post->post_status )
        return $post_link;

    if( isset($GLOBALS['wp_post_types'][$post->post_type],
             $GLOBALS['wp_post_types'][$post->post_type]->rewrite['slug'])){
        $slug = $GLOBALS['wp_post_types'][$post->post_type]->rewrite['slug'];
    } else {
        $slug = $post->post_type;
    }

    // remove post slug from url
    $post_link = str_replace( '/' . $slug  . '/', '/', $post_link );

    return $post_link;
}
add_filter( 'post_type_link', 'wpse_remove_cpt_slug', 10, 3 );
add_filter( 'post_link', 'wpse_remove_cpt_slug', 10, 3 );

function wpse_parse_request( $query ) {

    // Only noop the main query
    if ( ! $query->is_main_query() )
        return;

    // Only noop our very specific rewrite rule match
    if ( 2 != count( $query->query )
         || ! isset( $query->query['page'] ) )
        return;

    // 'name' will be set if post permalinks are just post_name, otherwise the page rule will match
    if ( ! empty( $query->query['name'] ) )
        $query->set( 'post_type', array( 'post', 'project', 'page' ) );
}
add_action( 'pre_get_posts', 'wpse_parse_request' );

Reference

post_type_link is a filter applied to the permalink URL for a post or custom post type prior to printing by the function get_post_permalink.

post_link is a filter applied to the permalink URL for a post prior to returning the processed url by the function get_permalink.

This hook is called after the query variable object is created, but before the actual query is run. The pre_get_posts action gives developers access to the $query object by reference (any changes you make to $query are made directly to the original object - no return value is necessary).

4
  • Could you add further explanation please? Just to clarify what the functions are doing and why.
    – Howdy_McGee
    Commented Nov 4, 2016 at 20:16
  • I've seen this solution posted before in other places. This only works to change the permalink when it is displayed (e.g. via get_the_permalink()), but the actual URLs do not change. The post is still only accessible by its original URL, /project/hello/world.
    – darrylyeo
    Commented Nov 4, 2016 at 20:23
  • 1
    pre_get_posts allows the name to be queried by including the post type in the main query so it works both ways. @Howdy_McGee - sure but not soon... It's Friday! I won't say I think this is 100% but it's the best answer so far.
    – jgraup
    Commented Nov 4, 2016 at 21:11
  • I take back what I said above; I forgot that I need to set 'rewrite' => array( 'slug' => '/' ) for this solution to work (as countless other articles and websites have said to do). With this in place, the URLs for my custom post type work as I want, but all the pages and blog posts on the site now return 404 errors as I anticipated. I need something much more robust than this.
    – darrylyeo
    Commented Nov 4, 2016 at 23:55
0

Try this. It will replace the slug project without 404 error for both parent and child.

/**
 * Strip the slug out of a hierarchical custom post type
 */

if ( !class_exists( 'project_Rewrites' ) ) :

class project_Rewrites {

    private static $instance;

    public $rules;

    private function __construct() {
        /* Don't do anything, needs to be initialized via instance() method */
    }

    public static function instance() {
        if ( ! isset( self::$instance ) ) {
            self::$instance = new project_Rewrites;
            self::$instance->setup();
        }
        return self::$instance;
    }

    public function setup() {
        add_action( 'init',                array( $this, 'add_rewrites' ),            20 );
        add_filter( 'request',             array( $this, 'check_rewrite_conflicts' )     );
        add_filter( 'project_rewrite_rules', array( $this, 'strip_project_rules' )           );
        add_filter( 'rewrite_rules_array', array( $this, 'inject_project_rules' )          );
    }

    public function add_rewrites() {
        add_rewrite_tag( "%project%", '(.+?)', "project=" );
        add_permastruct( 'project', "%project%", array(
            'ep_mask' => EP_PERMALINK
        ) );
    }

    public function check_rewrite_conflicts( $qv ) {
        if ( isset( $qv['project'] ) ) {
            if ( get_page_by_path( $qv['project'] ) ) {
                $qv = array( 'pagename' => $qv['project'] );
            }
        }
        return $qv;
    }

    public function strip_project_rules( $rules ) {
        $this->rules = $rules;
        # We no longer need the attachment rules, so strip them out
        foreach ( $this->rules as $regex => $value ) {
            if ( strpos( $value, 'attachment' ) )
                unset( $this->rules[ $regex ] );
        }
        return array();
    }

    public function inject_project_rules( $rules ) {
        # This is the first 'page' rule
        $offset = array_search( '(.?.+?)/trackback/?$', array_keys( $rules ) );
        $page_rules = array_slice( $rules, $offset, null, true );
        $other_rules = array_slice( $rules, 0, $offset, true );
        return array_merge( $other_rules, $this->rules, $page_rules );
    }
}

project_Rewrites::instance();

endif;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.