Create a custom archive page for a custom post type in a plugin

I’m writing a plugin that creates a custom post type named “my_plugin_lesson”:

$args = array (
    'public' => true,
    'has_archive' => true,
    'rewrite' => array('slug' => 'lessons', 'with_front' => false)
);
register_post_type ('my_plugin_lesson', $args);

The custom post type has an archive, and the URL of the archive is:

http://example.com/lessons

I want to customize the look of this archive; I want to list the posts in a table format, rather than the standard WordPress blog post archive. I understand that a custom archive template could be created in the theme by making the archive-my_plugin_lesson.php file; however, I would like the plugin to work with any theme.

How can I alter the content of the archive page without adding or modifying theme files?

Edit:
I understand that I could use the archive_template filter hook. However, all this does is replace the theme template, which still needs to be theme-specific. For example, just about every theme template will need the get_header, get_sidebar, and get_footer functions, but what should the id of the content <div> be? This is different in every theme.

What I would like to do is to replace the content itself with my own content, and use that in place of the archive page for my custom post type.

Solutions Collecting From Web of "Create a custom archive page for a custom post type in a plugin"

You can use the archive_template hook to process the content of a theme’s archive template, by using the scheme below, but obviously you’ll only ever be able to process a fraction of the themes out there, given that a template can basically contain any old thing.

The scheme is to load the template into a string ($tpl_str) in the archive_template filter, substitute your content, include the string (using the trick eval( '?>' . $tpl_str );), and then return a blank file so that the include in “wp-includes/template-loader.php” becomes a no-op.

Below is a hacked version of code I use in a plugin, which targets “classic” templates that use get_template_part and is more concerned with processing single templates than archive, but should help you get started. The setup is that the plugin has a subdirectory called “templates” which holds a blank file (“null.php”) and content templates (eg “content-single-posttype1.php”, “content-archive-postype1.php”) as well as a fall back template “single.php” for the single case, and utilizes a custom version of get_template_part that looks in this directory.

define( 'MYPLUGIN_FOLDER', dirname( __FILE__ ) . '/' );
define( 'MYPLUGIN_BASENAME', basename( MYPLUGIN_FOLDER ) );

add_filter( 'single_template', 'myplugin_single_template' );
add_filter( 'archive_template', 'myplugin_archive_template' );

function myplugin_single_template( $template ) {
    static $using_null = array();

    // Adjust with your custom post types.
    $post_types = array( 'posttype1', );

    if ( is_single() || is_archive() ) {
        $template_basename = basename( $template );
        // This check can be removed.
        if ( $template == '' || substr( $template_basename, 0, 4 ) == 'sing' || substr( $template_basename, 0, 4 ) == 'arch' ) {
            $post_type = get_post_type();
            $slug = is_archive() ? 'archive' : 'single';
            if ( in_array( $post_type, $post_types ) ) {
                // Allow user to override.
                if ( $single_template = myplugin_get_template( $slug, $post_type ) ) {
                    $template = $single_template;
                } else {
                    // If haven't gone through all this before...
                    if ( empty( $using_null[$slug][$post_type] ) ) {
                        if ( $template && ( $content_template = myplugin_get_template( 'content-' . $slug, $post_type ) ) ) {
                            $tpl_str = file_get_contents( $template );
                            // You'll have to adjust these regexs to your own case - good luck!
                            if ( preg_match( '/get_template_part\s*\(\s*\'content\'\s*,\s*\'' . $slug . '\'\s*\)/', $tpl_str, $matches, PREG_OFFSET_CAPTURE )
                            || preg_match( '/get_template_part\s*\(\s*\'content\'\s*,\s*get_post_format\s*\(\s*\)\s*\)/', $tpl_str, $matches, PREG_OFFSET_CAPTURE )
                            || preg_match( '/get_template_part\s*\(\s*\'content\'\s*\)/', $tpl_str, $matches, PREG_OFFSET_CAPTURE )
                            || preg_match( '/get_template_part\s*\(\s*\'[^\']+\'\s*,\s*\'' . $slug . '\'\s*\)/', $tpl_str, $matches, PREG_OFFSET_CAPTURE ) ) {
                                $using_null[$slug][$post_type] = true;
                                $tpl_str = substr( $tpl_str, 0, $matches[0][1] ) . 'include \'' . $content_template . '\'' . substr( $tpl_str, $matches[0][1] + strlen( $matches[0][0] ) );
                                // This trick includes the $tpl_str.
                                eval( '?>' . $tpl_str );
                            }
                        }
                    }
                    if ( empty( $using_null[$slug][$post_type] ) ) {
                        // Failed to parse - look for fall back template.
                        if ( file_exists( MYPLUGIN_FOLDER . 'templates/' . $slug . '.php' ) ) {
                            $template = MYPLUGIN_FOLDER . 'templates/' . $slug . '.php';
                        }
                    } else {
                        // Success! "null.php" is just a blank zero-byte file.
                        $template = MYPLUGIN_FOLDER . 'templates/null.php';
                    }
                }
            }
        }
    }
    return $template;
}

