• Resolved Ryan Sechrest

    (@sovereign)


    The requirement was that parent and child pages, of a specified custom post type, would be accessible by making a request to the root domain followed by the page slug.

    In other words, instead of having URLs like this:

    example.org/services/web-development
    example.org/services/web-development/wordpress

    The goal was to have URLs like this:

    example.org/web-development
    example.org/wordpress

    This was accomplished by using the post_type_link filter and the pre_get_posts action hook.

    Here is the code for post_type_link:

    function post_type_link($permalink, $post, $leavename) {
    
    	// Post types of interest
    	$post_types = array('services');
    
    	// If not post type of interest
    	if(!in_array($post->post_type, $post_types)) {
    
    		return $permalink;
    	}
    
    	// Break URL into scheme, host, path and query
    	$url_components = parse_url($permalink);
    
    	// Save path component from URL
    	$post_path = $url_components['path'];
    
    	// Do nothing if root URL
    	if($post_path == '/') {
    
    		return $permalink;
    	}
    
    	// Strip beginning and trailing slash from path
    	$post_path = trim($post_path, '/');
    
    	// Do nothing if there is no post slug
    	if(empty($post_path)) {
    
    		return $permalink;
    	}
    
    	// Break down post slug
    	$post_slugs = explode('/', $post_path);
    
    	// Extract post name from post slugs
    	$post_name = end($post_slugs);
    
    	// Do nothing is post name is empty
    	if(empty($post_name)) {
    
    		return $permalink;
    	}
    
    	// Remove parent slugs from URL
    	$permalink = str_replace($post_path, $post_name, $permalink);
    
    	return $permalink;
    }

    Here is the code for pre_get_posts:

    function pre_get_posts($query) {
    	global $wpdb;
    
    	// If user is browsing the website
    	if(!is_admin()) {
    
    		// If query is main page query
    		if(!$query->is_main_query()) {
    			return;
    		}
    
    		// Get the post name
    		$post_name = $query->get('pagename');
    
    		// Look up the post's post type via the post name
    		$post_type = $wpdb->get_var(
    			$wpdb->prepare(
    				'SELECT post_type FROM ' . $wpdb->posts . ' WHERE post_name = %s LIMIT 1',
    				$post_name
    			)
    		);
    
    		// If look-up was successful
    		if(!empty($post_type)) {
    
    			// Determine actions based on post type
    			switch($post_type) {
    
    				case 'services':
    					$query->set('services', $post_name);
    					$query->set('post_type', $post_type);
    					break;
    
    			}
    		}
    	}
    }

    This worked successfully in WordPress 3.9.3 and below, but stopped working in WordPress 4.0 and up.

    Specifically, parent pages still load fine, but child pages now return a 404.

    If I print out $wp_query on both a parent and child page, in both the template (single-services.php) and in pre_get_posts, I see my WP_Query adjustments were applied for both parent and child page.

    I also see my changes applied in the template for the parent page. In other words, a post is, in fact, returned and this is the query it made:

    SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND (wp_posts.ID = '1234') AND wp_posts.post_type = 'services' ORDER BY wp_posts.post_date DESC

    But I don’t get that result when a child page is requested:

    SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND (wp_posts.ID = '0') AND wp_posts.post_type = 'services' ORDER BY wp_posts.post_date DESC

    What stands out is that the ID is zero.

    I looked back over query.php (in WordPress 4.1) and on line 2585 is the following conditional:

    if ( isset($this->queried_object_id) ) {
    	$reqpage = $this->queried_object_id;
    } else {
    	if ( 'page' != $q['post_type'] ) {
    		foreach ( (array)$q['post_type'] as $_post_type ) {
    			$ptype_obj = get_post_type_object($_post_type);
    			if ( !$ptype_obj || !$ptype_obj->hierarchical )
    				continue;
    
    			$reqpage = get_page_by_path($q['pagename'], OBJECT, $_post_type);
    			if ( $reqpage )
    				break;
    		}
    		unset($ptype_obj);
    	} else {
    		$reqpage = get_page_by_path($q['pagename']);
    	}
    	if ( !empty($reqpage) )
    		$reqpage = $reqpage->ID;
    	else
    		$reqpage = 0;
    }

    On the parent page, $reqpage is 1234, but on the child page, $reqpage is 0.

    I found that this is because $reqpage = get_page_by_path($q['pagename'], OBJECT, $_post_type); returns a WP_Post object for the parent page, but 0 for the child page.

    Looking at the get_page_by_path function in post.php, I noticed the following on line 4215:

    if ( $p->post_parent == 0 && $count+1 == count( $revparts ) && $p->post_name == $revparts[ $count ] ) {
    	$foundid = $page->ID;
    	if ( $page->post_type == $post_type )
    		break;
    }

    If I remove this check: $p->post_parent == 0, the child page loads fine. From what I can tell, however, this part of the code wasn’t updated since 2011, so I don’t think that’s the issue.

    All that said, that’s where I’m at with troubleshooting. I figured I’d post it here in case I missed something, someone has an idea of what might be affecting this, or suggestions on how to resolve it.

    PS: I did the following statement in the pre_get_posts docs today:

    pre_get_posts cannot be used to alter the query for Page requests (page templates) because ‘is_page’, ‘is_singular’, ‘pagename’ and other properties (depending if pretty permalinks are used) are already set by the parse_query() method.

    I don’t know if that was always there, but it sounds like this is what I’m trying to do, depending on your interpretation of “Page” and whether it’s used generally, Posts vs Pages, or anything not custom post type. My code did, nevertheless, work since the update to 4.0.

