Here's an idea:
Definitions:
- Posts per page:
PPP
- Custom post type:
Y
- Main query post type:
X
- How many
Y
posts to inject each time: y
- How many
X
posts to display before injecting the Y
posts: x
Formula:
We will use:
PPP(Y) = y * floor( ( PPP(X) -1 ) / x )
where PPP(X)
, x
and y
are positive.
Examples:
Setup 1:
PPP(X)=1, x=3, y=2, PPP(Y) = 2 * floor( (1-1)/3 ) = 2 * 0 = 0
Loop: X
Setup 2:
PPP(X)=3, x=3, y=2, PPP(Y) = 2 * floor( (3-1)/3 ) = 2 * 0 = 0
Loop: XXX
Setup 3:
PPP(X)=4, x=3, y=2, PPP(Y) = 2 * floor( (4-1)/3 ) = 2 * 1 = 2
Loop: XXX YY X
Setup 4:
PPP(X)=11, x=3, y=2, PPP(Y) = 2 * floor( (11-1)/3 ) = 2 * 3 = 6
Loop: XXX YY XXX YY XXX YY XX
Setup 5:
PPP(X)=12, x=3, y=2, PPP(Y) = 2 * floor( (12-1)/3 ) = 2 * 3 = 6
Loop: XXX YY XXX YY XXX YY XXX
Setup 6:
PPP(X)=13, x=3, y=2, PPP(Y) = 2 * floor( (13-1)/3 ) = 2 * 4 = 8
Loop: XXX YY XXX YY XXX YY XXX YY X
Strategy:
Let's try to find a way to inject the Y
posts into the loop of X
posts, without modifying the template files directly.
We want to hook into the loop_start
action for the main query of post type X
, and fetch PPP(Y)
posts of type Y
.
Then we want to use the the_post
hook to display:
get_template_part( 'content', 'y' );
We can use the for example the current_post
and posts_per_page
properties of the main $wp_query
object to control the injection logic.
I think it's easier to call WP_Query()
for each injection, but let's constrain ourselves to call it only once before the main loop.
Implementation:
Create a template part file in your active theme's directory, for example content-page.php
, containing:
<article>
<header class="entry-header">
<h2 class="entry-title"><?php the_title(); ?></h2>
</header>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
Then you can try the following:
add_action( 'wp', function(){
// We want the injection only on the home page:
if( ! is_home() ) return;
// Start the injection:
$inject = new WPSE_Inject( array(
'items_before_each_inject' => 3,
'cpt_items_per_inject' => 2,
'cpt' => 'page',
'template_part' => 'content-page',
'paged' => ( $pgd = get_query_var( 'paged' ) ) ? $pgd : 1,
) );
$inject->init();
});
and modify it to your needs.
The WPSE_Inject
class is defined as:
/**
* class WPSE_Inject
*
* Inject custom posts into the main loop, through hooks,
* with only a single WP_Query() call
*
* @link http://wordpress.stackexchange.com/a/141612/26350
*
* Definitions:
* Posts per page: PPP
* Custom post type: Y
* Main query post type: X
* How many Y posts to inject: y
* How many X posts to display before injecting the Y posts: x
*
* Formula:
* PPP(Y) = y * floor( ( PPP(X) -1 ) / x )
* where PPP(X), x and y are positive
*/
class WPSE_Inject
{
protected $results = NULL;
protected $query = NULL;
protected $nr = 0;
protected $inject_mode = FALSE;
protected $args = array();
public function __construct( $args = array() )
{
$defaults = array(
'items_before_each_inject' => 5,
'cpt_items_per_inject' => 1,
'cpt' => 'post',
'paged' => 1,
'template_part' => 'content-post'
);
$this->args = wp_parse_args( $args, $defaults );
}
public function init()
{
add_action( 'loop_start', array( $this, 'loop_start' ) );
add_action( 'loop_end', array( $this, 'loop_end' ) );
}
public function cpt_items_on_this_page( WP_Query $query )
{
$count = $this->args['cpt_items_per_inject'] * floor( ( $query->get( 'posts_per_page' ) -1 ) / $this->args['items_before_each_inject'] );
return ( $count > 0 ) ? $count : 1;
}
public function loop_start( WP_Query $query )
{
$this->query = $query;
if( $query->is_main_query() )
{
$args = array(
'post_type' => $this->args['cpt'],
'posts_per_page' => $this->cpt_items_on_this_page( $query ),
'paged' => $this->args['paged'],
'suppress_filters' => TRUE,
);
$this->results = new WP_Query( $args );
add_action( 'the_post', array( $this, 'the_post' ) );
}
}
public function loop_end( WP_Query $query )
{
if( $query->is_main_query() )
remove_action( 'the_post', array( $this, 'the_post' ) );
}
public function the_post()
{
if( ! $this->inject_mode
&& 0 < $this->nr
&& 0 === $this->nr % $this->args['items_before_each_inject'] )
{
$this->inject_mode = TRUE;
$this->results->rewind_posts();
$this->results->current_post = ( absint( $this->nr / $this->args['items_before_each_inject'] ) -1 ) * $this->args['cpt_items_per_inject'] - 1;
$j = 1;
if ( $this->results->have_posts() ) :
while ( $this->results->have_posts() ) :
$this->results->the_post();
get_template_part( $this->args['template_part'] );
if( $this->args['cpt_items_per_inject'] < ++$j )
break;
endwhile;
wp_reset_postdata();
endif;
$this->inject_mode = FALSE;
}
if( ! $this->inject_mode )
$this->nr++;
}
}
I tested this on the default theme, where I injected page
content into the main post
loop. The pagination seemed to work as well.
This can be refined more, but hopefully this is a starting point for you.