How to control output of custom post type without modifying theme?

I have a custom post type ‘properties’ that I’d like to control the HTML output for. For simplicity, let’s focus on the archive view.

As a basic example, here is what a loop looks like in an archive.php file:

<?php while ( have_posts() ) : the_post(); ?>
    <h2><?php the_title(); ?></h2>
    <div><?php the_content(); ?></div>
<?php endwhile; ?>

I’d like to be able to modify the output of the loop with my custom ‘properties’ HTML without adding a new template or using a shortcode – basically, without user intervention. Just to be clear, I’d like to replace the <h2> and <div> in the above example, and nothing before/after it.

Note: The above is only an example. I would like to control the loop output regardless of theme.

Right now I am using output buffering to capture the output from loop_start to loop_end and replacing it with my own, but this potentially can create conflicts with other plugins.

Is there an accepted/better way to do this?

Solutions Collecting From Web of "How to control output of custom post type without modifying theme?"

There’re two very often forgotten action ref arrays: loop_start/_end().

Just turn on output buffering and you’re ready to go.

add_action( 'loop_start', 'wpse75307_plugin_loop' );
add_action( 'loop_end', 'wpse75307_plugin_loop' );
/**
 * Callback function triggered during:
 * + 'loop_start'/`have_posts()` after the last post gets rendered
 * + 'loop_end'/`the_post()` before the 1st post gets rendered
 * @param  object \WP_Query Passed by reference
 * @return 
 */
function wpse75307_plugin_loop( &$obj )
{
    # if ( is_main_query() )
    # DO STUFF ... OR DONT
    global $post;

    // Start output buffering at the beginning of the loop and abort
    if ( 'loop_start' === current_filter() )
        return ob_start();

    // At the end of the loop, we end the buffering and save into a var
    # if ( is_main_query() )
    # DO STUFF ... OR DONT
    $loop_content = ob_get_flush();

    // You can do something with $loop_content...
    // Add your own loop, or...
    // Whatever you can imagine
}

Note: I wouldn’t do it like this, but as you said, you want exactly that level of overriding, here you go.

You can change the loop via hook, like this for cpt ‘my_post_type ‘

// $this? - example was used in class-structures
// add custom post type to wp loop
add_filter( 'pre_get_posts', array( $this, 'add_to_query') );

// ads to query
function add_to_query( $query ) {

    if ( is_admin() || is_preview() )
        return;

    if ( ! isset( $query -> query_vars['suppress_filters'] ) )
        $query -> query_vars['suppress_filters'] = FALSE;

    // conditional tags for restrictions
    if ( is_home() || is_front_page() && ( FALSE == $query -> query_vars['suppress_filters'] ) ) 
        $query->set( 'post_type', array( 'post', 'my_post_type' ) );

    return $query;
}

If remembering well, I derived this technique from here: Use template_include with custom post types.

We use a plugin that will generate a “Virtual Template” for a given Post Type. The filter template_include will render a template file that resides in the plugin folder.

You have to refine the function custom_template and the template file to suit your needs. Hope this helps 😉


Plugin File

adjust the post_type name in the class instantiation

<?php
! defined( 'ABSPATH' ) AND exit;
/*
Plugin Name: Virtual Template for CPT
Plugin URI: https://wordpress.stackexchange.com/q/75307/12615
Description: Use the plugin's template file to render an outsider loop.
Author: brasofilo
Author URI: https://wordpress.stackexchange.com/users/12615/brasofilo
Version: 2012.12.11
License: GPLv2
*/

$virtual_template = new VirtualTemplateForCPT_class( 'movies' );

class VirtualTemplateForCPT_class
{   
    private $pt;
    private $url;
    private $path;

    /**
     * Construct 
     *
     * @return void
     **/
    public function __construct( $pt )
    {       
        $this->pt = $pt;
        $this->url = plugins_url( '', __FILE__ );
        $this->path = plugin_dir_path( __FILE__ );
        add_action( 'init', array( $this, 'init_all' ) );
    }


    /**
     * Dispatch general hooks
     *
     * @return void
     **/
    public function init_all() 
    {
        add_action( 'wp_enqueue_scripts',   array( $this, 'frontend_enqueue' ) );
        add_filter( 'body_class',           array( $this, 'add_body_class' ) );
        add_filter( 'template_include',     array( $this, 'custom_template' ) );
    }

    /**
     * Use for custom frontend enqueues of scripts and styles
     * http://codex.wordpress.org/Plugin_API/Action_Reference/wp_enqueue_scripts
     *
     * @return void
     **/
    public function frontend_enqueue() 
    {
        global $post;
        if( $this->pt != get_post_type( $post->ID ) )
            return;         
    }

    /**
     * Add custom class to body tag
     * http://codex.wordpress.org/Function_Reference/body_class
     *
     * @param array $classes
     * @return array
     **/
    public function add_body_class( $classes ) 
    {
        global $post;
        if( $this->pt != get_post_type( $post->ID ) )
            return $classes;

        $classes[] = $this->pt . '-body-class';
        return $classes;
    }

    /**
     * Use the plugin template file to display the CPT
     * http://codex.wordpress.org/Conditional_Tags
     *
     * @param string $template
     * @return string
     **/
    public function custom_template( $template ) 
    {
        $post_types = array( $this->pt );
        $theme = wp_get_theme();

        if ( is_post_type_archive( $post_types ) )
            $template = $this->path . '/single-virtual-cpt.php';

        if ( is_singular( $post_types ) )
            $template = $this->path . '/single-virtual-cpt.php';

        return $template;
    }

}

Template file

single-virtual-cpt.php
put in the same folder as the plugin file

<?php
/**
 * A custom -not from the theme- template
 *
 * @package WordPress
 * @subpackage Virtual_Template
 */

get_header(); 

while ( have_posts() ) : the_post(); $theID = $post->ID; 
    _e('This is a virtual template for the post:<br />');
    the_title();
endwhile; 
get_footer(); 

?>
</body>
</html>

Result

virtual template in frontend
click to enlarge ⤴

I’ve been trying to figure out a solution for this too – I have a plugin with custom post types, those custom post types require a custom archive but creating a specific template file means it wouldn’t work with every theme.

The closest alternative I can think of that hasn’t been mentioned above is to read the archive/index.php file on plugin activation (and theme change) and to copy the archive.php file to your plugin folder with the relevant name change to be specific for your custom post type.

Then, have your plugin automatically modify the custom template file to inject your own loop code.

<?php while (have_posts()) : the_post(); ?>     
    <?php if(is_search()): ?>
        <?php get_template_part( 'includes/loop' , 'search'); ?>
    <?php else: ?>
        <?php get_template_part( 'includes/loop' , 'index'); ?>
    <?php endif; ?>
<?php endwhile; ?>

e.g. in above little snippet from an index.php file, scan for while(have_posts()…) line and corresponding line and replace everything in between with your custom loop html code.

I’ve not tried this, it’s just another potential solution and I’d appreciate comments if anyone else has used this approach.