How to exclude content types from the feed with hooks in PHP in WordPress

Contents

Overview

This article shows complete, production-ready techniques to exclude content types from WordPress feeds using hooks in PHP. It covers the why, the how, multiple real-world examples (single CPT, multiple CPTs, taxonomies, category exclusions, SQL-level filtering), edge cases, testing, performance considerations, and troubleshooting. All code examples are ready to drop into a themes functions.php or a small plugin. Use the approach that fits your site architecture.

Key concepts

  • Feeds in WordPress: WordPress serves feeds for posts, archives, categories, authors, and custom queries. The conditional tag is_feed() returns true for any feed type (RSS, RSS2, RDF, Atom).
  • Custom post types (CPTs): When you register a CPT with public => true, WordPress often includes those posts in the feed unless you explicitly filter them out.
  • Recommended hook: pre_get_posts is the most reliable place to alter the main query before it runs, including feeds. Use it to adjust post_type, tax_query, post__not_in, or other query vars.
  • Alternative hooks: request (runs earlier and modifies query vars array) and posts_where/posts_join (modify SQL directly). Use SQL filters only when necessary.
  • Always check: Use query->is_main_query() to avoid affecting admin screens or secondary queries, and use is_admin() to skip the admin area.

General best practices

  • Always check query->is_main_query() and is_feed() and ! is_admin() before changing query vars.
  • Prefer to set post_type to an array of allowed types rather than trying to exclude with SQL if you can build the allowed list easily.
  • When removing multiple CPTs, use get_post_types() to build the allowed list and then remove the unwanted types to avoid accidentally sending an empty query.
  • Test multiple feed endpoints (site feed, category feed, author feed, etc.) because conditionals can behave differently in archive/term contexts.
  • Remember cache layers (object cache, CDN, feed cache plugins). Clear caches after updating code.

Minimal, safe example: exclude a single CPT from all feeds

This example ensures only standard posts are included in feeds (excludes a custom post type named product). It is concise and safe.

is_main_query() ) {
        return
    }

    if ( query->is_feed() ) {
        // Allow only posts in feeds (change or extend as needed)
        query->set( post_type, array( post ) )
    }
}
add_action( pre_get_posts, exclude_products_from_feed )

Notes

  • This keeps the feed behavior clear: only posts appear. If you want to keep other built-in types, add them to the array.
  • Do not forget to open with PHP tags when placing in functions.php or plugin main file (as shown).

Flexible: exclude multiple CPTs dynamically

Build an allowed list from all public post types, then remove the ones you want to exclude. This approach avoids accidentally omitting post types that should be in the feed and is maintainable as you add new CPTs.

is_main_query() ) {
        return
    }

    if ( query->is_feed() ) {
        // Get public post types (including post and any public CPTs)
        post_types = get_post_types( array( public => true ), names )

        // Post types to exclude from feeds
        exclude = array( product, book, movie )

        // Remove excluded types safely
        post_types = array_diff( post_types, exclude )

        // Ensure at least post remains to avoid creating an empty query
        if ( empty( post_types ) ) {
            post_types = array( post )
        }

        query->set( post_type, post_types )
    }
}
add_action( pre_get_posts, exclude_cpt_from_feed )

Why this is safer

  • New public CPTs wont accidentally be included/excluded without explicit action.
  • Using array_diff makes the exclusion list obvious and easy to update.

Exclude by taxonomy or category (exclude posts in specific category/taxonomy from feeds)

Sometimes you want to keep a post type in the feed but remove posts belonging to certain categories or taxonomy terms (for example: exclude internal or press posts).

is_main_query() ) {
        return
    }

    if ( query->is_feed() ) {
        // Exclude posts in these category slugs
        tax_query = array(
            array(
                taxonomy => category,
                field    => slug,
                terms    => array( internal, press ),
                operator => NOT IN,
            ),
        )

        // Merge with any existing tax_query
        existing = query->get( tax_query )
        if ( ! empty( existing ) ) {
            tax_query = array_merge( (array) existing, tax_query )
        }

        query->set( tax_query, tax_query )
    }
}
add_action( pre_get_posts, exclude_tax_terms_from_feed )

SQL-level exclusion: posts_where to exclude CPTs via SQL

If you need to alter the raw SQL because a higher-level query var solution wont work (rare), you can use posts_where to add a NOT IN condition on post_type. Use caution: this affects any query where you hook it, so scope it tightly.

is_main_query() ) {
        return where
    }

    if ( query->is_feed() ) {
        global wpdb
        // Exclude these post types from feed results
        exclude_types = array( product, book )
        exclude_sql = implode( ,, array_map( function ( t ) use ( wpdb ) {
            return wpdb->prepare( %s, t )
        }, exclude_types ) )

        // Safely add NOT IN clause
        where .=  AND {wpdb->posts}.post_type NOT IN (  . implode( ,, array_map( esc_sql, exclude_types ) ) .  )
    }

    return where
}
add_filter( posts_where, filter_posts_where_exclude_cpt, 10, 2 )

IMPORTANT

  • SQL filters are powerful and can cause unintended side effects. Scope them correctly and test thoroughly.
  • Prefer modifying query vars use SQL only if the query builder approach cannot achieve the desired result.

Alternative hook: modifying the request array early

You can alter the initial request array with the request filter. This is useful when you want low-level control over query vars before WP_Query is constructed.

 true ), names )
        exclude = array( product )

        post_types = array_diff( post_types, exclude )

        // Set the post_type array for the query
        vars[post_type] = array_values( post_types )
    }
    return vars
}
add_filter( request, filter_feed_request_exclude_cpt )

Selectively exclude only certain feeds or contexts

You might want to exclude CPTs only from the main site feed, but keep them in category or author feeds. Use additional conditional checks:

is_main_query() ) {
        return
    }

    // Only the main home feed (site-level feed) — not category, tag, or author feeds
    if ( query->is_feed()  is_home() ) {
        query->set( post_type, array( post ) )
    }
}
add_action( pre_get_posts, selective_exclude_from_feed )

Edge cases and troubleshooting

  1. Secondary queries: If you modify queries without checking is_main_query(), widgets or plugins that request feeds may unexpectedly be filtered. Always scope to the main query where appropriate.
  2. Admin screens: Admin list tables, feeds generated for preview endpoints, or AJAX requests might trigger the same hooks. Use is_admin() and other guards to prevent accidental changes.
  3. Feed caching: If a feed is cached by a caching plugin, CDN, or upstream service, you may not see changes instantly. Purge caches after deployment.
  4. Multiple feed types: Use is_feed(rss2) if you need to target a specific feed flavor (rss2, atom, rdf). is_feed() without args covers all feed types.
  5. Author/category feeds: If you want to exclude content only in certain feed types (for example, exclude in site feed but not in category feeds) combine conditionals: query->is_feed() is_home() or query->is_feed() ! is_category().
  6. Empty feed results: Avoid setting post_type to an empty array. Always ensure at least one post type remains or WordPress will generate an empty feed (and some feed validators will error).

Testing checklist

  • Visit the site feed: example.com/feed/ (and example.com/?feed=rss2)
  • Visit taxonomy and author feeds to verify your conditionals: example.com/category/news/feed/, example.com/author/joe/feed/
  • Check feeds for the presence/absence of the excluded CPT content and categories.
  • Use an RSS validator (if necessary) to verify the feed remains valid XML after changes.
  • Clear caches and test on different feed clients (feed reader, curl, browser) to ensure consistent behavior.

Plugin-ready wrapper example

A compact, safe plugin template that excludes specified CPTs from all feeds. Drop into a plugin file or use in functions.php.

is_main_query() ) {
            return
        }

        if ( query->is_feed() ) {
            // Config: edit this array to list CPTs you want to exclude
            exclude = array( product, event )

            // Build allowed list
            post_types = get_post_types( array( public => true ), names )
            post_types = array_diff( post_types, exclude )

            if ( empty( post_types ) ) {
                post_types = array( post )
            }

            query->set( post_type, array_values( post_types ) )
        }
    }

    feed_cpt_excluder_init()
}

Summary of recommended approach

  • Use pre_get_posts for most use-cases check is_feed(), is_main_query(), and ! is_admin().
  • Prefer building an allowed list (get_post_types() array_diff) over trying to craft complex SQL unless necessary.
  • Test all feed endpoints, clear caches, and avoid empty queries.
  • Only use posts_where/posts_join when high-level query vars cannot produce the result you need.

Quick reference code snippets

Action hook pre_get_posts — modify query before WP_Query runs.
Conditional query->is_main_query(), is_feed(), ! is_admin()
Typical modification query->set(post_type, array( post )) or set tax_query / post__not_in

Final notes

These methods give you complete control over what content appears in your WordPress feeds. Choose the approach that best fits your needs (simple exclusion, dynamic lists, or taxonomy-based exclusion), test thoroughly, and be mindful of caching and edge cases such as archive feeds and admin queries.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *