WordPress Archive Query — pre_get_posts Control PHP
A focused pre_get_posts helper for changing archive page sizes per post type without touching admin screens, secondary queries, or unrelated blog archives.
This WordPress archive query PHP snippet uses pre_get_posts to control posts per page for specific archives while avoiding admin and secondary query side effects.
Useful when your theme has multiple post type archives that need different grid counts — a Projects archive showing 12 cards per page, a Snippets archive showing 15 rows, and a Blog showing 10 posts. Without this WordPress archive query helper, you would need to set a single global posts_per_page in Settings → Reading and either pad or truncate every archive to match. The helper keeps all those decisions in one predictable PHP file, readable by anyone who maintains the theme, removable cleanly with <code>remove_action()</code> if a child theme needs to override it.
WordPress archive query — The Code
This WordPress archive query snippet uses pre_get_posts to set different posts_per_page values per archive type.
The absolute minimum is a pre_get_posts callback with two guard clauses. The is_admin() check prevents the WordPress archive query hook from running in wp-admin — without it, your custom posts_per_page value would alter admin list tables and confuse anyone managing content in the dashboard. The is_main_query() check keeps the hook scoped to the primary page query only, leaving secondary loops inside widgets, related post blocks, and sidebar templates completely untouched. Both conditions must be in place before any posts_per_page modification is safe to apply.
// WordPress archive query — PHP snippet by Mosharaf Hossain
<?php
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_post_type_archive( 'project' ) ) {
$query->set( 'posts_per_page', 12 );
}
} );
Now add a second post type. The logic stays in one callback — registering separate pre_get_posts hooks for each post type creates redundancy and scatters the WordPress archive query configuration across functions.php. Each post type archive gets its own posts_per_page value through a chained if/elseif block inside the single callback. This keeps all archive query settings in one predictable location: a developer reviewing query behavior knows exactly where to look, and changing the project grid count from 12 to 8 is a one-line edit rather than a file search.
// WordPress archive query
<?php
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_post_type_archive( 'project' ) ) {
$query->set( 'posts_per_page', 12 );
} elseif ( $query->is_post_type_archive( 'snippet' ) ) {
$query->set( 'posts_per_page', 9 );
}
} );
Now add the normal blog views. is_home() targets the main posts index — the URL WordPress serves when there is no static front page set in Settings → Reading. Category and tag archives typically display the same content type and can share a count. Date archives and author archives can be added to the same conditional chain if the design requires custom page sizes. Keep the default consistent with the global Posts Per Page setting so archives without an explicit condition behave predictably on the front end.
// WordPress archive query
<?php
add_action( 'pre_get_posts', function( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_home() || $query->is_category() || $query->is_tag() ) {
$query->set( 'posts_per_page', 8 );
}
} );
The production version uses a named function instead of an anonymous callback. A named function can be removed with remove_action() if a child theme or plugin author needs to override the WordPress archive query behavior cleanly. Place this in inc/archive-query.php and require it from functions.php immediately after your post type registration files. Keeping query logic in a dedicated include makes auditing straightforward: all archive behavior lives in one file, display logic stays in templates, and functions.php remains easy to read.
// WordPress archive query
<?php
/**
* Control public archive page sizes from one place.
*/
function theme_archive_query_sizes( WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_post_type_archive( 'project' ) ) {
$query->set( 'posts_per_page', 12 );
} elseif ( $query->is_post_type_archive( 'snippet' ) ) {
$query->set( 'posts_per_page', 9 );
} elseif ( $query->is_home() || $query->is_category() || $query->is_tag() ) {
$query->set( 'posts_per_page', 8 );
}
}
add_action( 'pre_get_posts', 'theme_archive_query_sizes' );
Place this after your custom post types are registered. The hook runs later during query setup, so the archive checks will work as long as the post types exist on init.
WordPress archive query — Guard Clauses Explained
Here is how this WordPress archive query works step by step: The helper is small, but the conditions are what make it safe.
pre_get_posts runs before the query hits the database
This hook lets you modify the main WP_Query object before WordPress fetches posts from the database. That means you can change posts_per_page, ordering, taxonomy filters, and meta queries without replacing the archive template or using query_posts(), which resets the main query and causes a double query on every page load. The WordPress documentation explicitly recommends pre_get_posts over query_posts() for this reason — it is the correct hook for controlling WordPress archive query behavior at the framework level.
is_admin() protects dashboard screens
Without this check, the WordPress archive query page-size modifier also runs on admin screens. A client opening the Posts or Projects list in wp-admin would see only 12 items per page instead of the default 20 — because the hook cannot distinguish a dashboard request from a front-end one without is_admin(). The modified count would affect bulk-edit screens and other admin list views that rely on stable pagination, making content management confusing for anyone who manages the site regularly.
is_main_query() protects custom loops
Archive templates often run secondary queries — related posts widgets, recent posts blocks, featured content cards, and custom loops for sidebars all create their own WP_Query instances on the same page. Without is_main_query(), the hook fires on every query, not just the archive pagination query, and those secondary loops would also return only 12 items. The is_main_query() check resolves this by restricting the modification to the primary query only.
Archive conditionals keep decisions explicit
is_post_type_archive(), is_home(), is_category(), and is_tag() document exactly which pages the WordPress archive query modification affects. Future developers can change a count without hunting through template files — the single callback function is the only place that controls archive pagination across the entire site. This explicitness also makes debugging easier: if the projects archive shows the wrong count, there is exactly one place to check.
Add WordPress archive query to Your Theme
To integrate this WordPress archive query into your project: The helper can live directly in functions.php, but a small include is cleaner on custom themes.
Create the include file
Create inc/archive-query.php and paste the production version from section 01 into it. Keeping WordPress archive query behavior in inc/ separates it from theme setup code and makes it straightforward to find when a grid count needs changing. Most custom themes organise inc/ by concern: CPT registration, custom taxonomies, archive queries, and utility helpers — placing the archive helper there follows the same convention and reduces onboarding time for new developers joining the project.
Require it from functions.php
Add require_once get_template_directory() . '/inc/archive-query.php'; to functions.php after your post type registration files. The order is generally not critical because the pre_get_posts hook fires at query time rather than at registration time, but placing query code below CPT setup is a readable convention — anyone reading functions.php top to bottom will see post types defined before the WordPress archive query behavior that depends on them.
Test each archive
Visit the front end of each configured archive and confirm the item count matches the value you set. Then check the same archive while logged into wp-admin — the admin list view should still show the default reading setting, not the custom front-end count, confirming that is_admin() is working. Finally, check a secondary loop on the archive page such as a related posts widget to confirm that is_main_query() has kept those loops untouched and the WordPress archive query modification is scoped correctly.
Do not use this hook to hide private content or enforce permissions. It is for shaping public archive queries. Access control belongs in capabilities, template checks, or dedicated permission logic.
Common WordPress archive query Customizations
Customise this WordPress archive query to match your specific needs: Once the page-size helper is stable, you can add ordering rules and archive-specific logic.
Change ordering per archive
Projects often sort by menu order or date, while blog posts sort by date. Add orderby and order inside the relevant archive condition block only — placing the ordering logic inside is_post_type_archive() means it applies to that archive exclusively without affecting any other WordPress archive query on the site. Use menu_order for projects you want to arrange manually in the admin, date for time-sensitive content, and title for alphabetically browsable archives like team members or documentation pages.
Use grid-friendly numbers
Choose counts that fit the archive design: 8 for two- or four-column grids, 9 for three-column grids, and 12 for flexible grids that reflow across breakpoints. The WordPress archive query should support the layout rather than fight it — an 11-item archive on a four-column layout produces an orphaned final row. Agree on the count with the designer before building the grid template, and document the number in a comment so future developers do not change it without understanding the grid relationship.
Avoid meta queries unless needed
Changing posts_per_page via pre_get_posts is cheap — WordPress uses it in the SQL LIMIT clause and adds no overhead. Adding meta queries is different: meta_query joins wp_postmeta and can be expensive on large sites without indexed columns. If the archive needs to filter by featured status or priority, use a taxonomy term instead of a custom field. Taxonomy queries use indexed tables and scale cleanly regardless of content volume.
This pattern matches the kind of archive control used in custom themes with Projects, Snippets, and Insights. It keeps archive behavior centralized while leaving templates focused on markup.

No comments yet. Be the first.