function myplugin_archive_template( $template ) {
    return myplugin_single_template( $template );
}

The custom get_template_part:

/*
 * Version of WP get_template_part() that looks in theme, then parent theme, and finally in plugin template directory (sub-directory "templates").
 * Also looks initially in "myplugin" sub-directory if any in theme and parent theme directories so that plugin templates can be kept separate.
 */
function myplugin_get_template( $slug, $part = '' ) {
    $template = $slug . ( $part ? '-' . $part : '' ) . '.php';

    $dirs = array();

    if ( is_child_theme() ) {
        $child_dir = get_stylesheet_directory() . '/';
        $dirs[] = $child_dir . MYPLUGIN_BASENAME . '/';
        $dirs[] = $child_dir;
    }

    $template_dir = get_template_directory() . '/';
    $dirs[] = $template_dir . MYPLUGIN_BASENAME . '/';
    $dirs[] = $template_dir;
    $dirs[] = MYPLUGIN_FOLDER . 'templates/';

    foreach ( $dirs as $dir ) {
        if ( file_exists( $dir . $template ) ) {
            return $dir . $template;
        }
    }
    return false;
}

For completeness here’s the fall back “single.php”, that uses the custom get_template_part:

<?php
get_header(); ?>

    <div id="primary" class="content-area">
        <div id="content" class="clearfix">
            <?php while ( have_posts() ) : the_post(); ?>

            <?php if ( $template = myplugin_get_template( 'content-single', get_post_type() ) ) include $template; else get_template_part( 'content', 'single' ); ?>

                <?php
                    // If comments are open or we have at least one comment, load up the comment template
                    if ( comments_open() || '0' != get_comments_number() ) :
                        comments_template();
                    endif;
                ?>

            <?php endwhile; ?>

        </div><!-- #content -->
    </div><!-- #primary -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

What you need is hooking template_include filter and selectively load your template inside plugin.

As a good practice, if you plan to distribute your plugin, you should check if archive-my_plugin_lesson.php (or maybe myplugin/archive-lesson.php) exists in theme, if not use the plugin version.

In this way is easy for users replace the template via theme (or child theme) without edit the plugin code.

This is the method used by popular plugins, e.g. WooCommmerce, just to say one name.

add_filter('template_include', 'lessons_template');

function lessons_template( $template ) {
  if ( is_post_type_archive('my_plugin_lesson') ) {
    $theme_files = array('archive-my_plugin_lesson.php', 'myplugin/archive-lesson.php');
    $exists_in_theme = locate_template($theme_files, false);
    if ( $exists_in_theme != '' ) {
      return $exists_in_theme;
    } else {
      return plugin_dir_path(__FILE__) . 'archive-lesson.php';
    }
  }
  return $template;
}

More info on Codex for

  • template_include filter
  • locate_template function

I’ve been pondering the same question, and this is the hypothetical solution I’ve come up with:

  • Within the plugin, create a shortcode that outputs your archive loop
    the way you desire.
  • When creating the custom post type, do not enable the ‘archive’
    option.
  • Add a stylesheet that controls all the styles of your loop contents.

Upon plugin activation create a page using wp_insert_post with the name being the post type and the content being the shortcode.

You can provide options in the shortcode for additional style considerations, or add classes to the post container to match theme specific or custom styles. The user can also add additional content before/after the loop by editing the page.

You can use the filter single_template. A basic example taken from the Codex:

function get_custom_post_type_template($single_template) {
     global $post;

     if ($post->post_type == 'my_post_type') {
          $single_template = dirname( __FILE__ ) . '/post-type-template.php';
     }
     return $single_template;
}

add_filter( "single_template", "get_custom_post_type_template" );