How to show Y number of custom posts after every X normal posts?

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.