REVISIT: Feb. 2nd 2016
The original code had many issues
Data wasn't sanitized and validated which can lead to serious security issues
Some parts were repetitive
Bit messy and sometimes hard to read
Some sections was only partly working
Used globals which is really evil
That is why I revisited this answer and updated the code in order to solve the issues above. The code is now cleaner, safer and easier to read and debug. Make sure to check it out in the ORIGINAL ANSWER section
Before I go to the original ORIGINAL ANSWER section, I want to add an alternative which I think is a bit better to use
ALTERNATIVE WAY
This is a straight forward alternative solution which does not involve custom templates (except maybe a content.php
) or modifying any templates. All you need to do is
create a new page with any page template you wish
create a content.php
template part of any such template part if your theme does not have these available by default
add the following code and your done
$query = new PreGetPostsForPages(
251, // Page ID we will target
'content', //Template part which will be used to display posts, name should be without .php extension
true, // Should get_template_part support post formats
false, // Should the page object be excluded from the loop
[ // Array of valid arguments that will be passed to WP_Query/pre_get_posts
'post_type' => 'post',
'posts_per_page' => 2
]
);
$query->init();
The PreGetPostsForPages
class can be found in my answer here and also a detailed explanation on how to use it
ORIGINAL ANSWER
If you have a look at the template hierarchy, custom post types are usually displayed on archive templates. Normal template hierarchy does not make provision for page.php
type templates to be used to display custom post types by default.
The problem with archive templates is that they don't automatically get added to the default nav menu, and creating a custom menu to create links is not always the most convenient way to go.
The way to go here is to use WP_Query
to create a custom query for the loop to include custom post types. WP_Query
have a post_type
type parameter which is used to call post types.
So, the following needs to be modified to make this work:
Firstly, create a custom page.php
template
To create the custom page.php
you need to copy your theme's page.php
and rename it something like page-cpt.php
. Now open it and change the loop. For the sake of this answer, I've used the default twentyfourteen theme. Delete everything inside the template and replace it with this code
EDIT I have came back to change the code. The previous code used the following in the custom query
global $post;
$tmp_post = $post;
$wp_query= null;
$wp_query = new WP_Query();
$wp_query->query( $args );
which also translate to query_posts
, which should never be used. So I changes the code accordingly to execute a proper instance of WP_Query
. Here is the edited code
<?php
/**
* Template Name: Custom Post Type Page
*/
get_header(); ?>
<?php
//See if we have any values
$post_meta = get_post_meta( $post->ID,false );
$posttype = isset( $post_meta['_cpt_post_type'] ) ? $post_meta['_cpt_post_type'][0] : 1;
$page_title = isset( $post_meta['_cpt_page_title'] ) ? $post_meta['_cpt_page_title'][0] : '';
$posts_title = isset( $post_meta['_cpt_posts_title'] ) ? $post_meta['_cpt_posts_title'][0] : '';
$orderby = isset( $post_meta['_cpt_order_by'] ) ? $post_meta['_cpt_order_by'][0] : 'date';
$asc = isset( $post_meta['_cpt_asc'] ) ? $post_meta['_cpt_asc'][0] : 'DESC';
$post_count = isset( $post_meta['_cpt_post_count'] ) ? $post_meta['_cpt_post_count'][0] : get_option('posts_per_page');
?>
<div id="main-content" class="main-content">
<div id="primary" class="content-area">
<div id="content" class="site-content" role="main">
<!-- Page Title -->
<?php if( $page_title ) { ?>
<article id="posts-title">
<header class="entry-header">
<h2 class="entry-title"><?php echo $page_title; ?></h2>
</header><!-- .entry-header -->
</article><!-- #posts-title -->
<?php } ?>
<?php the_post(); ?>
<?php global $post;
if( $post->post_content || $page_title ) : ?>
<div class="entry-content">
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<?php if( $posts_title ) : ?>
<header class="entry-header">
<h1 class="entry-title"><?php echo $posts_title; ?></h1>
</header><!-- .entry-header -->
<?php endif; ?>
<?php if( $post->post_content ) : ?>
<div class="entry-content">
<?php the_content(); ?>
<?php wp_link_pages( ['before' => '<div class="page-link"><span>' . __( 'Pages:' ) . '</span>', 'after' => '</div>'] ); ?>
</div><!-- .entry-content -->
<footer class="entry-meta">
</footer><!-- .entry-meta -->
<?php endif; ?>
</article><!-- #post-<?php the_ID(); ?> -->
</div>
<?php endif; ?>
<?php
/**-----------------------------------------------------------------------------
*
* Start our custom query to display custom post type posts
*
*------------------------------------------------------------------------------*/
$args = [
'post_type' => $posttype,
'posts_per_page' => $post_count,
'paged' => $paged,
'order' => $asc,
'ignore_sticky_posts' => 1,
];
$cpt_query = new WP_Query($args);
// Output
if ( $cpt_query->have_posts() ) :
// Start the Loop.
while ( $cpt_query->have_posts() ) {
$cpt_query->the_post();
get_template_part( 'content', get_post_format() );
}
if ( function_exists( 'pietergoosen_pagination' ) )
pietergoosen_pagination();
wp_reset_postdata();
} else {
get_template_part( 'content', 'none' );
} ?>
</div><!-- #content -->
</div><!-- #primary -->
<?php get_sidebar( 'content' ); ?>
</div><!-- #main-content -->
<?php
get_footer();
The first piece of code is used to call the settings from the db. This will be set via a metabox in the back end when creating a new page in the page editor screen. The important code here is the arguments for WP_Query
.
$args = [
'post_type' => $posttype,
'posts_per_page' => $post_count,
'paged' => $paged,
'order' => $asc,
'ignore_sticky_posts' => 1,
];
This will decide which custom post types will be displayed, posts per page and the order of posts. All these settings are called from the db, and is set in the custom meta box in the back end
Secondly, create a custom meta box
This metabox will be diplayed in the "Page" screen when a new page is created and "Custom Post Type Page" is selected in the "Page Attributes" meta box.
Add the following in your functions.php
or custom functions file
<?php
add_action( 'admin_init', function ()
{
$post_id = filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
if ( $post_id ) {
// Get the current page template
$post_meta = get_post_meta( $post_id );
// Make sure that we only target our desired template
if ( isset ( $post_meta['_wp_page_template'][0] )
&& 'page-cpt.php' === $post_meta['_wp_page_template'][0]
) {
add_meta_box(
'cpt_meta_box',
__( 'Page of post from a given custom post type' ),
'cpt_metabox_options',
'page',
'side',
'core'
);
} else {
if( isset( $meta['_cpt_post_type'][0] ) ) {
$meta_value_array = [
'_cpt_post_type',
'_cpt_page_title',
'_cpt_posts_title',
'_cpt_order_by',
'_cpt_asc',
'_cpt_post_count'
];
foreach ( $meta_value_array as $value )
cpt_helper_update_post_meta( $post_id, $value, '' );
remove_meta_box( 'cpt_meta_box', 'page', 'side' );
}
}
}
add_action( 'save_post', 'cpt_update_post_meta_box' );
});
function get_cpt_order_by_list()
{
// Set the sort order
$sort = [
[
'DESC' => [
'value' => 'DESC',
'label' => 'Descending'
],
'ASC' => [
'value' => 'ASC',
'label' => 'Ascending'
],
]
];
// Create an array of values to order the posts by
$order_list = [
[
'none' => [
'value' => 'none',
'label' => 'None'
],
'id' => [
'value' => 'ID',
'label' => 'Post ID'
],
'author' => [
'value' => 'author',
'label' => 'Author'
],
'title' => [
'value' => 'title',
'label' => 'Post Title'
],
'date' => [
'value' => 'date',
'label' => 'Post Date'
],
'modified' => [
'value' => 'modified',
'label' => 'Modified Date'
],
'parent' => [
'value' => 'parent',
'label' => 'Parent Post'
],
'rand' => [
'value' => 'rand',
'label' => 'Random'
],
'comment_count' => [
'value' => 'comment_count',
'label' => 'Comment Count'
],
'menu_order' => [
'value' => 'menu_order',
'label' => 'Menu Order'
],
]
];
return $list = array_merge( $sort, $order_list );
}
function cpt_metabox_options()
{
$post_id = filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
if ( !$post_id )
return;
// Make sure the current user have the edit_page ability
if ( !current_user_can( 'edit_post', $post_id ) )
return;
// Get the current page template
$template_file = get_post_meta( $post_id, '_wp_page_template', true );
// Make sure that we only target our desired template
if ( 'page-cpt.php' !== $template_file )
return;
// Get all the post meta values and sanitize/validate them
$post_meta = get_post_meta( $post_id );
$filters = [
'_cpt_post_type' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_page_title' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_posts_title' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_order_by' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => 'ID'
],
'_cpt_asc' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => 'DESC'
],
'_cpt_post_count' => [
'filter' => FILTER_VALIDATE_INT,
'default' => get_option( 'posts_per_page' )
],
];
foreach ( $filters as $key=>$value ) {
if ( !array_key_exists( $key, $post_meta ) ) {
$post_meta[$key][0] = $value['default'];
} else {
$post_meta[$key][0] = filter_var( $post_meta[$key][0], $value['filter'], $value['default'] );
}
}
?>
<!-- Sart the meta boxes -->
<div class="inside">
<p>
<label>
<strong><?php _e( 'Page Title' ); ?></strong>
</label>
</p>
<input id="_cpt_page_title" name="_cpt_page_title" type="text" style="width: 98%;" value="<?php echo $post_meta['_cpt_page_title'][0]; ?>"/>
<p>
<label>
<strong><?php _e( 'Post Title' ); ?></strong>
</label>
</p>
<input id="_cpt_posts_title" name="_cpt_posts_title" type="text" style="width: 98%;" value="<?php echo $post_meta['_cpt_posts_title'][0]; ?>"/>
<p>
<label>
<strong><?php _e( 'Custom Post Type' ); ?></strong>
</label>
</p>
<select id="_cpt_post_type" name="_cpt_post_type">
<?php
//Custom Post Type List
$args = [
'public' => true,
'_builtin' => false
];
$output = 'names'; // names or objects, note names is the default
$operator = 'and'; // 'and' or 'or'
$post_types = get_post_types( $args, $output, $operator );
foreach ( $post_types as $post_type ) {
$selected = ( $post_type == $post_meta['_cpt_post_type'][0] ) ? ' selected = "selected" ' : '';
$option = '<option '.$selected .'value="'. $post_type;
$option = $option .'">';
$option = $option .$post_type;
$option = $option .'</option>';
echo $option;
} //endforeach;
?>
</select>
<?php
if ( function_exists( 'get_pop_order_by_list' ) ) {
$list = get_pop_order_by_list();
?>
<p>
<label>
<strong><?php _e( 'Sort by' )?></strong>
</label>
</p>
<select id="_cpt_order_by" name="_cpt_order_by">
<?php
foreach ( $list[0] as $output ) {
$selected = ( $output['value'] == $post_meta['_cpt_order_by'][0] ) ? ' selected = "selected" ' : '';
$option = '<option '.$selected .'value="' . $output['value'];
$option = $option .'">';
$option = $option .$output['label'];
$option = $option .'</option>';
echo $option;
} //endforeach;
?>
</select>
<p>
<label>
<strong><?php _e( 'Order' )?><strong>
</label>
</p>
<select id="_cpt_asc" name="_cpt_asc">
<?php
foreach ( $list[1] as $output ) {
$selected = ( $output['value'] == $post_meta['_cpt_asc'][0] ) ? ' selected = "selected" ' : '';
$option = '<option '.$selected .'value="' . $output['value'];
$option = $option .'">';
$option = $option .$output['label'];
$option = $option .'</option>';
echo $option;
} //endforeach;
?>
</select>
<?php
}
?>
<p>
<label>
<strong><?php _e( 'Posts per Page' ); ?><strong>
</label>
</p>
<input id="_cpt_post_count" name="_cpt_post_count" type="text" value="<?php echo $post_meta['_cpt_post_count'][0]; ?>" size="3" />
</div>
<!-- End page of posts meta box -->
<?php
}
function cpt_update_post_meta_box( $post_id )
{
// Make sure we have a valid $_POST method
if ( !$_POST )
return;
// Make sure the current user have the edit_page ability
if ( !current_user_can( 'edit_page', $post_id ) )
return;
// Get the current page template
$template_file = get_post_meta( $post_id, '_wp_page_template', true );
// Make sure that we only target our desired template
if ( 'page-cpt.php' !== $template_file )
return;
// Do nothing on auto save, just bail
if ( defined( 'DOING_AUTOSAVE' )
&& DOING_AUTOSAVE
)
return;
$args = [
'_cpt_post_type' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_page_title' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_posts_title' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => ''
],
'_cpt_order_by' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => 'date'
],
'_cpt_asc' => [
'filter' => FILTER_SANITIZE_STRING,
'default' => 'DESC'
],
'_cpt_post_count' => [
'filter' => FILTER_VALIDATE_INT,
'default' => get_option( 'posts_per_page' )
],
];
$meta = filter_input_array( INPUT_POST, $args );
if ( !$meta )
return;
// Loop throught the array and update meta values
foreach ( $meta as $k=>$v )
cpt_helper_update_post_meta( $post_id, $k, $v );
}
function cpt_helper_update_post_meta( $post_id = '', $key = '', $data = '' )
{
// Make sure we have valid values, if not, return false
if ( !$post_id
|| !$key
)
return false;
// Sanitize and validate values
$post_id = filter_var( $post_id, FILTER_VALIDATE_INT );
$key = filter_var( $key, FILTER_SANITIZE_STRING );
$data = filter_var( $data, FILTER_SANITIZE_STRING );
// Get the post meta values
$post_meta = get_post_meta( $post_id, $key, true );
if( $data
&& $post_meta != $data
) {
update_post_meta( $post_id, $key, $data );
}
if ( $post_meta
&& !$data
) {
delete_post_meta( $post_id, $key );
}
}
What this code do is to register and display the meta box, add the options to the metabox and storing the options to the db for use in the page-cpt.php
template.
You can now go and create a new page, and call the page whatever you like. In the "Page Attributes", select "Custom Post Type Page" and "Publish" your page. The metabox for the custom post types options will now appear above the "Publish" metabox, and will display all the current available custom post types. Select and set the options you need to display and click "Update". Your page will now show posts from the custom post type you have selected, and your page will be visible in the nav bar.
You can add more functionality to this, or change the code to display categories or taxonomies in the same way. Hope this help