Exclude Posts From the WordPress Homepage With pre_get_posts

Quick Summary

  • Hook pre_get_posts and, when $query->is_home() and $query->is_main_query() are true, set post__not_in to the IDs of the posts to hide.
  • Use category__not_in or tag__not_in to hide posts by term, but exclude sticky posts by ID, because WordPress adds them back to page 1 with a separate query.
  • Don't use query_posts(): it runs the main query a second time and breaks pagination unless you pass paged yourself.

To hide a post from your WordPress blog homepage, hook a function to pre_get_posts and, when the main query of the posts page runs, set post__not_in to the IDs you want to leave out. The change happens before WordPress fetches the posts, so you don’t need a second query in your template, and pagination keeps working. Don’t use query_posts() for this.

How do you exclude a post from the homepage?

Add this code to your theme’s functions.php file, preferably in a child theme, and replace 99 with the ID of the post to hide. The ID appears in the address bar (post=99) when you edit the post.

PHP
function alpha_modify_main_query( $wpq ) {
    if ( $wpq->is_home() && $wpq->is_main_query() ) {
        $wpq->set( 'post__not_in', array( 99 ) );
    }
}
add_action( 'pre_get_posts', 'alpha_modify_main_query' );
  • pre_get_posts runs before every query and passes the query object, here $wpq, so $wpq->set() changes its arguments before the database is asked.
  • $wpq->is_home() is true on the blog posts index. It’s never true in the admin, so the Posts screen in the dashboard isn’t affected.
  • $wpq->is_main_query() is the important check. A custom loop on the blog page, such as new WP_Query( array( 'posts_per_page' => 3 ) ) in a sidebar, is flagged as home too, and only this check tells it apart from the main query. Without it, the post disappears from every list on that page.
  • post__not_in takes an array, so you can hide several posts at once: array( 99, 105 ).

Because the query itself changes, WordPress counts one post less when it works out the number of pages, so /page/2/ starts with the right post. Skipping the post inside the loop instead leaves its page one post short.

The conditionals are called on $wpq rather than as is_home(): the plain functions check the global query, not the one passed to the hook. For the main query they are the same object, so is_home() works too, but the method form is the safe habit in pre_get_posts.

How do you hide all posts in a category or with a tag?

Replace the $wpq->set() line in the function with one of these, and use the numeric ID of your tag or category:

PHP
$wpq->set( 'tag__not_in', array( 12 ) );     // posts with tag ID 12
$wpq->set( 'category__not_in', array( 7 ) ); // posts in category ID 7

You’ll find a tag’s ID under Posts > Tags: open the tag and look for tag_ID= in the address bar. Categories work the same way under Posts > Categories. Both parameters take arrays, so one line can exclude several terms.

Hiding by term is easier to maintain than a list of post IDs: to hide a post later, give it the tag instead of editing code.

Why does a sticky post still show on the homepage?

On page 1 of the blog, WordPress moves sticky posts to the top. Sticky posts that the main query didn’t return are fetched with a separate query, and that query ignores category__not_in and tag__not_in. A sticky post in an excluded category therefore still appears at the top of page 1, while later pages leave it out as expected.

  • Exclude the post by ID with post__not_in. WordPress removes those IDs from the sticky list before it fetches sticky posts.
  • Or unstick the post in the editor.
  • Or add $wpq->set( 'ignore_sticky_posts', true ); to the function. That stops every sticky post from moving to the top, so only use it if you don’t need the feature.

Why not use query_posts()?

query_posts() in a template throws away the main query WordPress has already run and runs a new one, so the posts are fetched twice. The function reference says it “isn’t intended for use by plugins or themes”: it can more than double the work of loading a page, it changes the globals and conditional tags that the rest of the template relies on, and pagination breaks unless you pass the paged value yourself. Use pre_get_posts to change the main query, and new WP_Query() for a separate list of posts.

Common problems

  • The post still shows on your homepage. is_home() is true on the blog posts index. If Settings > Reading sets a static page as your homepage, the snippet affects the Posts page instead, and a post list on the static homepage comes from its own query: add 'post__not_in' to that query’s arguments in the template.
  • A block theme’s homepage ignores the snippet. A Query Loop block with “Inherit query from template” turned on shows the main query, so the snippet works. With that option off, the block runs its own query; change it with the query_loop_block_query_vars filter (WordPress 6.1 and later), as shown below.
  • Another plugin’s exclusions stop working. set() replaces the whole value, so two functions that both set post__not_in overwrite each other. Merge with what’s already there: $wpq->set( 'post__not_in', array_merge( (array) $wpq->get( 'post__not_in' ), array( 99 ) ) );
  • post__not_in has no effect at all. If something also sets post__in on the same query, WordPress uses post__in and ignores post__not_in.

For a Query Loop block on a static front page that doesn’t inherit the query:

PHP
add_filter( 'query_loop_block_query_vars', function ( $query ) {
    if ( is_front_page() ) {
        $query['post__not_in'] = array( 99 );
    }

    return $query;
} );

The filter runs for every Query Loop block that builds its own query, so the is_front_page() check limits it to the homepage.

Frequently asked questions

How do I hide the post from search results and the RSS feed too?

The main feed and search results aren’t is_home(), so the post still appears there. Extend the condition, and add is_admin() so searches in the admin Posts list aren’t filtered:

PHP
function alpha_modify_main_query( $wpq ) {
    if ( is_admin() || ! $wpq->is_main_query() ) {
        return;
    }

    if ( $wpq->is_home() || $wpq->is_search() || $wpq->is_feed() ) {
        $wpq->set( 'post__not_in', array( 99 ) );
    }
}
add_action( 'pre_get_posts', 'alpha_modify_main_query' );

Does this hide the post from the whole site?

No. The post keeps its own URL, and category, tag, author and date archives still list it unless you add those conditions too, for example $wpq->is_category().

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More