TierAwareFeedNamespaceInterface
\Benecaster\Feed\Namespaces\TierAwareFeedNamespaceInterface
An optional extension of FeedNamespaceInterface for a feed namespace handler whose channel-level output depends on which episodes the requesting tier can see. It adds one method, channel_tags_for_tier(). It is a public extension point: a handler added through the benecaster_feed_namespaces filter can implement it.
Most channel tags are show metadata and need only the show ID, which is why channel_tags() takes nothing else. A few describe episodes from the channel, <podcast:trailer> being the first, and those cannot be built safely without the tier. A channel tag that names an episode hands that episode’s media URL to every recipient of the feed, so emitting one for an episode the availability rules are still withholding would bypass the gate the plugin enforces. Two obligations fall on the implementer:
- Restrict output to
$available_episode_ids. Do not run your own query for episodes; the ID list is the gate, not a hint. - Treat an empty list as “advertise nothing”, never “no restriction”. A handler that skips its filter when the list is empty advertises everything to the tier entitled to the least.
When the feed compiler builds the channel it asks each registered handler in turn. If the handler implements DataDrivenNamespaceInterface the compiler renders it from the filtered channel data array. Otherwise, if it implements this interface, the compiler calls channel_tags_for_tier(). Any other handler gets channel_tags(). Handlers that do not implement the interface need no change. A handler that implements both this and DataDrivenNamespaceInterface is treated as data-driven, and the compiler never calls its channel_tags_for_tier().
Recipe
Add a channel-level feed tag that names an episode, safely
Most channel-level feed tags are made of show metadata — a license, a GUID, a funding link — and need nothing but the show ID to build. A channel tag that names an episode is a different problem, and getting it wrong hands out media people paid for.
The <channel> block is compiled once per tier and sent to every subscriber on that tier. Put an episode’s enclosure URL there and you have published it to all of them at once — including, if you built the list from your own query, episodes the availability rules are still withholding. That routes around the gate the whole plugin exists to enforce, and it does it in the one part of the feed every recipient reads.
Implement TierAwareFeedNamespaceInterface instead of FeedNamespaceInterface. Benecaster then hands your handler the tier slug plus every episode ID that tier may receive — the whole feed, not just the page being compiled, so a trailer published years ago still appears on page 1.
Two rules the interface cannot enforce for you. Never widen the search beyond $available_episode_ids, and treat an empty array as “advertise nothing” rather than “no restriction”. The second is the one that looks like working code: a handler that skips its filter when the list is empty advertises everything to the tier entitled to least. Benecaster’s own <podcast:trailer> follows both rules.
Pass the media URL through the enclosure filters, as Benecaster does. A site running the download proxy has those rewriting every <enclosure>; a channel tag that skips them republishes the raw host URL the proxy exists to hide.
Handlers implementing only FeedNamespaceInterface keep working untouched — Benecaster falls back to channel_tags() for them, so nothing you have already shipped needs changing.
<?php
use Benecaster\Episode\EpisodeMeta;
use Benecaster\Feed\Namespaces\TierAwareFeedNamespaceInterface;
class MyHighlightsNamespace implements TierAwareFeedNamespaceInterface {
public function get_xmlns(): array {
return [ 'myns' => 'https://example.com/ns/1.0' ];
}
// No tier context here, so advertise no episodes at all rather than guess.
public function channel_tags( int $show_id ): array {
return $this->channel_tags_for_tier( $show_id, '', [] );
}
public function channel_tags_for_tier( int $show_id, string $tier_slug, array $available_episode_ids ): array {
// Empty means "this tier can reach nothing", NOT "no filter applied".
if ( empty( $available_episode_ids ) ) {
return [];
}
$frags = [];
foreach ( $available_episode_ids as $episode_id ) {
$episode_id = (int) $episode_id;
if ( ! get_post_meta( $episode_id, '_my_highlight', true ) ) {
continue;
}
$ep_meta = new EpisodeMeta( $episode_id );
// Pass the URL through the same filters the <enclosure> uses, or a
// site running the download proxy gets its raw host URL republished
// in the channel — the one place every recipient sees it.
$url = (string) apply_filters(
'benecaster_feed_enclosure_url',
$ep_meta->get_audio_url(),
$episode_id,
$show_id,
$tier_slug
);
if ( '' === $url ) {
continue;
}
$frags[] = '<myns:highlight url="' . esc_url( $url ) . '"/>' . "\n";
}
return $frags;
}
public function episode_tags( int $episode_id, int $show_id ): array {
return [];
}
}
add_filter( 'benecaster_feed_namespaces', function ( array $handlers ): array {
$handlers[] = new MyHighlightsNamespace();
return $handlers;
} );
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
channel_tags_for_tier( int $show_id, string $tier_slug, array $available_episode_ids ): array |
Public | — | Returns channel-level XML fragments (string[], each a complete tag ending in a newline) for one show as seen by one tier. $available_episode_ids is every episode the tier may receive across the whole feed, not just the page being compiled, because a trailer published long ago belongs in the channel whichever page is built. Do not treat it as a page window. Must treat an empty array as "advertise no episodes". The compiler passes the requesting tier's slug, which for the public feed is public. |
Notes
Pass any media URL you emit through the public URL filters, as Benecaster does: benecaster_episode_audio_url and then benecaster_feed_enclosure_url. A site with a download proxy has those filters rewriting every <enclosure>; a channel tag that skips them republishes the raw host URL the proxy exists to hide, in the one block every recipient reads.
Per-tier audio variants are separate and are not applied on the channel path. Benecaster advertises the show's default file there.
A handler must still implement the parent channel_tags() and episode_tags() methods. The plain channel_tags() should answer as a caller with no tier would, and so advertise no episodes.