Extracting post lists from categories in WordPress, developer guide
EN

Extracting post lists from categories in WordPress, developer guide

Last verified: July 1, 2026
10 min read
Tutorial
500+ WP projects
Full-stack developer

#Introduction to Category-Based Post Retrieval

Learn more about professional WordPress development at WPPoland. One of the most common tasks in WordPress development is retrieving posts from specific categories. Whether you’re building a custom homepage layout, creating a category archive template, or displaying related content, understanding how to efficiently query posts by category is essential for any WordPress developer.

This comprehensive guide covers multiple approaches to extracting post lists from categories, from simple implementations to advanced optimization techniques. By the end, you’ll have a complete toolkit for handling category-based queries in any WordPress project.

#Understanding WordPress Categories

Before diving into code, it’s important to understand how WordPress handles categories:

  • Categories are a built-in taxonomy in WordPress
  • Each post can belong to multiple categories
  • Categories can be hierarchical (parent/child relationships)
  • Category data is stored in the wp_terms and wp_term_taxonomy tables
  • Post-category relationships are stored in wp_term_relationships

Understanding this structure helps you write more efficient queries and troubleshoot issues when they arise.

#Finding a category ID from a slug or name

Most query arguments accept either a slug or a numeric ID, but ID-based arguments (cat, category__in) are the fastest because they skip a term lookup. When you only know the slug, resolve it once and reuse the ID:

// From a slug
$term = get_category_by_slug('news');
$news_id = $term ? $term->term_id : 0;

// From a display name
$news_id = get_cat_ID('News'); // returns 0 if not found

// From any taxonomy (categories included)
$term = get_term_by('slug', 'news', 'category');

Cache the resolved ID in a constant or option if you hard-code it in a template; calling get_cat_ID() on every request adds an avoidable query.

#Method 1: WP_Query (The Flexible Approach)

WP_Query is WordPress’s primary class for querying posts. It offers maximum flexibility and is the recommended approach for most use cases.

#Basic Category Query

$args = array(
    'category_name' => 'news',
    'posts_per_page' => 10,
    'orderby' => 'date',
    'order' => 'DESC'
);

$query = new WP_Query($args);

if ($query->have_posts()) {
    while ($query->have_posts()) {
        $query->the_post();
        // Display post content
        ?>
        <article>
            <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
            <div class="entry-content">
                <?php the_excerpt(); ?>
            </div>
        </article>
        <?php
    }
    wp_reset_postdata();
}

#Query by Category ID

$args = array(
    'cat' => 5, // Category ID
    'posts_per_page' => 5
);

$query = new WP_Query($args);

#Multiple Categories

// Posts in ANY of these categories (OR relationship)
$args = array(
    'category__in' => array(5, 10, 15),
    'posts_per_page' => 10
);

// Posts in ALL of these categories (AND relationship)
$args = array(
    'category__and' => array(5, 10),
    'posts_per_page' => 10
);

// Exclude specific categories
$args = array(
    'category__not_in' => array(3, 7),
    'posts_per_page' => 10
);

#Including Child Categories

// Get posts from category and all its children
$parent_category_id = 5;

$args = array(
    'cat' => $parent_category_id,
    'posts_per_page' => 20
);

// WP_Query automatically includes child categories when using 'cat'

#Method 2: get_posts() (The Simple Approach)

For simpler use cases, get_posts() provides a more straightforward API.

#Basic Usage

$posts = get_posts(array(
    'category' => 5,
    'posts_per_page' => 10,
    'orderby' => 'date',
    'order' => 'DESC'
));

foreach ($posts as $post) {
    setup_postdata($post);
    ?>
    <article>
        <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
    </article>
    <?php
}
wp_reset_postdata();

#With Category Name

$posts = get_posts(array(
    'category_name' => 'technology',
    'numberposts' => 5
));

#Method 3: Shortcodes for Content Editors

Creating a shortcode allows content editors to insert category post lists anywhere.

function category_posts_shortcode($atts) {
    $atts = shortcode_atts(array(
        'category' => '',
        'posts' => 5,
        'orderby' => 'date',
        'order' => 'DESC'
    ), $atts);
    
    $args = array(
        'category_name' => $atts['category'],
        'posts_per_page' => intval($atts['posts']),
        'orderby' => $atts['orderby'],
        'order' => $atts['order']
    );
    
    $query = new WP_Query($args);
    
    ob_start();
    
    if ($query->have_posts()) {
        echo '<div class="category-posts-list">';
        while ($query->have_posts()) {
            $query->the_post();
            ?>
            <article class="category-post">
                <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
                <p><?php the_excerpt(); ?></p>
            </article>
            <?php
        }
        echo '</div>';
    } else {
        echo '<p>No posts found in this category.</p>';
    }
    
    wp_reset_postdata();
    
    return ob_get_clean();
}
add_shortcode('category_posts', 'category_posts_shortcode');

