Using pre_get_posts to update query of a child post no longer works as expected
-
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/wordpressThe goal was to have URLs like this:
example.org/web-development
example.org/wordpressThis was accomplished by using the
post_type_link
filter and thepre_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 inpre_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
is1234
, but on the child page,$reqpage
is0
.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, but0
for the child page.Looking at the
get_page_by_path
function inpost.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.
- The topic ‘Using pre_get_posts to update query of a child post no longer works as expected’ is closed to new replies.