My situation is somewhat complex, I’ll try to explain it as succinctly as possible.
I’m currently using query_posts
to modify the main query on custom pages on my site, which as far as I can tell works quite well, though I’ve read that using query_posts is bad practice for a number of different reasons.
So, why am I using query_posts
and not creating a WP_Query
object you may ask?
It’s because I’m using the infinite-scroll plugin, infinite-scroll doesn’t play nice with WP_query, but it works absolutely fine when you simply modify the main query with query_posts. For example, pagination doesn’t work using infinite scroll + WP_query (main concern).
On one page, I’m modifying the query to get most viewed posts.
<?php $paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1; ?>
<?php query_posts( array( 'meta_key' => 'wpb_post_views_count', 'orderby' => 'meta_value_num', 'order' => 'DESC' , 'paged' => $paged, ) ); ?>
<?php if (have_posts()) : ?>
<?php while ( have_posts() ) : the_post() ?>
<?php if ( has_post_format( 'video' )) {
get_template_part( 'video-post' );
}elseif ( has_post_format( 'image' )) {
get_template_part( 'image-post' );
} else {
get_template_part( 'standard-post' );
}
?>
<?php endwhile;?>
<?php endif; ?>
So after a lot of reading I gather that my other option to modify the main query is using pre_get_posts
, though I’m somewhat unsure as to how to go about this.
Take this for example:-
function textdomain_exclude_category( $query ) {
if ( $query->is_home() && $query->is_main_query() ) {
$query->set( 'cat', '-1,-2' );
}
}
add_action( 'pre_get_posts', 'textdomain_exclude_category' );
Alright, so simple enough – if it’s the home page, modify the main query and exclude two categories.
What I’m confused about and can’t figure out is:-
-
the use case scenario for custom page templates. With my
query_posts
modification I can just drop in the array beforeif (have_posts())
, select my page template, publish it and away I go.
Withpre_get_posts
I can’t figure out how to say for example$query->most-viewed
etc -
array( 'meta_key' => 'wpb_post_views_count', 'orderby' => 'meta_value_num', 'order' => 'DESC' , 'paged' => $paged, ) );
How the heck do I do that with pre_get_posts
and make sure it’s paginated, ie. works with infinite scroll? In all the examples I’ve seen with pre_get_posts
there’s no arrays.
How to use the
pre_get_posts
hook to display list of posts on a page, through a custom page template?I’ve been playing with the
pre_get_posts
hook and here’s one ideaStep #1:
Ceate a page called for example Show with the slug:
Step #2:
Create a custom page template:
located in the current theme directory.
Step #3:
We construct the following
pre_get_posts
action callback:where
This should also give us pagination:
etc.
Notes
I updated the answer and removed the query-object part modification, based on the suggestion from @PieterGoosen, since it could e.g. break the breadcrumbs on his setup.
Also removed the
is_page()
check within thepre_get_posts
hook, since it might still give some irregularities in some cases. The reason is that the query-object is not always available. This is being worked on, see e.g. #27015. There are workarounds possible if we want to use theis_page()
oris_front_page()
.I constructed the following table, just to get a better overview of some of the properties and query varaiables of the main
WP_Query
object, for a given slug:It’s interesting to note that the pagination in
WP_Query
depends on thenopaging
not being set and the current page not being singular (from the 4.4 source):where we can see that the
LIMIT
part of the generated SQL query is within the conditional check. This explains why we modify theis_singular
property above.We could have used other filter/hooks, but here we used
pre_get_posts
as mentioned by the OP.Hope this help.
With inspiration from @birgire answer, I came up with the following idea. (NOTE: This is a copy of my answer from this answer over at WPSE)
What I tried to do here was to rather use post injection than completely altering the main query and getting stuck with all the above issues, like directly altering globals, the global value issue and reassigning page templates.
By using post injection, I’m able to keep full post integrity, so
$wp_the_query->post
,$wp_query->post
,$posts
and$post
stays constant throughout the template, they all only holds the current page object as is the case with true pages. This way, functions like breadcrumbs still think that the current page is a true page and not some kind of archiveI had to alter the main query slightly (through filters and actions) to adjust for pagination though, but we will come to that.
POST INJECTION QUERY
In order to accomplish post injection, I used a custom query to return the posts needed for injection. I also used the custom query’s
$found_pages
property to adjust that of the main query to get pagination working from the main query. Posts are injected into the main query through theloop_end
action.In order to make the custom query accessible and usable outside the class, I introduced a couple of actions.
Pagination hooks in order to hook pagination funtions:
pregetgostsforgages_before_loop_pagination
pregetgostsforgages_after_loop_pagination
Custom counter which counts the posts in the loop. These actions can be used to alter how posts are displayed inside the loop according to post number
pregetgostsforgages_counter_before_template_part
pregetgostsforgages_counter_after_template_part
General hook to access the query object and current post object
pregetgostsforgages_current_post_and_object
These hooks gives you a total hands-off experience as you do not need to change anything in the page template itself, which was my original intention from the start. A page can completely be altered from a plugin or a function file which makes this very dynamic
I have also used
get_template_part()
in order to load a template part which will be used to display the posts on. Most themes today make use of template parts, that makes this very useful in the class. If your theme usescontent.php
, you can simply passcontent
to$templatePart
to loadcontent.php
.If you need post format support for template parts, it is easy, you can still just pass
content
to$templatePart
and simply set$postFormatSupport
totrue
and a template partcontent-video.php
will be loaded for a post with a post format ofvideo
THE MAIN QUERY
The following changes was done to the main query through the respective filters and actions
In order to paginate the main query:
The injector query’s
$found_posts
property value is passes to that of the main query object through thefound_posts
filterSet the value of the user passed parameter
posts_per_page
to the main query throughpre_get_posts
$max_num_pages
is calculated using the amount of posts in$found_posts
andposts_per_page
. Becauseis_singular
is true on pages, it inhibits theLIMIT
clause being set. Simply settingis_singular
to false caused a few issues, so I decided to set theLIMIT
clause through thepost_limits
filter. I kept theoffset
of theLIMIT
clause set to0
to avoid 404’s on paged pagesThis takes care of pagination and any issue that might arise from the post injection
THE PAGE OBJECT
The current page object is available to display as a post by using the default loop on the page, separate and on top of the injected posts. If you do not need this, you can simply set
$removePageFromLoop
to true, this will hide the page content from being displayed.At this stage I’m using CSS to hide the page object through the
loop_start
andloop_end
actions as I cannot find another way of doing this. The downside with this method is that anything hooked tothe_post
action hook inside the main query will also be hidden by default if you hide the page object.THE CLASS
The
PreGetPostsForPages
class can be improved and should be properly namespaced as well Although you can simply drop this in your theme’s functions file, it would be better to drop this into a custom plugin.Use, modify and abuse as you see fit. The code is well commented, so it should be easy to follow and adjust
USAGE
You can now initiate the class (also in your plugin or functions file) as follow to target the page with ID 251 on which we will show 2 posts per page from the
post
post typeADDING PAGINATION AND CUSTOM STYLING
As I said, there are a few actions in the injector query in order to add pagination or custom styling. Here I added pagination after the loop using my own pagination function from the linked answer. Also, using the build in counter, I added a div to to display my posts in two colums.
Here are the actions I used
Note that pagination is set by the main query, not the injector query, so build-in functions like
the_posts_pagination()
should also work.This is the end result
STATIC FRONT PAGES
Everything works as expected on static front pages together with my pagination function without having to do no modifications
CONCLUSION
This might seem like a really lot of overheads, and it might be, but the pro’s outweigh the con’s big time
BIG PRO’S
You do not need to alter the page template for the specific page in any way. This makes everything dynamic and can easily be transferred between themes without making modifications to the code whatsoever if everything is done in a plugin.
At most, you only need to create a
content.php
template part in your theme if your theme does not have one yetAny pagination that works on the main query will work on the page without any type of alteration or anything extra from the query being passed to function.
There are more pro’s which I cannot think of now, but these are the important ones
I hope this will help someone in future