🎯 Displaying Custom Content in Themes
Master advanced techniques for displaying CPTs and taxonomies
Create dynamic content displays with custom queries and loops
Learning Objectives
- Query custom post types with WP_Query
- Create custom loops for CPT display
- Filter content by taxonomies
- Implement pagination for custom queries
- Build archive pages with filtering
- Create related content sections
- Display content in different layouts
- Optimize queries for performance
Advanced Content Display Techniques
Displaying custom post types and taxonomies effectively requires understanding WordPress query system, loop structures, and various display patterns. This lesson covers comprehensive techniques for presenting custom content.
Key Concept
Querying Custom Post Types
Basic CPT Query
<?php
// Basic query for custom post type
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
);
$products = new WP_Query( $args );
if ( $products->have_posts() ) :
?>
<div class="products-grid">
<?php
while ( $products->have_posts() ) :
$products->the_post();
?>
<article id="product-<?php the_ID(); ?>" <?php post_class( 'product-card' ); ?>>
<?php if ( has_post_thumbnail() ) : ?>
<div class="product-image">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'medium' ); ?>
</a>
</div>
<?php endif; ?>
<h3 class="product-title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h3>
<div class="product-excerpt">
<?php the_excerpt(); ?>
</div>
<a href="<?php the_permalink(); ?>" class="product-link">
<?php _e( 'View Product', 'mytheme' ); ?>
</a>
</article>
<?php endwhile; ?>
</div>
<?php
wp_reset_postdata();
else :
?>
<p><?php _e( 'No products found.', 'mytheme' ); ?></p>
<?php endif; ?>
WP_Query Parameters for CPTs
| Parameter | Description | Example |
|---|---|---|
post_type |
Specify post type(s) | 'product' or array('product', 'portfolio') |
posts_per_page |
Number of posts | 10 (use -1 for all) |
orderby |
Sort posts by | 'date', 'title', 'menu_order', 'meta_value' |
order |
Sort direction | 'ASC' or 'DESC' |
meta_key |
Custom field key | '_product_price' |
meta_value |
Custom field value | 'featured' |
meta_query |
Complex meta queries | Array of conditions |
tax_query |
Taxonomy queries | Array of taxonomy conditions |
paged |
Current page number | get_query_var('paged') |
offset |
Number to skip | 3 (skip first 3) |
Complex Custom Queries
Query with Meta and Taxonomy
<?php
// Complex query with multiple conditions
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_product_featured',
'value' => 'yes',
'compare' => '='
),
array(
'key' => '_product_price',
'value' => 100,
'compare' => '<=',
'type' => 'NUMERIC'
),
),
'tax_query' => array(
array(
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => 'electronics',
),
),
'orderby' => 'meta_value_num',
'meta_key' => '_product_price',
'order' => 'ASC',
);
$featured_products = new WP_Query( $args );
Multiple Post Types Query
<?php
// Query multiple custom post types
$args = array(
'post_type' => array( 'product', 'portfolio', 'testimonial' ),
'posts_per_page' => 15,
'orderby' => 'post_type date',
'order' => 'DESC',
);
$mixed_content = new WP_Query( $args );
if ( $mixed_content->have_posts() ) :
while ( $mixed_content->have_posts() ) :
$mixed_content->the_post();
// Get template part based on post type
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
wp_reset_postdata();
endif;
Filtering and Sorting Content
AJAX Filter Implementation
<?php
// Filter form
?>
<form id="product-filter" class="filter-form">
<div class="filter-group">
<label><?php _e( 'Category:', 'mytheme' ); ?></label>
<?php
wp_dropdown_categories( array(
'taxonomy' => 'product_category',
'name' => 'product_cat',
'show_option_all' => __( 'All Categories', 'mytheme' ),
'hierarchical' => true,
'show_count' => true,
'orderby' => 'name',
) );
?>
</div>
<div class="filter-group">
<label><?php _e( 'Sort by:', 'mytheme' ); ?></label>
<select name="orderby" id="orderby">
<option value="date"><?php _e( 'Latest', 'mytheme' ); ?></option>
<option value="title"><?php _e( 'Name', 'mytheme' ); ?></option>
<option value="price_asc"><?php _e( 'Price: Low to High', 'mytheme' ); ?></option>
<option value="price_desc"><?php _e( 'Price: High to Low', 'mytheme' ); ?></option>
<option value="popularity"><?php _e( 'Popularity', 'mytheme' ); ?></option>
</select>
</div>
<div class="filter-group">
<label><?php _e( 'Price Range:', 'mytheme' ); ?></label>
<input type="number" name="min_price" placeholder="Min" min="0">
<input type="number" name="max_price" placeholder="Max" min="0">
</div>
<button type="submit"><?php _e( 'Filter Products', 'mytheme' ); ?></button>
<button type="reset" id="reset-filter"><?php _e( 'Reset', 'mytheme' ); ?></button>
</form>
<div id="products-container">
<!-- Products will be loaded here -->
</div>
<?php
// AJAX handler for filtering
add_action( 'wp_ajax_filter_products', 'mytheme_filter_products' );
add_action( 'wp_ajax_nopriv_filter_products', 'mytheme_filter_products' );
function mytheme_filter_products() {
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'paged' => isset( $_POST['page'] ) ? $_POST['page'] : 1,
);
// Category filter
if ( ! empty( $_POST['product_cat'] ) && $_POST['product_cat'] != '0' ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'product_category',
'field' => 'term_id',
'terms' => intval( $_POST['product_cat'] ),
),
);
}
// Price filter
if ( ! empty( $_POST['min_price'] ) || ! empty( $_POST['max_price'] ) ) {
$meta_query = array( 'relation' => 'AND' );
if ( ! empty( $_POST['min_price'] ) ) {
$meta_query[] = array(
'key' => '_product_price',
'value' => intval( $_POST['min_price'] ),
'compare' => '>=',
'type' => 'NUMERIC',
);
}
if ( ! empty( $_POST['max_price'] ) ) {
$meta_query[] = array(
'key' => '_product_price',
'value' => intval( $_POST['max_price'] ),
'compare' => '<=',
'type' => 'NUMERIC',
);
}
$args['meta_query'] = $meta_query;
}
// Sorting
if ( ! empty( $_POST['orderby'] ) ) {
switch ( $_POST['orderby'] ) {
case 'title':
$args['orderby'] = 'title';
$args['order'] = 'ASC';
break;
case 'price_asc':
$args['orderby'] = 'meta_value_num';
$args['meta_key'] = '_product_price';
$args['order'] = 'ASC';
break;
case 'price_desc':
$args['orderby'] = 'meta_value_num';
$args['meta_key'] = '_product_price';
$args['order'] = 'DESC';
break;
case 'popularity':
$args['orderby'] = 'meta_value_num';
$args['meta_key'] = '_product_views';
$args['order'] = 'DESC';
break;
default:
$args['orderby'] = 'date';
$args['order'] = 'DESC';
}
}
$query = new WP_Query( $args );
if ( $query->have_posts() ) :
while ( $query->have_posts() ) :
$query->the_post();
get_template_part( 'template-parts/content', 'product' );
endwhile;
// Pagination
echo paginate_links( array(
'total' => $query->max_num_pages,
'current' => $args['paged'],
) );
else :
echo '<p>' . __( 'No products found.', 'mytheme' ) . '</p>';
endif;
wp_reset_postdata();
wp_die();
}
Custom Query Pagination
Pagination for Custom Queries
<?php
// Get current page
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'paged' => $paged,
);
$products = new WP_Query( $args );
if ( $products->have_posts() ) :
// Display posts
while ( $products->have_posts() ) :
$products->the_post();
// Display content
endwhile;
// Custom pagination
$big = 999999999; // need an unlikely integer
echo '<div class="pagination">';
echo paginate_links( array(
'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
'format' => '?paged=%#%',
'current' => max( 1, $paged ),
'total' => $products->max_num_pages,
'prev_text' => __( '« Previous', 'mytheme' ),
'next_text' => __( 'Next »', 'mytheme' ),
'type' => 'list',
'end_size' => 3,
'mid_size' => 3,
) );
echo '</div>';
wp_reset_postdata();
endif;
// Alternative: Load More Button
?>
<div id="products-container">
<!-- Initial products -->
</div>
<button id="load-more" data-page="1" data-max="<?php echo $products->max_num_pages; ?>">
<?php _e( 'Load More Products', 'mytheme' ); ?>
</button>
<script>
jQuery('#load-more').on('click', function() {
var button = jQuery(this);
var page = button.data('page');
var maxPages = button.data('max');
jQuery.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'load_more_products',
page: page + 1
},
success: function(response) {
jQuery('#products-container').append(response);
button.data('page', page + 1);
if (page + 1 >= maxPages) {
button.hide();
}
}
});
});
</script>
Displaying Related Content
Related Products by Category
<?php
// Get related products
function mytheme_get_related_products( $post_id = null, $limit = 4 ) {
if ( ! $post_id ) {
$post_id = get_the_ID();
}
// Get product categories
$terms = wp_get_post_terms( $post_id, 'product_category', array( 'fields' => 'ids' ) );
if ( empty( $terms ) ) {
return false;
}
$args = array(
'post_type' => 'product',
'posts_per_page' => $limit,
'post__not_in' => array( $post_id ),
'tax_query' => array(
array(
'taxonomy' => 'product_category',
'field' => 'term_id',
'terms' => $terms,
),
),
'orderby' => 'rand',
);
return new WP_Query( $args );
}
// Display related products
$related = mytheme_get_related_products();
if ( $related && $related->have_posts() ) :
?>
<section class="related-products">
<h2><?php _e( 'Related Products', 'mytheme' ); ?></h2>
<div class="products-grid">
<?php
while ( $related->have_posts() ) :
$related->the_post();
get_template_part( 'template-parts/content', 'product-card' );
endwhile;
wp_reset_postdata();
?>
</div>
</section>
<?php endif; ?>
Related by Custom Field
<?php
// Get related items by custom field value
function mytheme_get_related_by_meta( $meta_key, $meta_value, $post_type = 'product', $limit = 6 ) {
$args = array(
'post_type' => $post_type,
'posts_per_page' => $limit,
'post__not_in' => array( get_the_ID() ),
'meta_query' => array(
array(
'key' => $meta_key,
'value' => $meta_value,
'compare' => '=',
),
),
);
return new WP_Query( $args );
}
// Example: Related products by brand
$brand = get_post_meta( get_the_ID(), '_product_brand', true );
if ( $brand ) {
$related_by_brand = mytheme_get_related_by_meta( '_product_brand', $brand );
}
Content Display Layouts
Grid Layout
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 2rem;
}
Item
Item
Item
List Layout
.products-list .product {
display: flex;
gap: 2rem;
padding: 1.5rem 0;
border-bottom: 1px solid #e5e7eb;
}
List Item Content
Masonry Layout
// Using Masonry.js
var grid = document.querySelector('.masonry-grid');
var masonry = new Masonry(grid, {
itemSelector: '.grid-item',
columnWidth: '.grid-sizer',
percentPosition: true
});
Item
Item
Item
Item
Creating Shortcodes for Custom Content
CPT Display Shortcode
<?php
/**
* Shortcode to display products
* Usage: [products category="electronics" count="6" layout="grid"]
*/
function mytheme_products_shortcode( $atts ) {
$atts = shortcode_atts( array(
'category' => '',
'count' => 6,
'layout' => 'grid',
'orderby' => 'date',
'order' => 'DESC',
'featured' => false,
), $atts, 'products' );
$args = array(
'post_type' => 'product',
'posts_per_page' => intval( $atts['count'] ),
'orderby' => $atts['orderby'],
'order' => $atts['order'],
);
// Add category filter
if ( ! empty( $atts['category'] ) ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => explode( ',', $atts['category'] ),
),
);
}
// Featured products only
if ( $atts['featured'] ) {
$args['meta_query'] = array(
array(
'key' => '_product_featured',
'value' => 'yes',
),
);
}
$products = new WP_Query( $args );
ob_start();
if ( $products->have_posts() ) :
?>
<div class="products-shortcode layout-<?php echo esc_attr( $atts['layout'] ); ?>">
<?php
while ( $products->have_posts() ) :
$products->the_post();
get_template_part( 'template-parts/content', 'product-' . $atts['layout'] );
endwhile;
?>
</div>
<?php
wp_reset_postdata();
else :
?>
<p><?php _e( 'No products found.', 'mytheme' ); ?></p>
<?php
endif;
return ob_get_clean();
}
add_shortcode( 'products', 'mytheme_products_shortcode' );
/**
* Taxonomy terms shortcode
* Usage: [product_categories show_count="true" hide_empty="false"]
*/
function mytheme_product_categories_shortcode( $atts ) {
$atts = shortcode_atts( array(
'show_count' => true,
'hide_empty' => false,
'parent' => 0,
'columns' => 3,
), $atts, 'product_categories' );
$terms = get_terms( array(
'taxonomy' => 'product_category',
'hide_empty' => $atts['hide_empty'],
'parent' => $atts['parent'],
) );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return '';
}
ob_start();
?>
<div class="category-grid columns-<?php echo esc_attr( $atts['columns'] ); ?>">
<?php foreach ( $terms as $term ) :
$term_image = get_term_meta( $term->term_id, 'term_image_id', true );
?>
<div class="category-item">
<a href="<?php echo esc_url( get_term_link( $term ) ); ?>">
<?php if ( $term_image ) : ?>
<?php echo wp_get_attachment_image( $term_image, 'medium' ); ?>
<?php endif; ?>
<h3><?php echo esc_html( $term->name ); ?></h3>
<?php if ( $atts['show_count'] ) : ?>
<span class="count">(<?php echo $term->count; ?>)</span>
<?php endif; ?>
</a>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
add_shortcode( 'product_categories', 'mytheme_product_categories_shortcode' );
Performance Optimization
Query Optimization Tips
Efficient Queries
<?php
// Use specific fields when you don't need all data
$args = array(
'post_type' => 'product',
'posts_per_page' => 100,
'fields' => 'ids', // Only get post IDs
);
// Cache expensive queries
$cache_key = 'featured_products_' . get_locale();
$featured = get_transient( $cache_key );
if ( false === $featured ) {
$featured = new WP_Query( array(
'post_type' => 'product',
'posts_per_page' => 6,
'meta_key' => '_product_featured',
'meta_value' => 'yes',
) );
set_transient( $cache_key, $featured, 12 * HOUR_IN_SECONDS );
}
// Avoid unnecessary queries
$args = array(
'post_type' => 'product',
'posts_per_page' => 10,
'no_found_rows' => true, // Skip pagination counting
'update_post_meta_cache' => false, // Skip meta if not needed
'update_post_term_cache' => false, // Skip terms if not needed
);
// Pre-fetch related data
if ( have_posts() ) :
// Collect all post IDs
$post_ids = array();
while ( have_posts() ) :
the_post();
$post_ids[] = get_the_ID();
endwhile;
// Pre-cache meta for all posts at once
update_meta_cache( 'post', $post_ids );
// Reset and display
rewind_posts();
while ( have_posts() ) :
the_post();
// Display content
endwhile;
endif;
Best Practices
Custom Content Display Best Practices
- Always reset post data: Use wp_reset_postdata() after custom queries
- Escape output: Use esc_html(), esc_url(), etc.
- Check for posts: Always use have_posts() before loops
- Use template parts: Create reusable content templates
- Implement pagination: Don't load all posts at once
- Cache complex queries: Use transients for expensive operations
- Optimize images: Use appropriate image sizes
- Consider AJAX: Load content dynamically when appropriate
- Mobile-first: Ensure responsive display
- Test performance: Monitor query times and optimize
Be careful with posts_per_page => -1 as it can cause performance issues with large datasets. Always paginate when possible.
Practice Exercise
Build Complete Content Display System