WordPress pre_get_posts Example: Modify the Main Query Safely
Use pre_get_posts safely to modify the main WordPress query with admin guards, main-query targeting, pagination testing, and query_posts avoidance.
Published
April 22, 2026
Reading Time
3 min read
Updated
April 22, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 22, 2026.
Full Report
Last reviewed: April 22, 2026
pre_get_posts is the right tool when you need to adjust the main WordPress query before it runs. It is also one of the easiest hooks to misuse. A loose condition can accidentally change admin screens, sidebar queries, search results, feeds, or custom loops that were supposed to stay untouched.
This guide shows how to use pre_get_posts safely for frontend archives, how to target only the main query, and why this hook is usually better than query_posts().
When to use it
Use pre_get_posts when the primary archive query should change before WordPress retrieves posts. Common examples include changing posts per page for a category, including a custom post type on the blog index, limiting search results to posts, or excluding a specific post type from archives.
Do not use it for small independent sections inside a template. For those, create a separate WP_Query or use get_posts().
Safe category archive example
<?php
add_action( 'pre_get_posts', 'vulnwp_adjust_security_category_archive' );
function vulnwp_adjust_security_category_archive( WP_Query $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_category( 'security' ) ) {
return;
}
$query->set( 'posts_per_page', 15 );
$query->set( 'ignore_sticky_posts', true );
}
The important guards are is_admin() and $query->is_main_query(). Without them, the callback may affect unintended queries.
Limit search to posts
add_action( 'pre_get_posts', 'vulnwp_limit_frontend_search_to_posts' );
function vulnwp_limit_frontend_search_to_posts( WP_Query $query ) {
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
return;
}
$query->set( 'post_type', array( 'post' ) );
}
Use the conditional methods on the passed query object where possible. Some global conditional functions do not behave as expected this early in the request lifecycle.
Avoid query_posts()
query_posts() replaces the main query after WordPress has already built it. That can create extra work and confusing pagination bugs. If the goal is to alter the main query, use pre_get_posts before the query runs.
Offset warning
Using offset with the main query can break pagination because WordPress still calculates page counts based on the original query shape. If you need offset-style behavior, test pagination carefully or build a custom query and navigation logic explicitly.
Testing workflow
- Visit the target archive and confirm the expected number of posts appears.
- Check page 2 and older pages to verify pagination still works.
- Run a search and confirm the callback did not change unrelated queries.
- Open wp-admin post lists and confirm admin queries are unchanged.
- Check feeds if the site exposes category or search feeds.
Most pre_get_posts bugs are scope bugs. The callback works on the intended URL but also changes something else. Testing should prove both sides.
Production checklist
- Always guard against admin requests unless the change is intentionally admin-only.
- Target the main query with
$query->is_main_query(). - Use query-object conditional methods where possible.
- Avoid broad rules that affect every archive.
- Test pagination, feeds, search, and category archives after changes.
- Do not use
query_posts()for main query changes.
Common mistakes
- No main query guard. This can change widgets, related posts, or custom loops.
- No admin guard. Admin list tables may unexpectedly change.
- Using global conditionals too early. Prefer methods on the passed query object.
- Changing single views into archives. The main query already has route-specific assumptions.
- Ignoring pagination after offsets. Offset logic needs special handling.
Related reading
For separate custom queries, read our WP_Query performance example. For custom post type archive behavior, pair this with the custom post type example.


