Skip to main content

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_on only for a free add-on.
  • The screen owns every path under it. Your fill receives path (what follows #/{path}/, empty on the screen itself) and navigate( 'sub/path' ) to move between your own sub-screens. React Router isn’t shared with add-ons, so use navigate rather than a router of your own.
  • Unsaved changes: call the fill’s setDirty( true ) as soon as someone edits and setDirty( 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 you navigate() after a save.
  • icon is one of bar-chart, book-open, calendar, file-text, gift, mail, megaphone, message-square, mic, puzzle, settings, star or users; anything else draws a puzzle piece. The menu item carries data-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 id or path that 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 ) and closeShellPanel() 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. Call openShellPanel() 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 carries data-tour="{id}-panel" for tours.
  • add_on works 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 default scope: '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 it activeShowIds. 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 out add_on only for a free add-on.
  • Shortcuts combine any of mod (Cmd on a Mac, Ctrl elsewhere), alt and shift with 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 neither mod nor alt does 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 id already 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:

  1. Enqueue your script with benecaster-admin as a dependency, so Benecaster’s runtime is published before your bundle is parsed.
  2. Map react, react-dom, react-dom/client, react/jsx-runtime, @wordpress/i18n and @tanstack/react-query as externals pointing at window.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, or show_id on BenecasterEpisodePrePublish), 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. On BenecasterEpisodeReferenceRow, a reference added in the editor gets its reference_id as soon as the episode’s save finishes; key your data on reference_id, never the row’s id, which changes on every save.

BenecasterEpisodePrePublish uses 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 reads showId gets undefined. 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’s subscriberId is 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.

  • BenecasterEpisodeListRow renders 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.
  • BenecasterEpisodeListToolbar sits 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 of route (a REST route of your own; Benecaster sends POST { episode_ids, show_id }) or onApply( episodeIds, { showId } ). With addon set 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 →