Admin Extension Points
Benecaster exposes a structured set of extension surfaces for add-ons to add UI, configuration, and behavior to the admin — without editing Benecaster’s own files or fighting with WordPress hooks. There are two tiers: a PHP tier that declares where your add-on appears, and a React SlotFill tier that draws what it shows there.
Two Tiers of Admin Extension
Tier 1 — PHP Filter (Declarations)
Declare tabs, settings pages, dashboard cards, menu items, email types and setup reminders using PHP arrays. From the declaration Benecaster draws the tab button, the navigation entry or the menu item, and decides which shows it appears on. It does not draw form fields: whatever goes inside a tab, page, card or screen is a Tier 2 component.
Tier 1 on its own is enough for an email type or a setup reminder. For anything with a panel of its own, it is the first half of the job.
Tier 2 — React SlotFill (Interactive Components)
Fill a named slot with a React component compiled into your add-on’s JavaScript bundle. Use Tier 2 when your UI needs state, effects, third-party widget embeds, live previews, or custom pickers that can’t be expressed as field declarations.
The two tiers work together: declare the tab with a Tier 1 PHP filter, so its button appears in the navigation, then fill its panel with a Tier 2 React component, which saves through your own REST route or meta.
PHP Extension Filters
Episode Editor Tabs
Filter: benecaster_episode_editor_tabs
Adds a tab to the episode editor. Each declaration takes an id, a label, and optionally an icon and add_on. Benecaster draws the tab’s button; the panel is the React component you register for its BenecasterEpisodeEditorTab_{id} slot (Tier 2), which receives { episodeId, showId } and saves through your own REST route or meta. A sections key is passed through to GET /admin/extensions as data only: nothing renders or saves it.
The filter takes two arguments ($tabs, $show_id), so register it with 10, 2. $show_id is the show of the episode being edited, or 0 when the list is fetched without one. Put your entitlement slug in the add_on key and Benecaster hides the tab on any show where your add-on is switched off; a tab without add_on shows on every show.
#/episodes/{id}?tab={tab id} opens the episode on your tab. On a show where your add-on is off, it opens on Details.
Full spec and example: benecaster_episode_editor_tabs · add-episode-editor-tab-via-php
Custom Settings Pages
Filter: benecaster_settings_pages
Adds a page to the left navigation of the Benecaster Settings screen, which is install-wide, not per show. Benecaster renders no fields for it: the page’s content is whatever React component you register for its BenecasterSettingsPage_{id} slot (Tier 2). The fill receives no props, because no show is in scope there.
Full spec and example: benecaster_settings_pages
Per-Show Settings Tabs
Filter: benecaster_show_settings_pages
Adds a tab to one show’s settings navigation, below Templates. Use it for anything whose data belongs to a single show. As with settings pages, Benecaster renders no fields: the tab’s content is the React component you register for its BenecasterShowSettingsPage_{id} slot, and the fill receives { showId }.
The filter takes two arguments ($pages, $show_id), so register it with 10, 2. Put your entitlement slug in the add_on key and Benecaster hides the tab on any show where your add-on is switched off.
Full spec and example: benecaster_show_settings_pages · add-a-tab-to-a-shows-settings
Settings Sections
Filter: benecaster_settings_sections
A data-only extension point. Declarations reach the GET /admin/extensions response, but no settings screen renders them, so the fields are not shown and nothing saves them. For an add-on tab in a show’s settings, use benecaster_show_settings_pages; for a few fields below the show editor’s own, use the BenecasterShowEditorAfterFields slot.
Full spec: benecaster_settings_sections
Dashboard Cards
Filter: benecaster_dashboard_cards
Adds cards to the Benecaster dashboard after the built-in stat cards. Cards render via the BenecasterDashboardCards SlotFill slot.
Full spec and example: benecaster_dashboard_cards · inject-react-component-into-slot
Email Types
Filter: benecaster_managed_email_types
Registers a new email type with Benecaster’s email system. Every type returned by this filter appears as an editable template in the Email Editor add-on’s template manager. Benecaster registers welcome, token_reset, and token_revoked at boot.
Full spec and example: benecaster_managed_email_types · register-email-type
Add-on Admin Screens
Filter: benecaster_admin_screens
Gives your add-on its own item in the Benecaster menu and a screen to go with it. Declare the screen in PHP; draw it in React by filling the slot BenecasterAddonScreen_{id}.
Each declaration takes an id (letters, numbers, - and _), a translated label, and optionally a path (one segment; defaults to the id), an icon, add_on (your entitlement slug) and sub_screens (tour screen ids mapped to sub-paths, so a tour can step onto a sub-screen).
add_filter( 'benecaster_admin_screens', function ( array $screens ): array {
$screens[] = [
'id' => 'my-addon',
'label' => __( 'My Add-on', 'my-addon' ),
'icon' => 'puzzle',
'add_on' => 'benecaster-addon-my-addon',
'sub_screens' => [ 'my-addon-settings' => 'settings' ],
];
return $screens;
} );
- The menu item shows while your add-on is switched on for at least one show, and your fill runs only when the selected show is one of them. On any other show Benecaster says your add-on isn’t switched on for it, so don’t check activation yourself. Leave out
add_ononly for a free add-on. - The screen owns every path under it. Your fill receives
path(what follows#/{path}/, empty on the screen itself) andnavigate( 'sub/path' )to move between your own sub-screens. React Router isn’t shared with add-ons, so usenavigaterather than a router of your own. - Unsaved changes: call the fill’s
setDirty( true )as soon as someone edits andsetDirty( false )after a save. While it’s set, a menu click,navigate(), Back or Forward, switching show, reloading or closing the tab all ask “You have unsaved changes. Leave without saving them?” first. Clear it before younavigate()after a save. iconis one ofbar-chart,book-open,calendar,file-text,gift,mail,megaphone,message-square,mic,puzzle,settings,starorusers; anything else draws a puzzle piece. The menu item carriesdata-tour="{id}-nav"for tours.- The menu is worked out when the page loads, so switching an add-on on or off reaches it on the next load. An
idorpaththat is already one of Benecaster’s own screens is ignored, and so is a second claim on one another add-on already took.
Full walkthrough: add-a-screen-to-the-admin · reference: benecaster_admin_screens
The Analytics screen is Benecaster’s own, and only the Analytics Dashboard add-on takes it over, through the BenecasterAnalyticsScreen slot. See analytics-screen-takeover.
Side Panels Beside Every Screen
Filter: benecaster_admin_shell_panels
Adds a side panel to the Benecaster admin, beside every screen. Declare the panel in PHP; draw its body in React by filling the slot BenecasterShellPanel_{id}. The panel sits outside the screens, so an open panel stays open while the podcaster moves between them.
Each declaration takes an id (letters, numbers, - and _), a translated label, and optionally add_on (your entitlement slug), scope (show, the default, or any_show) and a shortcut such as alt+shift+k.
add_filter( 'benecaster_admin_shell_panels', function ( array $panels ): array {
$panels[] = [
'id' => 'my-addon-notes',
'label' => __( 'Notes', 'my-addon' ),
'add_on' => 'benecaster-addon-my-addon',
'shortcut' => 'alt+shift+n',
];
return $panels;
} );
- One panel is open at a time. Its shortcut toggles it, Escape closes it, and
window.BenecasterExtensions.openShellPanel( id )andcloseShellPanel()open and close it from your own button. Benecaster draws no launcher of its own, so put a button where the podcaster will see it, such as your add-on screen or a Dashboard card. CallopenShellPanel()from an event handler, not when your script loads. Focus moves into the panel when it opens and back when it closes. - Your fill receives
{ showId, activeShowIds, close }. The panel element carriesdata-tour="{id}-panel"for tours. add_onworks as it does for add-on screens. The panel is offered while your add-on is switched on for at least one show. With the defaultscope: 'show', your fill runs only when the selected show is one of them; on any other show Benecaster says your add-on isn’t switched on for it.scope: 'any_show'runs your fill on every show and hands itactiveShowIds. Use it only for the podcaster’s own tools, such as a search across their back catalogue, and never for a panel that shows or changes the selected show’s data. Leave outadd_ononly for a free add-on.- Shortcuts combine any of
mod(Cmd on a Mac, Ctrl elsewhere),altandshiftwith one key: a letter, a digit, or one of/ . , ; [ ] - =and the backtick. The browser’s, developer tools’ and the editor’s own shortcuts (copy, paste, undo, find, reload, bold, link and so on) are refused, and the panel then has no shortcut. A shortcut with neithermodnoraltdoes nothing while someone is typing in a field. If two panels declare the same shortcut, the first keeps it. - The list is worked out when the page loads, so switching an add-on on or off reaches it on the next load. A second declaration of an
idalready taken is ignored. A panel whose fill never registered says the add-on did not load.
Full walkthrough: open-your-own-panel-beside-every-screen · reference: benecaster_admin_shell_panels
Add-on Setup Checks
Filter: benecaster_addon_setup_checks
Adds setup reminders for an add-on that needs configuring before it works — an API key, a platform connection, a list to sync to. They appear as the first rows of the Dashboard’s Needs attention section, not as WordPress admin notices, and they are site-wide: the same reminders show on every show’s dashboard.
Each entry you add is a callable. It returns null once its requirement is met, or a notice array while it is still missing: key (unique across every add-on, so prefix it with your slug), severity ('warning' or 'error'), message, and optionally action_url with action_label (give both or neither) and requires.
The checks run each time a dashboard loads, so keep them cheap. A reminder disappears on its own once its callable returns null. The × hides it for that user; it comes back if the problem returns after being fixed.
add_filter( 'benecaster_addon_setup_checks', function ( array $checks ): array {
$checks[] = function (): ?array {
if ( '' !== (string) get_option( 'my_addon_api_key', '' ) ) {
return null;
}
return [
'key' => 'my_addon_api_key_missing',
'severity' => 'warning',
'message' => __( 'My Add-on needs an API key before it can sync.', 'my-addon' ),
'action_url' => admin_url( 'admin.php?page=my-addon-settings' ),
'action_label' => __( 'Add API key', 'my-addon' ),
];
};
return $checks;
} );
Setup in several steps. Give a later step 'requires' => '<key of the earlier check>' and it stays hidden while the earlier one is failing, so the podcaster sees one step at a time. A requires naming a key that is not failing — passed, misspelt, or from an add-on that is not loaded — hides nothing. The full reference is on the benecaster_addon_setup_checks hook page.
React SlotFill Slots
All slots are rendered unconditionally — they are simply empty when no add-on has registered a fill. Register fills after wp.domReady fires using window.BenecasterExtensions?.registerFill( slotName, Component ).
Build your bundle against Benecaster’s React. A fill renders inside Benecaster’s React tree, so it must share Benecaster’s copy of React to use hooks. Benecaster publishes its runtime on window.BenecasterExtensions for this. Your add-on needs two things:
- Enqueue your script with
benecaster-adminas a dependency, so Benecaster’s runtime is published before your bundle is parsed. - Map
react,react-dom,react-dom/client,react/jsx-runtime,@wordpress/i18nand@tanstack/react-queryas externals pointing atwindow.BenecasterExtensions.
The @wordpress/i18n Benecaster publishes is version 6, so write sprintf() format strings for that version.
Without both, a component that calls useState or useEffect throws Invalid hook call at runtime, and nothing in the admin explains why. The key names, a bundler config and the setup-wizard exceptions are in inject-react-component-into-slot.
| Slot name | Location | fillProps |
|---|---|---|
BenecasterEpisodePrePublish |
Pre-publish panel in the episode editor (fires before Publish is confirmed) | { episode_id, show_id } |
BenecasterEpisodeEditorTab_{id} |
Episode editor tab panel (where {id} matches the tab’s id in benecaster_episode_editor_tabs) |
{ episodeId, showId } |
BenecasterEpisodeEditorAfterFields |
Below all fields in the episode editor | { episodeId, showId } |
BenecasterEpisodeListRow |
Inside each row of Benecaster → Episodes, on a line under the episode’s title | { episodeId, showId } |
BenecasterEpisodeListToolbar |
The toolbar above the table on Benecaster → Episodes, left of the bulk-action controls | { showId } |
BenecasterShowEditorAfterFields |
Below all fields in the show editor General section | { showId } |
BenecasterDashboardCards |
After built-in stat cards on the dashboard | { showId } |
BenecasterShellPanel_{id} |
The body of an add-on’s side panel (declared with benecaster_admin_shell_panels), beside every screen |
{ showId, activeShowIds, close } |
BenecasterAddonScreen_{id} |
The whole body of an add-on’s own screen (declared with benecaster_admin_screens), at #/{path} and every path under it — only for a selected show the add-on is switched on for |
{ showId, path, navigate, setDirty } |
BenecasterAnalyticsScreen |
The whole body of Benecaster’s Analytics screen — rendered only for the Analytics Dashboard add-on, on a show where it is licensed and switched on | { showId, path, navigate, setDirty } |
BenecasterSettingsPage_{id} |
An add-on page on the install-wide Benecaster Settings screen | none |
BenecasterShowSettingsPage_{id} |
An add-on tab inside one show’s settings (declared with benecaster_show_settings_pages) |
{ showId } |
BenecasterSubscriberDetailAfter |
Below the token section in the subscriber detail slide-in panel | { subscriberId, showId } |
The four episode-editor slots pass the episode’s own show (
showId, orshow_idonBenecasterEpisodePrePublish), not the one picked in the show switcher: the show an existing episode is saved under, or the show a new one will be created under. OnBenecasterEpisodeReferenceRow, a reference added in the editor gets itsreference_idas soon as the episode’s save finishes; key your data onreference_id, never the row’sid, which changes on every save.
BenecasterEpisodePrePublishuses underscore-separated prop names (episode_id,show_id) unlike most other slots which use camelCase. This matches the PHP-side convention for episode and show IDs throughout the REST API.
The two settings-page slots are easy to confuse. A fill registered under the wrong name never renders, and a fill on
BenecasterSettingsPage_{id}that readsshowIdgetsundefined. Neither raises an error, so the symptom is a blank tab.
A fill that throws doesn’t take the admin down with it. Each fill renders inside its own error boundary: if it fails, Benecaster shows a short message naming the add-on and the rest of the screen keeps working. A screen whose fill was never registered says the add-on did not load, rather than showing a blank page.
BenecasterSubscriberDetailAfter’ssubscriberIdis the WordPress user ID — not the subscriber row ID the/subscribers/{id}REST routes take. Look up the subscriber row by user ID if your fill needs to call those routes.
Episodes Screen
Three extension points on Benecaster → Episodes let an add-on that processes episodes — transcribing them, pushing them somewhere — show where each one stands and act on the ones a podcaster ticks.
BenecasterEpisodeListRowrenders once per row, on its own line under the title, for a compact status badge. Nothing is drawn on a row where your fill returns nothing. Fetch the statuses for the whole page in one request rather than one per row.BenecasterEpisodeListToolbarsits above the table. Use it for an action that does not depend on which rows are ticked, such as a button that processes every episode without a transcript.window.BenecasterExtensions.registerEpisodeBulkAction( { id, label, addon, accepts, route } )adds an action to the bulk-action menu. Give exactly one ofroute(a REST route of your own; Benecaster sendsPOST { episode_ids, show_id }) oronApply( episodeIds, { showId } ). Withaddonset to your slug, the action is offered only on shows where your add-on is on;accepts( row )can leave rows out. The rows gain tick boxes only while at least one action applies to the selected show, and the selection never spans pages.
Check your add-on is on for the show yourself in both slots, with window.BenecasterExtensions.addonIsActive( slug, showId ). A fill carries no add-on identity, so Benecaster cannot hide it on a show where your add-on is switched off. addonIsActive() accepts the short or full slug and returns false for an unknown show. The bulk action’s own addon key does this for you, but your route must still check the capability and the show, since hiding an action in the UI is not access control.
For a complete SlotFill example covering tab injection and dashboard cards, see inject-react-component-into-slot.
Front-End Extension
Account Page Sections
Filter: benecaster_account_sections
Fires at the end of the [benecaster_account] shortcode output, after all built-in subscriber account sections (feed URL, QR code, app deep links). Hook here to append HTML sections to the subscriber-facing account page.
Full spec: benecaster_account_sections
Quick Reference
| Surface | Mechanism | Where it appears |
|---|---|---|
| Episode editor tab | benecaster_episode_editor_tabs filter + BenecasterEpisodeEditorTab_{id} SlotFill |
Episode editor |
| Episode editor extra fields | BenecasterEpisodeEditorAfterFields SlotFill |
Episode editor |
| Pre-publish panel | BenecasterEpisodePrePublish SlotFill |
Episode editor |
| Episode list row badge | BenecasterEpisodeListRow SlotFill |
Benecaster → Episodes |
| Episode list toolbar | BenecasterEpisodeListToolbar SlotFill |
Benecaster → Episodes |
| Episode bulk action | registerEpisodeBulkAction() |
Benecaster → Episodes |
| Show editor extra fields | BenecasterShowEditorAfterFields SlotFill |
Show editor |
| Dashboard card | benecaster_dashboard_cards filter + BenecasterDashboardCards SlotFill |
Dashboard |
| Add-on settings page | benecaster_settings_pages filter + BenecasterSettingsPage_{id} SlotFill |
Benecaster Settings (install-wide) |
| Add-on tab in a show’s settings | benecaster_show_settings_pages filter + BenecasterShowSettingsPage_{id} SlotFill |
One show’s settings |
| Settings section declarations | benecaster_settings_sections filter |
Nowhere (data-only) |
| Subscriber detail panel section | BenecasterSubscriberDetailAfter SlotFill |
Subscriber list |
| Subscriber account page section | benecaster_account_sections action |
Front-end account page |
| Email type registration | benecaster_managed_email_types filter |
Email Editor |
| Add-on setup reminders | benecaster_addon_setup_checks filter |
Dashboard → Needs attention (until configured) |
| Add-on menu item and screen | benecaster_admin_screens filter + BenecasterAddonScreen_{id} SlotFill |
Benecaster menu |
| Add-on side panel | benecaster_admin_shell_panels filter + BenecasterShellPanel_{id} SlotFill |
Beside every Benecaster screen |
See Also
Need this built rather than just documented? See our services →