Viewing 2 replies - 1 through 2 (of 2 total)
  • Thread Starter Ryan Sechrest

    (@sovereign)

    Since my post I’ve learned about what changed and found a way to solve the issue.

    If you look on line 2375 in query.php of WordPress 3.9.3, it has the following:

    if ( ! $ptype_obj->hierarchical || strpos($q[ $ptype_obj->query_var ], '/') === false ) {
    	// Non-hierarchical post_types & parent-level-hierarchical post_types can directly use 'name'
    	$q['name'] = $q[ $ptype_obj->query_var ];
    } else {
    	// Hierarchical post_types will operate through the
    	$q['pagename'] = $q[ $ptype_obj->query_var ];
    	$q['name'] = '';
    }

    But in WordPress 4.1, on line 2566, it now has:

    if ( ! $ptype_obj->hierarchical ) {
    	// Non-hierarchical post types can directly use 'name'.
    	$q['name'] = $q[ $ptype_obj->query_var ];
    } else {
    	// Hierarchical post types will operate through 'pagename'.
    	$q['pagename'] = $q[ $ptype_obj->query_var ];
    	$q['name'] = '';
    }

    In other words, now that || strpos($q[ $ptype_obj->query_var ], '/' is gone, which used to evaluate to true, since query_var was set to services and did not contain a /, it no longer moves into the if statement, but into the else instead, which in turn means $q['name'] is now blank.

    Because $q['name'] is blank, the code execution follows a different path, specifically, instead of moving into if below, it moves into the first elseif, which is where get_page_by_path comes into play and what prevents the page from loading:

    if ( '' != $q['name'] ) {
    	$q['name'] = sanitize_title_for_query( $q['name'] );
    	$where .= " AND $wpdb->posts.post_name = '" . $q['name'] . "'";
    } elseif ( '' != $q['pagename'] ) {
    	if ( isset($this->queried_object_id) ) {
    		$reqpage = $this->queried_object_id;
    	} else {
    		if ( 'page' != $q['post_type'] ) {
    			foreach ( (array)$q['post_type'] as $_post_type ) {
    				$ptype_obj = get_post_type_object($_post_type);
    				if ( !$ptype_obj || !$ptype_obj->hierarchical )
    					continue;
    
    				$reqpage = get_page_by_path($q['pagename'], OBJECT, $_post_type);
    				if ( $reqpage )
    					break;
    			}
    			unset($ptype_obj);
    		} else {
    			$reqpage = get_page_by_path($q['pagename']);
    		}
    		if ( !empty($reqpage) )
    			$reqpage = $reqpage->ID;
    		else
    			$reqpage = 0;
    	}
    
    	$page_for_posts = get_option('page_for_posts');
    	if  ( ('page' != get_option('show_on_front') ) || empty($page_for_posts) || ( $reqpage != $page_for_posts ) ) {
    		$q['pagename'] = sanitize_title_for_query( wp_basename( $q['pagename'] ) );
    		$q['name'] = $q['pagename'];
    		$where .= " AND ($wpdb->posts.ID = '$reqpage')";
    		$reqpage_obj = get_post( $reqpage );
    		if ( is_object($reqpage_obj) && 'attachment' == $reqpage_obj->post_type ) {
    			$this->is_attachment = true;
    			$post_type = $q['post_type'] = 'attachment';
    			$this->is_page = true;
    			$q['attachment_id'] = $reqpage;
    		}
    	}
    } elseif ( '' != $q['attachment'] ) {
    	$q['attachment'] = sanitize_title_for_query( wp_basename( $q['attachment'] ) );
    	$q['name'] = $q['attachment'];
    	$where .= " AND $wpdb->posts.post_name = '" . $q['attachment'] . "'";
    }

    What ended up solving the problem is to prefix the $post_name in the pre_get_posts hook with the parent slug if it was a child post.

    I accomplished this as follows:

    $result = $wpdb->get_row(
    	$wpdb->prepare(
    		'SELECT wpp1.post_type, wpp2.post_name AS parent_post_name FROM ' . $wpdb->posts . ' as wpp1 LEFT JOIN ' . $wpdb->posts . ' as wpp2 ON wpp1.post_parent = wpp2.ID WHERE wpp1.post_name = %s LIMIT 1',
    		$post_name
    	)
    );
    
    if(!empty($result->post_type)) {
    
    	switch($result->post_type) {
    
    	    case 'services':
    	    	if ($result->parent_post_name !== '') {
    	    		$post_name = $result->parent_post_name . '/' . $post_name;
    	    	}
    	    	$query->set('expertise', $post_name);
    	    	$query->set('post_type', $result->post_type);
    	    	$query->is_single = true;
    	    	$query->is_page = false;
    	    	break;
    
        }
    }

    Dear Ryan i use your tut from here:
    https://ryansechrest.com/2013/04/remove-post-type-slug-in-custom-post-type-url-and-move-subpages-to-website-root-in-wordpress/

    I change permalink of custom post type successfuly but give 404 error. And this is my code:

    function custom_pre_get_posts($query) {
        global $wpdb;
    
        if(!$query->is_main_query()) {
          return;
        }
    
        $post_name = $query->get('name');
     	$result = $wpdb->get_row(
    	$wpdb->prepare(
    		'SELECT wpp1.post_type, wpp2.post_name AS parent_post_name FROM ' . $wpdb->posts . ' as wpp1 LEFT JOIN ' . $wpdb->posts . ' as wpp2 ON wpp1.post_parent = wpp2.ID WHERE wpp1.post_name = %s LIMIT 1',
    		$post_name
    	)
    );
    
    if(!empty($result->post_type)) {
    
    	switch($result->post_type) {
    
    	    case 'chapter':
    	    	if ($result->parent_post_name !== '') {
    	    		$post_name = $result->parent_post_name . '/' . $post_name;
    	    	}
    	    	$query->set('chapter', $post_name);
    	    	$query->set('post_type', $result->post_type);
    	    	$query->is_single = true;
    	    	$query->is_page = false;
    	    	break;
    
        }
    }
        return $query;
    }
    add_action('pre_get_posts','custom_pre_get_posts');

    Could you check and fix it?

Viewing 2 replies - 1 through 2 (of 2 total)
  • The topic ‘Using pre_get_posts to update query of a child post no longer works as expected’ is closed to new replies.