Usage: [category_posts category="news" posts="5"]

#Method 4: Modifying the Main Query

When you want to change which posts appear on category archive pages, use the pre_get_posts action.

function modify_category_queries($query) {
    // Only modify category archives on the main query
    if ($query->is_category() && $query->is_main_query() && !is_admin()) {
        // Show 20 posts per page instead of default
        $query->set('posts_per_page', 20);
        
        // Exclude posts from specific category on certain category pages
        $current_cat = get_queried_object();
        if ($current_cat->slug === 'featured') {
            $query->set('category__not_in', array(10)); // Exclude category ID 10
        }
    }
}
add_action('pre_get_posts', 'modify_category_queries');

#Method 5: tax_query for precise, taxonomy-aware filtering

The cat and category_name shortcuts are convenient, but tax_query is the argument you reach for once requirements get specific: combining categories with tags, excluding child terms, or querying a custom taxonomy. It also expresses AND/OR logic explicitly, which the shortcut arguments cannot.

$args = array(
    'posts_per_page' => 10,
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array('news'),
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array('featured'),
        ),
    ),
);

$query = new WP_Query($args);

The include_children key is the practical detail most people miss. By default a category tax_query pulls in every descendant term, which is usually what an archive wants but rarely what a curated homepage block wants:

'tax_query' => array(
    array(
        'taxonomy'         => 'category',
        'field'            => 'term_id',
        'terms'            => array(5),
        'include_children' => false, // only posts filed directly under term 5
    ),
),

Because tax_query works against any taxonomy, the same pattern retrieves posts from a custom product_cat, portfolio_type, or any taxonomy your theme registers, without a separate function per taxonomy.

#Performance Optimization

#1. Use Transients for Expensive Queries

function get_cached_category_posts($category_id, $count = 5) {
    $cache_key = 'cat_posts_' . $category_id . '_' . $count;
    $posts = get_transient($cache_key);
    
    if (false === $posts) {
        $args = array(
            'cat' => $category_id,
            'posts_per_page' => $count
        );
        
        $query = new WP_Query($args);
        $posts = $query->posts;
        
        // Cache for 1 hour
        set_transient($cache_key, $posts, HOUR_IN_SECONDS);
    }
    
    return $posts;
}

#2. Optimize Database Queries

// Only retrieve fields you need
$args = array(
    'category_name' => 'news',
    'posts_per_page' => 10,
    'fields' => 'ids' // Only get post IDs for better performance
);

$query = new WP_Query($args);

#3. Use Object Caching

If your site uses an object cache (Redis, Memcached), WP_Query results are automatically cached, improving performance for repeated queries.

#Advanced Techniques

#Custom Templates for Category Archives

Create a template file category-news.php for specific category styling:

<?php
/* Template Name: Category - News */
get_header(); ?>

<div class="category-archive">
    <h1><?php single_cat_title(); ?></h1>
    
    <?php if (have_posts()) : ?>
        <div class="posts-grid">
            <?php while (have_posts()) : the_post(); ?>
                <?php get_template_part('content', 'category'); ?>
            <?php endwhile; ?>
        </div>
        
        <?php the_posts_pagination(); ?>
    <?php else : ?>
        <p>No posts found in this category.</p>
    <?php endif; ?>
</div>

<?php get_footer(); ?>

#AJAX Loading for Category Posts

For better user experience, implement AJAX loading. Two details trip people up: ajaxurl is only defined in wp-admin, so on the front end you must pass it in yourself, and every request needs a nonce to survive a security review.

Enqueue the script and hand it the admin-ajax URL plus a nonce:

function enqueue_category_loader() {
    wp_enqueue_script(
        'category-loader',
        get_theme_file_uri('/js/category-loader.js'),
        array('jquery'),
        '1.0',
        true
    );
    wp_localize_script('category-loader', 'catLoader', array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('load_category_posts'),
    ));
}
add_action('wp_enqueue_scripts', 'enqueue_category_loader');

The front-end request then reads from the localized object:

