add_feed rewrite overwriting standard permalinks

If you don’t want to use add_feed(), then you can use add_rewrite_rule():

add_action( 'init', function () {
    add_rewrite_rule(
        '^feed/(schedule|calendar)/?$',
        'index.php?feed=$matches[1]',
        'top'
    );
} );

add_action( 'do_feed_schedule', function () {
    header( 'Content-Type: text/plain' ); // dummy type just for testing
    echo 'Yay, it works!';
} );

But regarding the original question about add_feed()..

Why would WordPress overwrite existing page rules with the feed,
instead of just using only the /feed/ base as expected?

So that example.com/schedule would serve your custom feed, just like standard feed URLs such as example.com/feed. But WordPress does create the (proper) rules having the feed/ base for custom feed URLs like example.com/feed/schedule.

Therefore you shouldn’t use schedule as the slug of your Page, or if you have/need to, then you can alter the feed rewrite so that the example.com/schedule will serve the Page instead of your feed:

  • If you know the exact rule/RegEx:

    add_filter( 'rewrite_rules_array', function ( $rules ) {
        $rules2 = [];
        foreach ( $rules as $regex => $query ) {
            if ( '(feed|rdf|rss|rss2|atom|schedule)/?$' === $regex ) { // it has 'schedule'
                $rules2['(feed|rdf|rss|rss2|atom)/?$'] = $query;       // so let's remove it
            } else {
                $rules2[ $regex ] = $query;
            }
        }
        return $rules2;
    } );
    
  • Otherwise, try something dynamic like this:

    add_filter( 'rewrite_rules_array', function ( $rules ) {
        global $wp_rewrite;
    
        $feeds = $wp_rewrite->feeds;         // original feeds
        $pages = [ 'schedule', 'calendar' ]; // page slugs list
        $feeds2 = array_diff( $feeds, $pages );
    
        $rules2 = [];
        $feeds = implode( '|', $feeds );
        $feeds2 = implode( '|', $feeds2 );
    
        foreach ( $rules as $regex => $query ) {
            if ( '(' . $feeds . ')/?$' === $regex ) {
                $rules2[ '(' . $feeds2 . ')/?$' ] = $query;
            } else {
                $rules2[ $regex ] = $query;
            }
        }
    
        return $rules2;
    } );
    

Note that I use the foreach so that the rule stays in its original position. And another possible option is that just move the rule to a position after the Page rewrite rules. But I haven’t yet tried that and the above code should be enough as a workaround..


And if I’m not mistaken, here’s the code which WordPress use to generate the rule that’s being altered in the above function:

// Create query for /(feed|atom|rss|rss2|rdf) (see comment near creation of $feedregex).
$feedmatch2 = $match . $feedregex2;
$feedquery2 = $feedindex . '?' . $query . '&feed=' . $this->preg_index( $num_toks + 1 );

See the source on Trac.