WordPress 3.3 custom post type with /%postname%/ permastruct?

There is earlier post with similar title, but it does not look in to the WordPress 3.3, and that is important as 3.3 advertises interestingly: “Use the postname permalink structure without a performance penalty”

Problem with WordPress 3.2 and earlier was that first it looked page names, and then 404. It didn’t check the arbitrary post types first. 3.3 on the other hand must look post types, then pages, and finally 404 (as it advertises this feature). This implies that custom post types without slug should be simple, if they didn’t hard code post_type=post somewhere.

Read More

I can’t find a 3.3 specific solution yet though.

Question: How can I define permalink struct “/%postname%/” for any given custom post type “xyz”?

Thanks.

Related posts

Leave a Reply

6 comments

  1. This isn’t easily done in WP 3.3 unless you trick the rewrite rules to be in the correct spot and make wp_rewrite think verbose rules are being used in the front end. The class below works.

    class Test_Post_Type {
        const POST_TYPE = 'test';
    
        public static function init() {
            global $wp_rewrite;
    
            $post_type_obj = register_post_type( self::POST_TYPE, array(
                'labels' => array(
                    'name' => __( 'Tests' ),
                    'singular_name' => __( 'Test' ),
                    'add_new' => __( 'Add New' ),
                    'add_new_item' => __( 'Add New Test' ),
                    'edit_item' => __( 'Edit Test' ),
                    'new_item' => __( 'New Test' ),
                    'all_items' => __( 'All Tests' ),
                    'view_item' => __( 'View Test' ),
                    'search_items' => __( 'Search Tests' ),
                    'not_found' => __( 'No Tests found' ),
                    'not_found_in_trash' => __( 'No Tests found in Trash' ),
                    'menu_name' => __( 'Tests' )
                ),
                'publicly_queryable' => true,
                'exclude_from_search' => true,
                'hierarchical' => false,
                'public' => true,
                'rewrite' => false,
                'has_archive' => true,
                'supports' => array( 'title', 'editor', 'thumbnail', 'test_source' ),
                'taxonomies' => array( 'category', 'post_tag' ),
            ) );
    
            $post_type_obj = get_post_type_object(self::POST_TYPE);
    
            //register the rewrite tag for permalink building
            $wp_rewrite->add_rewrite_tag( '%' . $post_type_obj->query_var . '%', '([^/]+)', $post_type_obj->query_var . '=' );
    
            //we have to add the permastruct here in order to build the permalink, otherwise we'll need to filter the post_type link
            add_permastruct(self::POST_TYPE, '%' . $post_type_obj->query_var . '%/', false );
    
            //add a filter to remove the permastructs generated above
            add_filter(self::POST_TYPE . '_rewrite_rules', array(__CLASS__, '_remove_default_rules')); 
    
            //now we add a filter to put the generated rewrite rules in the correct spot
            add_action('generate_rewrite_rules', array(__CLASS__, '_filter_rewrite_rules'));
    
            if(!is_admin()) {
                //we need verbose_page_rules to be on on the front end in order for pages to be process properly
                $wp_rewrite->use_verbose_page_rules = true;
            }
        }
    
        /**
         * Filter to remove the rules for this post type when they're automatically generated due to the permastruct registration
         * @param type $rules
         * @return type 
         */
        public static function _remove_default_rules($rules) {
            return array();
        }
    
        /**
         * Filters the rules at the end to add back the ones for this post type at the bottom
         * @param WP_Rewrite $wp_rewrite 
         */
        public static function _filter_rewrite_rules($wp_rewrite) {
            $post_type_obj = get_post_type_object(self::POST_TYPE);
            $my_rules = $wp_rewrite->generate_rewrite_rules('%' . $post_type_obj->query_var . '%', EP_NONE);
            $wp_rewrite->rules += $my_rules;
        }
    
    }
    
    add_action( 'init', array( 'Test_Post_Type', 'init' ) );
    
  2. Holy car keys!

    I think this works. It almost works, it’s super simple, only one line:

    global $wp_rewrite;
    $args = array(
        'public' => true,
        'publicly_queryable' => true,
        'show_ui' => true,
        'show_in_menu' => true,
        'query_var' => true,
        'rewrite' => array('slug' => 'anything'),
        'capability_type' => 'post',
        'has_archive' => true,
        'hierarchical' => false,
        'menu_position' => null,
        'supports' => array('title','editor','thumbnail')
    );
    register_post_type('my_custom_post_type', $args);
    
    $wp_rewrite->add_permastruct('my_custom_post_type', "%my_custom_post_type%");
    

    P.S. If you try this at home, after adding this one line Go to “Settings” -> “Permalinks” and Save Changes, it refreshes the permalinks.

    I was reading the WP register_post_type() source code and found a line:

    $wp_rewrite->add_permastruct($post_type, "{$args->rewrite['slug']}/%$post_type%", $args->rewrite['with_front'], $args->permalink_epmask);
    

    Needless to say but without slug I concluded it should work, and it did. Even the permalink editing underneath the title in editor works correctly!

    Update: This breaks page permalinks, back to drawing board…

  3. prettyboymp answer is almost the same I got yesterday, but I’m not happy with it. prettyboymp’s answer has a one flaw, it does not work when /%postname%/ is being used simultaenously on multiple post types.

    Here is my answer, which also looks in to current structure, and creates array of post types to fallback on. There is one flaw in this too though, if two post types have same slug and both are /%postname%/ then it shows both.

    class MyCustomPostType {
        /**
         * Register post type
         **/
        public static function register_post_type() {
            global $wp_rewrite;
    
            $args = array(
                'public' => true,
                'publicly_queryable' => true,
                'show_ui' => true,
                'show_in_menu' => true,
                'query_var' => true,
                'rewrite' => false,
                'capability_type' => 'post',
                'has_archive' => true,
                'hierarchical' => false,
                'menu_position' => null,
                'supports' => array('title','editor','thumbnail')
            );
    
            register_post_type('my_custom_post_type', $args);
    
            // Enables the pages to work simultaneously
            $wp_rewrite->use_verbose_page_rules = true;
            add_filter("rewrite_rules_array", array(__CLASS__, 'rewrite_rules_array'));
            add_action("parse_query", array(__CLASS__, 'parse_query'));
            add_filter("post_type_link", array(__CLASS__, 'post_type_link'), 1, 4);
        }
    
        public static function post_type_link($link, $post, $leavename=false, $sample=false) {
            if ($sample && ($begin = strpos($link, "?my_custom_post_type=")) !== false) {
                return substr($link, 0, $begin-1) . "/%my_custom_post_type%/";
            }
            return str_replace("?my_custom_post_type=", "", $link) . "/";
        }
    
        public static function parse_query($query) {
            global $wp, $wp_rewrite;
    
            // Is this query for /%post_name%/? Is it main request query?
            if (isset($query->query['name'])
                && substr($wp->matched_rule, 0, 7) == "([^/]+)"
                && isset($query->query)
                && isset($wp->query_vars)
                && $query->query == $wp->query_vars)
            {
                //echo '<p><h1>hit!</h1></p>';
                if (!($post_types = get_query_var("post_type"))) {
                    if ($wp_rewrite->permalink_structure == "/%postname%/")
                        $post_types = array("post");
                    else
                        $post_types = array();
                }
    
                if (is_array($post_types))
                    $post_types[] = "my_custom_post_type";
    
                set_query_var("post_type", $post_types);
                //set_query_var("posts_per_page", 1);
            }
        }
    
        public static function rewrite_rules_array($array) {
            global $wp_rewrite;
            // Same rules as in /%post_name%/
            return array_merge($array, $wp_rewrite->generate_rewrite_rules("/%postname%/", EP_PERMALINK));
        }
    }
    
    
    add_action('init', array("MyCustomPostType", "register_post_type"));
    
  4. I created a solution and i couldnt find a problem with it. Please try and tell me if you find a problem

    add_action('init', 'firmasite_resimlitarif_cpt', 0);
    function firmasite_resimlitarif_cpt() 
    {
    
    // Yemek Tarifi
    
      $args = array(
        'public' => true,
        'show_in_menu' => true, 
        'permalink_epmask' => EP_NONE,
        'rewrite' => array('slug'=>'/','with_front'=>false),
        'has_archive' => false,
        'supports' => array('title','editor','thumbnail')
      ); 
      register_post_type('yemek',$args);
    
    }
    
    
    // http://wordpress.stackexchange.com/questions/37650/wordpress-3-3-custom-post-type-with-postname-permastruct
    add_action("parse_query", 'firmasite_resimlitarif_parse_query');
    function firmasite_resimlitarif_parse_query($query) {
        global $wp, $wp_rewrite;
    
    
        // Is this query for /%post_name%/? Is it main request query?
        if (isset($query->query['name'])
            && substr($wp->matched_rule, 0, 7) == "([^/]+)"
            && isset($query->query)
            && isset($wp->query_vars)
            && $query->query == $wp->query_vars)
        {
            if (!($post_types = get_query_var("post_type"))) {
                if ($wp_rewrite->permalink_structure == "/%postname%/")
                    $post_types = array("post");
                else
                    $post_types = array();
            }
    
            if (is_array($post_types)){ 
                $post_types[] = 'yemek';
                $post_types[] = 'page';
            }
    
    
            set_query_var("post_type", $post_types);
        } 
    }
    

    Change ‘yemek’ with your post type name.

  5. The cleanest answer I could come up with for this (I’m building out a plugin that really needs a custom post type without any leading slug) is to use a custom page template instead of using a custom post type.

    By doing this, your “custom post type” can have urls such as /whatever without having to worry about stepping on page or post permalinks.

    To do this, I ended up doing the following:

    • Adding a custom page template inside of my plugin
    • Setting up the page template so that it could be selected in the page editor
    • Creating custom meta boxes that only show up for my page template

    This allowed me to:

    The down sides

    Of course, while this does not stomp on page or post links, it does have a couple of obvious downsides.

    No archive
    You won’t have an archive (if you want such), though that can be solved by creating another page template to draw an archive of all pages using your custom template.

    Managed under Pages
    You don’t get the nice left hand navigation in the admin that groups all of the post type together.

    This could be partially solved by adding a filter to the page list (to let you filter by the page template being used), showing any page template used in a new column, etc.


    That being said, I wanted something that would not cause users to wonder why they created a new custom page and found that they could either no longer reach normal pages or the new custom page caused an existing page on their site to disappear.

    I know it isn’t a real solution, but it is an alternative that worked great for my needs.