Skip to main content

Course Progress

Loading...

🎯 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
The WP_Query class is the foundation for retrieving and displaying custom content. Understanding its parameters and methods enables you to create any content display pattern.

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

Create a comprehensive content display system:

  1. Create product archive with filtering
  2. Implement AJAX loading for filters
  3. Add pagination with load more button
  4. Display related products section
  5. Create grid and list view toggle
  6. Build category filter dropdown
  7. Add price range filter
  8. Create featured products section
  9. Implement search within CPT
  10. Add sorting options
  11. Create shortcodes for content
  12. Optimize queries with caching

Additional Resources