Skip to content

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.

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.

There are two layers, and you almost always want the outer one.

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.

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.

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 );

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;
} );

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).

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' );

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.

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';
} );

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).

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.

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.

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 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 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'.

  1. 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 );
    } );
  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.

  3. 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.

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).

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.