Extending the Plugin
BuddyPress Member Blog overviewFree + ProGet BuddyPress Member Blog →
Recipes for the hooks developers use most, checked against version 4.3.0. Drop them into a must-use plugin or your own plugin.
The complete list of every action and filter, with parameters and the file that fires it, is the Hook Reference. It is generated from source. If something you need is not hookable, that is a gap in the hook surface; do not patch a plugin class.
Hooks at a glance
Section titled “Hooks at a glance”| Hook | Type | Use it to |
|---|---|---|
bpmb_can_create_post |
filter | Decide who may write. The gate every entry point asks. |
bpmb_can_publish_post |
filter | Decide who publishes directly and who goes to review. |
bpmb_moderate_edits |
filter | Send edits of live posts back to review. |
bpmb_post_published |
action | React when a post goes live. |
bpmb_member_home_tabs |
filter | Add, remove or reorder member home tabs. |
bpmb_posts_toolbar |
action | Add a control to the post list toolbar. |
bpmb_dashboard_show_title |
filter | Show or hide the dashboard’s own heading. |
bpmb_hide_peepso_blogposts_tab |
filter | Keep or hide PeepSo’s own Blog Posts profile tab. |
bpmb_reject_quick_reasons |
filter | Set the quick reasons offered when sending a post back. |
bpmb_followable_taxonomies |
filter | Let members follow tags or a custom taxonomy. |
bpmb_topic_hub_taxonomies |
filter | Choose which term archives become topic hubs. |
bpmb_default_post_image |
filter | Set a fallback card image for posts without one. |
bpmb_writer_url |
filter | Point writer links somewhere else. |
bpmb_max_claps |
filter | Change the per-member clap cap. |
bpmb_feature_capability |
filter | Change who may feature posts. |
bpmb_rest_upload_allowed_types |
filter | Change the image types the editor accepts. |
bpmb_rest_upload_max_size |
filter | Change the upload size limit, in bytes. |
bpmb_denial_reason_icons |
filter | (Pro) Change the icon beside each refusal reason. |
buddypress_member_blog_pro_should_load_assets |
filter | (Pro) Decide whether Pro’s CSS and JavaScript load. |
Email hooks (bpmb_email_enabled, bpmb_email_subject, bpmb_email_body, bpmb_email_palette)
are covered in Emails. Template hooks are in Templates and Overrides.
Restrict who may write
Section titled “Restrict who may write”There are two layers, and you almost always want the outer one.
The setting layer: bpmb_allowed_roles
Section titled “The setting layer: bpmb_allowed_roles”BP_Member_Blog_Access_Control::get_allowed_roles() reads the roles the site owner ticked on the
Access tab and passes them through bpmb_allowed_roles (array $roles). The member-type
equivalent, used when BuddyPress member types are configured, is bpmb_allowed_member_types
(array $types).
add_filter( 'bpmb_allowed_roles', function ( $roles ) { return array( 'editor', 'columnist' );} );If both roles and member types are configured, either one grants access.
The decision layer: bpmb_can_create_post
Section titled “The decision layer: bpmb_can_create_post”This is the gate the post form, the autosave route and core’s POST /wp/v2/posts all ask. Pro hangs
its post limits and credit balance here. Filter it and every entry point closes together.
/** * Only members registered for at least 7 days may write. * * @param bool $can_create Whether the plugin has decided they may. * @param int $user_id The user being asked about. */add_filter( 'bpmb_can_create_post', function ( $can_create, $user_id ) { if ( ! $can_create ) { return false; // Never widen a denial you did not issue. }
if ( user_can( $user_id, 'manage_options' ) ) { return true; }
$user = get_userdata( $user_id );
return $user && ( time() - strtotime( $user->user_registered ) ) >= WEEK_IN_SECONDS;}, 10, 2 );Related gates with the same shape: bpmb_can_edit_post ($can_edit, $post_id, $user_id),
bpmb_can_delete_post ($can_delete, $post_id, $user_id) and bpmb_can_manage_categories
($can_manage, $user_id). Results are cached per request and cleared when a user’s role changes.
Control moderation
Section titled “Control moderation”Who goes to review: bpmb_can_publish_post
Section titled “Who goes to review: bpmb_can_publish_post”can_publish() answers “may this member put a post straight on the site?”. A member who may not
lands in pending, where editors review it from the Member submissions view under Posts.
/** * New members are reviewed until they have 3 published posts. * * @param bool $can_publish Whether the plugin has decided they may publish. * @param int $post_id Post being published (0 for a new post). * @param int $user_id The author. */add_filter( 'bpmb_can_publish_post', function ( $can_publish, $post_id, $user_id ) { if ( ! $can_publish || user_can( $user_id, 'edit_others_posts' ) ) { return $can_publish; }
return count_user_posts( $user_id, 'post', true ) >= 3;}, 10, 3 );Edits of live posts: bpmb_moderate_edits
Section titled “Edits of live posts: bpmb_moderate_edits”By default, editing a post that is already published keeps it live. That matches core, where an author may edit their own published post, and it stops a typo fix from quietly taking a post off the site. To send every edit back to review:
/** * @param bool $moderate_edits Default false. * @param string $current The post's current status ('publish' or 'private'). */add_filter( 'bpmb_moderate_edits', function ( $moderate_edits, $current ) { return ! current_user_can( 'edit_others_posts' );}, 10, 2 );Quick reasons for sending a post back: bpmb_reject_quick_reasons
Section titled “Quick reasons for sending a post back: bpmb_reject_quick_reasons”The send-back form offers a row of quick reasons. Clicking one adds it to the reason box as a sentence the reviewer can still edit. The filter receives and returns an array of sentences.
add_filter( 'bpmb_reject_quick_reasons', function ( $reasons ) { $reasons[] = __( 'Please add a source for the statistics you quote.', 'my-plugin' ); return $reasons;} );React to a published post
Section titled “React to a published post”bpmb_post_published (int $post_id, int $user_id) fires when a member’s post goes live:
- from the post form, when the member publishes;
- from the member home, when the member publishes a draft;
- when an editor approves a pending post and it goes live now. The second argument is always the post’s author, never the reviewer.
It fires after the featured image is saved, so a listener can read get_post_thumbnail_id(). Pro
uses it to deduct a credit. More than one path can fire it for the same post, so make your listener
idempotent (store a marker in post meta and check it first).
add_action( 'bpmb_post_published', function ( $post_id, $user_id ) { if ( get_post_meta( $post_id, '_my_plugin_pinged', true ) ) { return; } update_post_meta( $post_id, '_my_plugin_pinged', 1 );
wp_remote_post( 'https://hooks.example.com/new-post', array( 'blocking' => false, 'body' => array( 'title' => get_the_title( $post_id ), 'url' => get_permalink( $post_id ), ), ) );}, 10, 2 );Moderation fires its own actions as well:
| Hook | Arguments | When |
|---|---|---|
bpmb_post_approved |
int $post_id, int $reviewer_id |
A pending post is approved, whether it publishes now or is scheduled. |
bpmb_post_rejected |
int $post_id, int $reviewer_id |
A reviewer sends the post back. |
Approve only acts on a post that is still pending, so two reviewers clicking at once cannot publish
it twice. Pro’s editorial workflow uses the same path, so these hooks fire for it too.
The clap equivalent is bpmb_post_clapped ($post_id, $user_id, $mine, $total).
Customize the member home
Section titled “Customize the member home”The member home is one tab set shared by the profile Blog tab (BuddyPress, BuddyBoss, PeepSo) and the
[member-blog-dashboard] shortcode. Both call DashboardService::tabs(), which builds the list and
passes it through bpmb_member_home_tabs. This filter replaces the separate dashboard tab filters
older versions had.
Built-in tabs and positions: Published posts (10), Drafts drafts (20), Pending pending (30),
Saved saved (60, when the reading list is on), Following following (70, when following is on). Pro adds Calendar (40), Analytics (50) and
Settings (90). Tabs are sorted by position.
Each tab is an array with label, icon (a name from the plugin’s icon set), position, an
optional count, and a render callable for any tab that is not one of the three post lists. The
callable receives the home owner’s user ID and must echo its output.
/** * A "Guidelines" tab for writers, on their own home only. * * @param array $tabs Tabs keyed by slug. * @param int $user_id Home owner. * @param array $context { owner: bool, writer: bool } for the current viewer. */add_filter( 'bpmb_member_home_tabs', function ( $tabs, $user_id, $context ) { if ( empty( $context['owner'] ) || empty( $context['writer'] ) ) { return $tabs; }
$tabs['guidelines'] = array( 'label' => __( 'Guidelines', 'my-plugin' ), 'icon' => 'file-text', 'position' => 80, 'render' => function ( $user_id ) { $page = get_page_by_path( 'writing-guidelines' );
if ( ! $page ) { echo bpmb_member_home()->empty_state( array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped inside. 'title' => __( 'No guidelines yet', 'my-plugin' ), ) ); return; }
echo apply_filters( 'the_content', $page->post_content ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped }, );
return $tabs;}, 10, 3 );To remove a tab, unset( $tabs['following'] ). A visitor to someone else’s home only ever sees
Published.
Add a control to the post list toolbar: bpmb_posts_toolbar
Section titled “Add a control to the post list toolbar: bpmb_posts_toolbar”Fires inside the toolbar above each post list, after the search box. It receives the list’s status:
publish, draft or pending. Pro’s category filter hooks here. Echo your markup.
add_action( 'bpmb_posts_toolbar', function ( $post_status ) { if ( 'publish' !== $post_status ) { return; }
printf( '<a class="bpmb-btn" href="/bp-family/buddypress-member-blog/developer-guide/%s/">%s</a>', esc_url( home_url( '/writing-guidelines/' ) ), esc_html__( 'Guidelines', 'my-plugin' ) );} );Show the dashboard heading: bpmb_dashboard_show_title
Section titled “Show the dashboard heading: bpmb_dashboard_show_title”When the dashboard is the page’s own content, it does not print its own heading, because the theme already prints the page title. A theme that hides page titles can bring it back.
/** * @param bool $show Default false when the dashboard is the page's own content. * @param int $user_id Dashboard owner. */add_filter( 'bpmb_dashboard_show_title', '__return_true' );PeepSo’s Blog Posts tab: bpmb_hide_peepso_blogposts_tab
Section titled “PeepSo’s Blog Posts tab: bpmb_hide_peepso_blogposts_tab”On PeepSo, the plugin hides PeepSo’s own Blog Posts profile tab so members see one blog, not two.
Return false to keep PeepSo’s tab.
add_filter( 'bpmb_hide_peepso_blogposts_tab', '__return_false' );Topics and following
Section titled “Topics and following”Let members follow tags: bpmb_followable_taxonomies
Section titled “Let members follow tags: bpmb_followable_taxonomies”Members can follow categories. Tags are left out by default because every member types their own, so a site soon has thousands. If your tags are curated, add them:
add_filter( 'bpmb_followable_taxonomies', function ( $taxonomies ) { $taxonomies[] = 'post_tag'; return $taxonomies;} );The same list decides which Follow buttons appear on topic hubs and which object_type=term follows
the REST API accepts.
Add a taxonomy to the topic hub: bpmb_topic_hub_taxonomies
Section titled “Add a taxonomy to the topic hub: bpmb_topic_hub_taxonomies”By default the topic hub takes over category and post_tag archives. Add a custom taxonomy:
add_filter( 'bpmb_topic_hub_taxonomies', function ( $taxonomies ) { $taxonomies[] = 'topic'; return $taxonomies;} );To hand one taxonomy back to the theme, use bpmb_takeover_term_archive. See
Templates and Overrides.
Cards and links
Section titled “Cards and links”A fallback card image: bpmb_default_post_image
Section titled “A fallback card image: bpmb_default_post_image”Posts without a featured image show no image on their card. Since 4.1.1 the default is empty; return a URL to bring back a fallback.
add_filter( 'bpmb_default_post_image', function () { return get_stylesheet_directory_uri() . '/images/post-placeholder.jpg';} );Change the writer URL: bpmb_writer_url
Section titled “Change the writer URL: bpmb_writer_url”bpmb_writer_url( $user_id ) is the one answer to “link to this writer”, used by every byline,
author card and REST payload.
/** * @param string $url The URL the plugin resolved. * @param int $user_id The writer. */add_filter( 'bpmb_writer_url', function ( $url, $user_id ) { $user = get_userdata( $user_id ); return $user ? home_url( '/w/' . $user->user_nicename . '/' ) : $url;}, 10, 2 );The plugin renders nothing at your new URL; build that page yourself, or keep the default and
restyle writer-profile.php instead (see Templates and Overrides).
Engagement limits
Section titled “Engagement limits”Cap claps: bpmb_max_claps
Section titled “Cap claps: bpmb_max_claps”A member may clap a post up to 50 times. The cap is enforced in one atomic SQL statement.
add_filter( 'bpmb_max_claps', function () { return 1; // One clap per member per post: a like button.} );The minimum is 1; returning 0 still allows one clap. The POST /reactions route also limits a single
request to 50 claps, separately from this per-member total.
Change who can feature a post: bpmb_feature_capability
Section titled “Change who can feature a post: bpmb_feature_capability”Featuring is an editorial act. The default capability is edit_others_posts. This one filter governs
the REST route, the Featured row action in wp-admin and anything Pro adds.
add_filter( 'bpmb_feature_capability', function () { return 'moderate_comments';} );Return a capability, not a role name.
Uploads
Section titled “Uploads”The editor’s image uploads read one policy from bpmb_media(). The site owner sets the allowed types
and size on the settings screen; these filters run last and win:
// Allow only JPEG and WebP from the editor.add_filter( 'bpmb_rest_upload_allowed_types', function ( $mimes ) { return array( 'image/jpeg', 'image/webp' );} );
// 2 MB limit, in bytes.add_filter( 'bpmb_rest_upload_max_size', function () { return 2 * MB_IN_BYTES;} );SVG is always removed from the allowlist. bpmb_allowed_image_mime_types and bpmb_max_upload_size
(in MB) run earlier in the same chain.
Pro hooks
Section titled “Pro hooks”Refusal icons: bpmb_denial_reason_icons
Section titled “Refusal icons: bpmb_denial_reason_icons”When Pro refuses a member (a role restriction, a post limit, no credits), each reason shows an icon.
The filter maps a reason type to an icon name; unknown types fall back to alert-circle.
add_filter( 'bpmb_denial_reason_icons', function ( $icons ) { $icons['credits'] = 'alert-circle'; return $icons;} );Default map: role => lock, limit => clock, credits => banknote.
When Pro’s assets load: buddypress_member_blog_pro_should_load_assets
Section titled “When Pro’s assets load: buddypress_member_blog_pro_should_load_assets”One gate decides whether Pro’s front-end CSS and JavaScript load on a request. Force them on for a page that renders Pro output the plugin cannot detect, such as a custom template:
/** * @param bool $load Whether the page renders something that uses them. * @param WP_Post|null $post The queried post, when there is one. * @param WP_Query|null $wp_query The main query. */add_filter( 'buddypress_member_blog_pro_should_load_assets', function ( $load, $post ) { return $load || ( $post && 'writers-lounge' === $post->post_name );}, 10, 2 );Posts published in the current limit period
Section titled “Posts published in the current limit period”bpmbp_get_posts_count_this_period( $period, $post_type = 'post', $user_id = 0 ) returns how many
posts a member has published in the current period, the same count Pro’s post limits use. $period
is hour, day, week, month or year, and periods are calendar periods in the site’s timezone
(this week, this month), not rolling windows.
$this_month = bpmbp_get_posts_count_this_period( 'month', 'post', get_current_user_id() );Removed in 4.3.0
Section titled “Removed in 4.3.0”| Removed | Use instead |
|---|---|
bpmbp_notification_subject, bpmbp_notification_content filters |
The shared email copy store (Pro Notification Emails tab) and the bpmb_email_subject / bpmb_email_body filters. See Emails. |
get_posts_count_from_last_hour(), _24h(), _week(), _month(), _year() |
bpmbp_get_posts_count_this_period( $period ). |
| The separate dashboard tab filters | bpmb_member_home_tabs. |
Extend the REST API
Section titled “Extend the REST API”Extend Wbcom\MemberBlog\Rest\RestController. You inherit the namespace, the permission callbacks
(can_read() and can_mutate(), including the Application Password refusal), the error shape
($this->error() prefixes codes with bpmb_) and the pagination helpers
(collection_params(), respond_collection()). Pro’s routes are built this way.
namespace My_Plugin\Rest;
use Wbcom\MemberBlog\Rest\RestController;use WP_REST_Server;
final class MyController extends RestController {
public function register_routes(): void { register_rest_route( self::NAMESPACE_V1, '/my-thing', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create' ), 'permission_callback' => array( $this, 'can_mutate' ), 'args' => array( 'post_id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), ), ), ) ); }
public function create( $request ) { $post_id = absint( $request->get_param( 'post_id' ) );
if ( ! bpmb_access()->can_edit( $post_id ) ) { return $this->error( 'cannot_edit', __( 'You cannot edit this post.', 'my-plugin' ), 403 ); }
return rest_ensure_response( array( 'ok' => true ) ); }}
add_action( 'rest_api_init', static function () { ( new \My_Plugin\Rest\MyController() )->register_routes();} );minimum and maximum on an argument are only enforced when the argument also declares
'validate_callback' => 'rest_validate_request_arg'.
Build an extension the way Pro does
Section titled “Build an extension the way Pro does”-
Gate platform-specific features before registering hooks. The plugin runs standalone and on BuddyPress, BuddyBoss and PeepSo. A feature that needs the activity stream should be absent where there is none. Ask
bpmb_module_requirements_met():add_action( 'init', function () {if ( ! function_exists( 'bpmb_module_requirements_met' ) ) {return;}// Tokens: members, profile_nav, activity, groups, notifications, friends, messages, xprofile.if ( ! bpmb_module_requirements_met( array( 'activity' ) ) ) {return;}add_action( 'bpmb_post_published', 'my_feature_post_to_activity', 10, 2 );} ); -
Render through the shared components.
bpmb_components()draws buttons, cards, badges, empty states and more;bpmb_post_cards()draws post cards. Your output then matches the plugin’s. -
Ask the services instead of re-deriving rules.
bpmb_access(),bpmb_submission(),bpmb_dashboard(). A second copy of a rule is how one entry point ends up allowing what another refuses.
Styling: the tokens are the API
Section titled “Styling: the tokens are the API”public/css/bpmb-front.css (handle bpmb-front) declares every --bpmb-* design token and the
reader components. public/css/bpmb-ui.css (handle bpmb-ui-helpers) is the interactive kit:
buttons, dialogs, skeletons and states. It depends on bpmb-front and declares no tokens. No other
stylesheet in Free or Pro declares a token.
Restyle by setting tokens, not by out-specifying selectors. The follow, clap and save buttons
(.bpmb-follow-btn, .bpmb-clap-btn, .bpmb-bookmark-btn) reset themselves with
all: unset !important so a theme’s blanket button rules cannot break their size or shape. Adding
your own !important rules to them is not supported; setting tokens is.
:root { --bpmb-primary: #7c3aed; --bpmb-primary-hover: #6d28d9; --bpmb-radius-md: 2px; --bpmb-space-md: 12px;}
/* One component only. */.bpmb-follow-btn { --bpmb-primary-filled: #111827;}Main tokens:
| Token | Default |
|---|---|
--bpmb-primary |
The theme’s accent (BuddyX, Reign, then --wp--preset--color--accent or --primary), falling back to #2563eb. |
--bpmb-primary-hover |
The theme’s hover colour. |
--bpmb-primary-filled |
The filled-button background: the accent darkened so white text stays readable. |
--bpmb-on-primary |
Text on the accent: white in light mode, dark in dark mode. |
--bpmb-space-xs to --bpmb-space-2xl |
4, 8, 16, 24, 32, 48px. |
--bpmb-radius-sm to --bpmb-radius-xl, --bpmb-radius-full |
4, 6, 10, 16px, and 100px. |
Status (--bpmb-success, --bpmb-danger, --bpmb-warning, --bpmb-info), text, surface, border,
typography, shadow and motion tokens are declared in the same :root block. There is no font-family
token; components inherit the site’s typeface.
Dark mode is the same token with a different value, overridden once at the root for
:root[data-bx-mode="dark"], :root[data-theme="dark"], html.dark, html.dark-mode,
body.dark-mode, body.dark-scheme and .bpmb-dark, and for :root[data-bx-mode="auto"] under
prefers-color-scheme: dark. If you add tokens for your own component, override them in the same
selectors.
RTL: each stylesheet has a generated -rtl.css sibling that WordPress loads on RTL locales. Write
with logical properties (margin-inline-start, padding-inline-end).
Data on uninstall
Section titled “Data on uninstall”Deleting the plugin always removes its own settings, schema versions, transients and cron events. Members’ data (reading lists, follows, claps, view history, writer statistics, series progress, and the plugin’s post and user meta) is removed only when the site owner ticks Also delete all Member Blog data in the Danger zone on the Content tab. One setting covers Free and Pro.
use Wbcom\MemberBlog\Core\Uninstaller;
Uninstaller::removes_data(); // true only when the owner opted in.Only the stored string 'yes' counts as consent.
Tables are never listed in the uninstaller. Each store owns its names and the uninstaller asks
ViewCountStore::tables() / ::options() and EngagementStore::tables() / ::options(). Add a table
to the relevant tables() method and it is dropped on removal. Pro does the same through
Buddypress_Member_Blog_Pro_Tables::all().
Each plugin removes only what it created. bpmb_series_progress is created by Free and written by
Pro, so Free drops it.

