Template Overrides (Developer Reference)
Benecaster renders episode pages, the episode archive, and subscriber account pages using PHP template parts. Every template part can be overridden from a child theme or parent theme — no plugin code required. Replacing a single file is enough to change any part of the output.
Looking for email templates? They use the same override mechanism but a different directory, and they’re documented separately in Customizing Email Templates — which lists every email template, the variables each one receives, and the benecaster_email_wrapper_args filter that handles branding without requiring an override at all.
Override Path
Place an overriding file at:
wp-content/themes/[your-child-theme]/benecaster/[template-relative-path].php
The path mirrors the plugin’s own templates/ directory exactly. For example, to override the episode title:
Plugin default: wp-content/plugins/benecaster/templates/episode/title.php
Your override: wp-content/themes/my-child-theme/benecaster/episode/title.php
Resolution order: child theme → parent theme → registered add-on directories → plugin default. The first readable file found is used.
Your theme override wins over everything, add-ons included. An add-on can register its own template directory (see below), but it sits third in that list — it can replace one of Benecaster’s own defaults and it can never outrank a file you put in your theme. If you have overridden a part and it has stopped taking effect, an add-on is not the cause.
Episode Template Parts
All episode template parts receive at minimum $episode_id (int) and $show_id (int).
| Template file | Additional $vars |
Description |
|---|---|---|
episode/single.php |
— | Outer wrapper; loads all other episode parts via benecaster_get_template_part() |
episode/title.php |
— | Episode title and episode type badge (badge suppressed for Full type) |
episode/explicit-badge.php |
$episode_id (int), $show_id (int), $context (string), $show_clean (bool) |
Explicit-content badge; shared by the single page, archive cards, [benecaster_episodes], [benecaster_player], and [benecaster_explicit_badge]. Renders nothing for clean episodes unless $show_clean. See template-episode-explicit-badge |
episode/player.php |
$can_access (bool), $user_tier (string) |
Audio or video player; delegates to player-locked.php when $can_access is false |
episode/player-locked.php |
$can_access (bool), $user_tier (string), $required_tier (string) |
Locked state shown instead of the player; padlock, subscribe CTA, or upgrade prompt |
episode/description.php |
— | Episode show notes / description (HTML) |
episode/content.php |
$can_access (bool), $user_tier (string) |
Extended episode content; may show teaser when $can_access is false |
episode/guests.php |
— | Guest list (rendered when episode has guest data from Guest Manager add-on) |
episode/references.php |
$references (array) |
Show notes references grouped by group_id; each entry: {id, label, url, description, display_label, group_id} |
episode/custom-fields.php |
$field_groups (array) |
Custom field values; each group: {group, fields, values} |
episode/share.php |
$share_links (array) |
Share links; each entry: {label, url, class} |
episode/subscribe.php |
— | Subscribe CTA for non-subscribers; suppressed for subscribers |
episode/navigation.php |
$prev_episode (WP_Post|null), $next_episode (WP_Post|null) |
Previous/next episode links within the same show |
$user_tier is the subscriber’s internal tier slug, or an empty string when the visitor is not subscribed. $required_tier is the minimum tier required to access the episode, or an empty string when undetermined.
Archive Template Parts
All archive template parts receive at minimum $show_id (int).
| Template file | Additional $vars |
Description |
|---|---|---|
archive/archive.php |
— | Outer archive wrapper; loads header, episode cards, pagination |
archive/header.php |
$title (string), $description (string) |
Archive heading and show description |
archive/filters.php |
— | Sort and filter controls bar |
archive/episode-card.php |
$episode_id (int), $can_access (bool) |
Episode card outer wrapper; loads all card sub-parts |
archive/episode-card/artwork.php |
$episode_id (int), $can_access (bool) |
Episode card artwork image |
archive/episode-card/title.php |
$episode_id (int), $can_access (bool) |
Episode card title link |
archive/episode-card/meta.php |
$episode_id (int), $meta (array) |
Card meta: episode_number, season_number, duration, pub_date |
archive/episode-card/excerpt.php |
$episode_id (int), $can_access (bool) |
Episode card excerpt |
archive/episode-card/player.php |
$episode_id (int), $can_access (bool) |
Inline player in the card (mini player) |
archive/episode-card/locked.php |
$episode_id (int) |
Locked state overlay on card when $can_access is false |
archive/pagination.php |
$total_pages (int), $pagination_html (string|null) |
Pagination controls |
archive/no-results.php |
— | Empty state shown when no episodes match the current filters |
Show Page Template Parts
All show page template parts receive $show_id (int).
| Template file | Additional $vars |
Description |
|---|---|---|
show/single.php |
— | Outer show page wrapper; loads all other show parts |
show/header.php |
$title (string), $description (string), $artwork_url (string) |
Show title, artwork, and description |
show/subscribe-links.php |
$links (array) |
Platform subscribe links; each entry: {platform, url} |
show/tiers.php |
$tiers (array) |
Tier listing from the active bridge |
show/episode-list.php |
$episodes (WP_Query) |
Embedded episode listing |
show/stats.php |
$episode_count (int) |
Public stats block (hidden by default; enable via benecaster_show_stats_visible filter) |
Global Template Parts
Locked state templates receive at minimum $episode_id (int) and $show_id (int).
| Template file | Additional $vars |
Description |
|---|---|---|
global/locked-message.php |
$required_tier (string), $user_tier (string) |
Locked state shown when a visitor cannot access an episode; renders padlock indicator + either upgrade prompt (wrong tier) or login prompt (not logged in) |
global/upgrade-prompt.php |
$required_tier (string), $user_tier (string) |
Upgrade CTA shown inside the locked state to logged-in subscribers at the wrong tier |
global/login-prompt.php |
— | Login/subscribe CTA shown inside the locked state to logged-out visitors |
Account Template Parts
Most account template parts receive $show_id (int) and $user_id (int). The wall name prompt receives $user_id only — the wall name is not show-scoped.
| Template file | Additional $vars |
Description |
|---|---|---|
account/subscription.php |
$subscription (array) |
Subscription summary card; $subscription includes tier name, status, join date |
account/buyups.php |
$buyups (array), $has_native (bool) |
The Add-ons section — one card per buy-up available to the subscriber’s tier on this show. Wrapped by the benecaster_before_account_buyups / benecaster_after_account_buyups actions, and contains the benecaster_buyup_soldout_display filter. The per-row shape is documented in the file header. See the upgrade note below before overriding this one. |
account/billing-portal.php |
$sub (array) |
Billing management panel for subscribers on Benecaster-managed billing |
account/feed-url.php |
$feed_url (string), $token_prefix (string) |
Feed URL with Copy button; $token_prefix is first 8 chars for display only |
account/qr-code.php |
$feed_url (string) |
QR code for the subscriber’s feed URL (generated client-side) |
account/app-links.php |
$app_links (array) |
Deep links to podcast apps; each entry: {app, label, url} |
account/token-reset.php |
— | Self-service token reset button and confirmation |
account/wall-name-prompt.php |
$wall_name_prompt_prefill (string), $prompt_show_name (string) |
First-run overlay prompting new subscribers to set their wall name; shown once, then suppressed. Gated by benecaster_show_wall_name_prompt. Receives $user_id only — no $show_id. |
account/avatar.php and account/dashboard.php are overridable the same way and have their own pages — see Account Avatar Template.
If you are looking for
account/display-name.phporaccount/display-name-prompt.php, neither exists. The wall name field lives inaccount/avatar.php, and the prompt isaccount/wall-name-prompt.php, taking$wall_name_prompt_prefill. An override sitting at either of those other paths does nothing at all — silently, because a template that does not exist in the plugin is never looked for in your theme.
Upgrade note — account/buyups.php overrides break buy-up purchases
If you overrode account/buyups.php before buy-up purchase consent shipped, every buy-up purchase from your theme now fails.
The stock template emits data-buyup-amount-cents on the “Add to my plan” button, and assets/js/buyups.js reads that attribute and sends it as the required confirm_amount_cents field. An older override renders a button without the attribute, the script sends nothing, and the endpoint rejects the purchase with 400 confirm_amount_required.
Nothing in the admin, the logs or the server surfaces this as a template problem — the purchase simply does not complete. Re-sync your override against the current stock template.
The value is the price in integer minor units:
data-buyup-amount-cents="<?php echo esc_attr( (string) (int) round( (float) $buyup['price'] * 100 ) ); ?>"
See Buy-ups Subscriber REST API for what the endpoint does with it.
Template Functions
Each of these has a full reference entry in the function library — signature, parameters, return values, and examples.
benecaster_get_template_part( $template, $vars = [], $echo = true )
Load a template part with child theme → parent theme → registered add-on directories → plugin default resolution.
// Load and echo a template part (typical usage in another template):
benecaster_get_template_part( 'episode/title', [
'episode_id' => $episode_id,
'show_id' => $show_id,
] );
// Load and return the rendered HTML:
$html = benecaster_get_template_part( 'episode/title', [
'episode_id' => $episode_id,
'show_id' => $show_id,
], false );
Variables in $vars are extracted into the template’s local scope — $vars['episode_id'] becomes $episode_id inside the template file.
benecaster_locate_template( $template )
Check whether a theme override exists for a template part. Returns the absolute path of the override file if found, or false if the plugin default will be used.
$override = benecaster_locate_template( 'episode/title' );
if ( $override ) {
// A theme file is overriding this template part.
}
This searches the child theme and parent theme only. It deliberately does not see registered add-on directories — an add-on directory is not a theme override, and this function answers the theme question. For “which file would actually load?”, use benecaster_get_template_path() below.
benecaster_get_template_path( $template )
Return the full path of the file that would be loaded — theme override, registered add-on directory, or plugin default. Returns an empty string when no file exists at any of the four locations.
$path = benecaster_get_template_path( 'episode/title' );
// '/var/www/html/wp-content/themes/my-child-theme/benecaster/episode/title.php'
// or the plugin's own default path if no theme override exists.
benecaster_register_template_directory( $path )
For add-on authors. Registers a directory that Benecaster will search when resolving template parts, so an add-on can ship its own parts — both ones that replace a Benecaster default and ones Benecaster has never heard of. Call it from a benecaster_boot callback.
add_action( 'benecaster_boot', function (): void {
benecaster_register_template_directory( plugin_dir_path( __FILE__ ) . 'templates' );
} );
The directory mirrors the plugin’s templates/ structure, so {$path}/guest/profile-card.php answers the template name guest/profile-card.
Registered directories sit third, behind both themes. A part shipped by an add-on is still fully overridable from your theme at {theme}/benecaster/guest/profile-card.php — registration does not change that, and add-on authors should treat any part they ship as a public template.
Among add-ons the order is registration order, which is add-on boot order — so two add-ons registering the same template name is not a supported arrangement.
benecaster_get_header( $name = null )
Load the site header in a way that is compatible with both classic and block (FSE) themes. Use this in outer template files (single.php, archive.php) instead of calling get_header() directly.
- On block themes: renders the
headerblock template part viablock_template_part( 'header' ). - On classic themes with
header.php: callsget_header( $name )normally. - On themes without a header file: degrades silently — no output, no deprecation notice in the log.
Eliminating the deprecation log spam from FSE themes was the primary reason this helper was added. If you are overriding templates/episode/single.php, templates/show/single.php, or templates/archive/archive.php in your theme, replace any get_header() call with benecaster_get_header().
<?php benecaster_get_header(); ?>
<main class="benecaster-episode-single">
<?php benecaster_get_template_part( 'episode/title', [ 'episode_id' => $episode_id ] ); ?>
<?php // ... ?>
</main>
<?php benecaster_get_footer(); ?>
benecaster_get_footer( $name = null )
Counterpart to benecaster_get_header(). Use in outer template files instead of get_footer().
- On block themes: renders the
footerblock template part viablock_template_part( 'footer' ). - On classic themes with
footer.php: callsget_footer( $name )normally. - On themes without a footer file: degrades silently.
Two Customization Layers
Template overrides replace whole files. When you only need to change a value, tweak markup, or inject content without replacing a template, use the complementary hook layers instead:
Before/after action hooks fire around every rendered section:
// Inject content before the episode navigation without overriding navigation.php:
add_action( 'benecaster_before_episode_navigation', function ( int $episode_id, int $show_id ): void {
echo '<hr class="my-divider">';
}, 10, 2 );
Output filters intercept specific values before they reach the template:
// Modify the share links array without touching episode/share.php:
add_filter( 'benecaster_episode_share_links', function ( array $links, int $episode_id ): array {
$links[] = [
'label' => 'Copy link',
'url' => '#',
'class' => 'benecaster-share-copy js-copy-link',
];
return $links;
}, 10, 2 );
See Filter Hooks Reference and Action Hooks Reference for the full catalog.
Templates Management REST Endpoints
The Settings → Templates screen is backed by four REST endpoints. Use these for headless setups, add-ons that bundle a custom template, or custom admin UI.
| Endpoint | Purpose |
|---|---|
GET /benecaster/v1/templates |
List all installed templates; response includes slug, name, preview_url, active (bool), built_in (bool) |
POST /benecaster/v1/templates/upload |
Upload a template ZIP (multipart/form-data, field name template_zip) |
POST /benecaster/v1/templates/{slug}/activate |
Activate an installed template for the current show |
DELETE /benecaster/v1/templates/{slug} |
Remove an installed template; returns 403 when built_in: true |
All endpoints require manage_options capability and a valid WP REST nonce. Built-in templates (Default Light, Default Dark) are always present and cannot be deleted.
Recipes
Worked examples for the patterns on this page live in the recipe library, each with
runnable code you can copy:
- Override a single episode template part from a child theme
- Load a custom template part from an add-on
- Check episode access in a shortcode or widget
- Replace the episode share links with a custom share card
- Inject a “Listen now” CTA button into every archive episode card
- Hide the sort controls on the episode archive
- Customize the upgrade prompt for a specific show
- Add a custom section to the show page
- Add a custom section below the QR code on the subscriber account page