jQuery(document).ready(function($) {
    $('.load-more').on('click', function() {
        var button = $(this);

        $.ajax({
            url: catLoader.ajaxurl,
            type: 'POST',
            data: {
                action: 'load_category_posts',
                nonce: catLoader.nonce,
                category: button.data('category'),
                page: button.data('page')
            },
            success: function(response) {
                $('.posts-container').append(response);
                button.data('page', button.data('page') + 1);
            }
        });
    });
});

Finally, register the handler for both logged-in and anonymous visitors (wp_ajax_ and wp_ajax_nopriv_), verify the nonce, and return the rendered markup:

function load_category_posts_handler() {
    check_ajax_referer('load_category_posts', 'nonce');

    $category = sanitize_text_field($_POST['category'] ?? '');
    $page     = max(1, intval($_POST['page'] ?? 1));

    $query = new WP_Query(array(
        'category_name'  => $category,
        'posts_per_page' => 5,
        'paged'          => $page,
    ));

    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            printf(
                '<article><h3><a href="%s">%s</a></h3></article>',
                esc_url(get_permalink()),
                esc_html(get_the_title())
            );
        }
        wp_reset_postdata();
    }

    wp_die(); // required so admin-ajax stops cleanly
}
add_action('wp_ajax_load_category_posts', 'load_category_posts_handler');
add_action('wp_ajax_nopriv_load_category_posts', 'load_category_posts_handler');

#Displaying category posts in a block theme

On block themes (the default since WordPress 6.1) you often do not need PHP at all. The Query Loop block filtered by category renders the same list from the editor: insert a Query Loop, open the block settings, and under Filters add the category you want. Save it as a synced pattern and editors can drop the curated list into any page without touching a template.

Reach for the code approaches above when you need logic the block cannot express: transient caching, conditional exclusions per archive, or output consumed by something other than the theme (an email digest, a REST response).

#Fetching category posts through the REST API

For headless front ends, a React island, or an external integration, WordPress exposes category posts over the REST API with no custom endpoint required:

GET /wp-json/wp/v2/posts?categories=5&per_page=10&_fields=id,title,link,excerpt

Use categories_exclude to filter out a term, _embed to pull featured images and term names in one round trip, and _fields to trim the payload to what the client actually renders. To resolve a slug to the ID the endpoint expects, query /wp-json/wp/v2/categories?slug=news first.

#Debugging a category query that returns the wrong posts

When a query returns too many, too few, or unexpected posts, inspect the SQL it actually ran rather than guessing at the arguments. Install Query Monitor and it shows every query on the page, including the one your loop generated, with the slow ones flagged. For a quick check without a plugin, dump the parsed query:

$query = new WP_Query($args);
// The exact SQL WordPress built from your $args
error_log($query->request);

The two culprits behind most surprises are child-term inclusion (see include_children above) and a stray pre_get_posts filter from a plugin or the theme altering the main query. If the SQL looks right but the result is empty, confirm the term actually has published posts assigned in the current language or context.

#Common Pitfalls to Avoid

  1. Not resetting post data: Always call wp_reset_postdata() after custom loops
  2. Querying on every page load: Use caching for expensive queries
  3. Not checking if posts exist: Always verify have_posts() before looping
  4. Modifying the main query incorrectly: Use pre_get_posts instead of creating new queries on archive pages
  5. Ignoring pagination: Remember to handle pagination for large category archives

#Conclusion

WordPress provides multiple ways to extract posts from categories, each suited to different scenarios:

  • WP_Query: Best for complex, custom displays
  • get_posts(): Ideal for simple post lists
  • Shortcodes: Perfect for content editor flexibility
  • pre_get_posts: Essential for modifying archive pages

Understanding these methods and when to use each one will make you a more effective WordPress developer. Remember to always consider performance, especially on sites with large amounts of content.

For production sites, implement caching strategies and test your queries with tools like Query Monitor to ensure optimal performance.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Do I need to be an experienced developer to use this guide?#
This guide is designed for intermediate WordPress developers. Basic PHP and WordPress knowledge is assumed, but all code is explained step-by-step.
Will these techniques work with any WordPress theme?#
Most techniques are theme-agnostic and work with standard WordPress themes. However, some heavily customized themes may require additional adaptation.
Is it safe to implement these changes on a live site?#
Always test changes on a staging environment first. While these techniques are production-ready, testing prevents potential issues on live sites.
Do these methods work with page builders like Elementor?#
Yes, most methods work alongside page builders. Some may require specific integration steps which are noted in the implementation section.
Do I still need code on a block theme?#
Not for a basic list. The Query Loop block filtered by category renders category posts from the editor. Use the code approaches when you need caching, conditional exclusions, or output consumed outside the theme, such as a REST response.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles