TechEarl

Remove WordPress Feed Links (and the /feed/ URLs)

How to remove WordPress feed links from the head with remove_action, plus the deeper step most guides skip: killing the /feed/ rewrite rules and redirecting the URLs so they stop getting crawled.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
How to remove WordPress feed links: a remove_action snippet that strips feed_links and feed_links_extra from the head, plus removing the /feed/ rewrite rules and 301-redirecting the URLs.

To remove the WordPress feed links from your page head, drop this into functions.php (or a small site-specific plugin). It stops both the main feed <link> tags and the per-category, per-tag, per-author, and per-comment ones:

php
add_action( 'init', function () {
    remove_action( 'wp_head', 'feed_links', 2 );
    remove_action( 'wp_head', 'feed_links_extra', 3 );
} );

That clears the tags from the <head>. But it does not stop the feeds existing: every /feed/ URL on the site still resolves and still serves RSS. If your goal is to stop search engines crawling and indexing those auto-generated feed URLs, you need the second half of this article too. First, what these tags are.

What WordPress prints for feeds

WordPress wires two callbacks into the head in wp-includes/default-filters.php:

php
add_action( 'wp_head', 'feed_links', 2 );
add_action( 'wp_head', 'feed_links_extra', 3 );

feed_links prints the two site-wide ones: the main posts feed and the global comments feed.

html
<link rel="alternate" type="application/rss+xml" title="Example &raquo; Feed" href="https://example.com/feed/" />
<link rel="alternate" type="application/rss+xml" title="Example &raquo; Comments Feed" href="https://example.com/comments/feed/" />

feed_links_extra prints the contextual ones, depending on the page: a per-post comments feed on single posts, and per-category, per-tag, per-taxonomy, per-author, per-search, and per-post-type-archive feeds on their respective archive pages. On a busy category page that is several more <link> tags.

WordPress generates all of these whether or not anyone reads them. On a real publication with RSS subscribers or a podcast, that is correct and you should leave it alone. On a brochure site, a business directory, or a set of custom post types nobody subscribes to, you are emitting and exposing a pile of XML endpoints for an audience of zero.

Two different jobs: the tags and the URLs

This is the distinction that matters and that most "remove feed links" snippets miss.

  1. Removing the head tags (remove_action above) stops WordPress advertising the feeds. The <link> discovery tags disappear from your HTML. The feeds themselves still work: visit /feed/ directly and you still get valid RSS.
  2. Removing the feeds means making those /feed/ URLs stop resolving, by dropping the rewrite rules that create them and redirecting the addresses back to the real page.

If you only do step 1, the feeds are merely undiscoverable from the page, but a crawler that already knows the /feed/ pattern (and they all do) will keep hitting them. For a site where thin, duplicate feed URLs are showing up in crawl reports, you want step 2.

Why not just hook do_feed and call wp_die()?

Many guides lead with intercepting the feed handler: hook do_feed_rss2, do_feed_atom, and friends and call wp_die() or redirect from inside them. That does kill the output, but it leaves the rewrite rules live. The /feed/ URL still routes; WordPress still matches it, still loads the request as a feed, and only then errors or redirects at the handler. You are catching the request after it has already been recognized as a valid feed route, not removing the route.

php
// Intercepts the handler; the rewrite rule still routes /feed/.
add_action( 'do_feed_rss2', 'te_block_feed', 1 );
add_action( 'do_feed_atom', 'te_block_feed', 1 );
function te_block_feed() {
    wp_die( 'Feeds are disabled.' );
}

Removing the rewrite rules (step 2 below) kills the route itself: there is no longer a rule that maps /feed/ to the feed handler, so the URL stops being a feed URL at all. That is the difference between a feed that errors and a feed that does not exist.

How much does this save?

For page weight, the head-tag removal is marginal: a handful of <link> tags, a few hundred bytes. No request, no script, nothing Lighthouse will flag.

The real win is on the SEO and crawl side, and only on the kind of site that has it. Every /feed/ endpoint is a separate crawlable URL serving near-duplicate content. On a large site with many custom post types and taxonomies, that is potentially thousands of thin feed URLs eating crawl budget and cluttering reports. Killing the rewrite rules and redirecting the addresses removes that whole class of URL. On a small blog with one feed and a few subscribers, it saves nothing worth chasing, and you would be removing something people actually use. Match the fix to the site.

Removing the /feed/ rewrite rules and redirecting

Here is the fuller must-use plugin. It removes the head tags, drops the feed rewrite rules so the URLs no longer route to the feed handler, and 301-redirects any leftover /feed/ request back to the page it hangs off:

php
<?php
/**
 * Plugin Name: TE Remove Feed Links
 * Plugin URI:  https://techearl.com/remove-wordpress-feed-links
 * Description: Removes feed discovery tags from the head, drops the /feed/ rewrite rules, and 301-redirects leftover feed URLs.
 * Version:     1.0.0
 * Author:      Ishan Karunaratne
 * Author URI:  https://techearl.com
 * License:     GPL-2.0-or-later
 * Text Domain: te-remove-feed-links
 */

// 1. Drop the discovery <link> tags from the head.
add_action( 'init', function () {
    remove_action( 'wp_head', 'feed_links', 2 );
    remove_action( 'wp_head', 'feed_links_extra', 3 );
} );

// 2. Remove the rewrite rules that create /feed/ URLs.
//    Feed rules route to index.php?...&feed=$matches[...], so match on the target.
add_filter( 'rewrite_rules_array', function ( $rules ) {
    foreach ( $rules as $rule => $rewrite ) {
        if ( false !== strpos( $rewrite, 'feed=' ) ) {
            unset( $rules[ $rule ] );
        }
    }
    return $rules;
} );

// 3. Redirect any leftover /feed/ request back to the canonical page.
add_action( 'template_redirect', function () {
    if ( ! is_feed() ) {
        return;
    }
    $url = home_url( preg_replace( '#/feed/?(rdf|rss|rss2|atom)?/?$#', '/', $_SERVER['REQUEST_URI'] ) );
    wp_safe_redirect( esc_url_raw( $url ), 301 );
    exit;
} );

One caveat before you drop this in: the template_redirect step keys off is_feed(), which is true for any feed, so this plugin redirects the main site feed too. If you want to keep the main feed and only clear the contextual ones, do not install the full plugin; use the "keep the main feed" variant further down instead.

One note on the redirect: it is the safety net for any feed URL that slips through (a cached rule, a hard-coded link). It sends /anything/feed/ to /anything/, so a crawler that hits an old feed address lands on the real page with a clean 301 instead of a thin XML document. The other half, the rule removal, only takes effect once you flush, which has more to it than the usual one-liner makes out.

Flushing the rewrite rules (and the trap to avoid)

Removing a rule in the rewrite_rules_array filter is only half the job, because WordPress does not run that filter on every request. It builds the rule set once, stores it in the rewrite_rules row of the wp_options table, and matches incoming URLs against that stored copy. Until the stored copy is rebuilt, your /feed/ URLs keep resolving from the old rules. That is exactly why you can strip the head tags, see them gone, and still get RSS at /feed/.

Rebuilding the stored copy is called flushing. Do it once, by hand, after you add the code:

  • Dashboard: Settings, Permalinks, then click Save Changes. You do not need to change anything; saving the page alone reruns rule generation and stores the new set.
  • WP-CLI: wp rewrite flush.

Now the part that bites, and it bites hardest in a performance article: never call flush_rewrite_rules() on a normal hook like init. It is a heavy operation that rewrites the entire rewrite_rules option, and running it on every page load drags the whole site down. It belongs on a plugin activation hook or a one-off manual flush, never in the request path. If you have ever inherited a slow site and found a flush_rewrite_rules() sitting in an init callback, this is why.

That leaves a wrinkle with the must-use plugin above. Must-use plugins have no activation hook. register_activation_hook() never fires for a file in mu-plugins/, because there is no activate/deactivate lifecycle for must-use plugins, so you cannot auto-flush the way a regular plugin would. For the must-use version, flush manually once (Settings, Permalinks, or wp rewrite flush) after dropping the file in, and again any time you change the rule logic.

If you would rather the flush happen automatically, install it as a regular plugin instead of a must-use one and flush on the activation hook:

php
// Only in a regular plugin, NOT in mu-plugins (no activation hook fires there).
register_activation_hook( __FILE__, 'flush_rewrite_rules' );
register_deactivation_hook( __FILE__, 'flush_rewrite_rules' );

The deactivation flush is worth pairing in: it forces a rebuild when the plugin is turned off, so the rules regenerate cleanly without your removal rather than leaving a stale set behind.

If you want to keep the main site feed but drop only the noisy contextual ones, do not run the rewrite removal. Just remove feed_links_extra and leave feed_links:

php
add_action( 'init', function () {
    // Keep the main posts feed; drop the per-category, per-author, per-comment ones.
    remove_action( 'wp_head', 'feed_links_extra', 3 );
} );

A gentler option: keep the feed, drop only the comments feed

If feeds are fine but you never want the comments feed, WordPress gives you a filter instead of a remove_action, so you do not have to remove and re-add the whole thing:

php
add_filter( 'feed_links_show_comments_feed', '__return_false' );

That keeps the main posts feed and its discovery tag, and removes only the global comments feed. It is the right tool when the comments feed is the only part you object to.

The honest caveat

Feeds are not dead. RSS readers, podcast directories, Mailchimp's RSS-to-email campaigns, IFTTT automations, and plenty of integrations consume /feed/. Before you remove feeds wholesale, confirm nothing on the site depends on them. The safe default for a normal blog is to leave the main feed alone and at most trim feed_links_extra. The full removal (rules plus redirect) is for sites where the feed URLs are genuinely unwanted clutter, like a directory of custom post types that exists for browsing, not subscribing.

Verify it worked

Check the head tags are gone:

text
$ curl -s https://example.com/ | grep -i 'application/rss'
(no output)

And, if you removed the URLs, that a feed address now redirects instead of serving XML:

text
$ curl -sI https://example.com/feed/ | grep -i '^location\|^HTTP'
HTTP/2 301
location: https://example.com/

If the head tags are gone but /feed/ still serves RSS, you skipped the rewrite flush. If everything still shows feeds, a page cache is serving old HTML, or a plugin (some SEO plugins manage feed output) is re-adding it.

See also

Sources

Authoritative references this article was fact-checked against.

TagsWordPressPerformancePHPRSSfeedwp_headSEO

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts