Removing base slug from hierarchical custom post type


Two things before I begin:

  1. You really shouldn’t do this. Having static anchors to rewrite rules is there for efficiency. Your best bet is to find a slug that you’re happy with — maybe instead of “state” use “activities” (or whatever your sub-pages are).
  2. Most likely, your sub-pages are not actually states (since you wouldn’t have /idaho/vermont/), so this data structure doesn’t seem to make much sense. You might consider having “state” be one data structure and “sub-page” be another. If state is a classification, you could make it a taxonomy. If state is more than that, it could be a post type. Here are instructions for setting up rewrites for the former from a WordCamp talk that I gave in Portland, and here are instructions for the latter from another question on this site.

The Code

If you decide to move forward with this structure, here is a singleton class that should work out of the box for you if your post type has ‘hierarchical’ set to true and ‘rewrite’ set to false. Be sure to flush your rewrites after adding this to your theme’s functions.php file or your plugin (go to Settings → Permalinks and click “Save Changes”).

 * Strip the slug out of a hierarchical custom post type

if ( !class_exists( 'State_Rewrites' ) ) :

class State_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 State_Rewrites;
        return self::$instance;

    public function setup() {
        add_action( 'init',                array( $this, 'add_rewrites' ),            20 );
        add_filter( 'request',             array( $this, 'check_rewrite_conflicts' )     );
        add_filter( 'state_rewrite_rules', array( $this, 'strip_state_rules' )           );
        add_filter( 'rewrite_rules_array', array( $this, 'inject_state_rules' )          );

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

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

    public function strip_state_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_state_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 );



I thought this topic was interesting and I wrote up a deep explanation of this on my blog. There, I included some slightly more complex code that wasn’t as relevant to your specific question.

Leave a Comment