Templates and Overrides
Every template the plugin ships can be overridden from the active theme. Copy the plugin file into your theme, keep the filename, and edit your copy. Nothing in the plugin needs to change and the override survives updates.
There are two override directories, because there are two loaders. This is not a mistake and you need to know which one applies to the file you are overriding.
| Loader | Theme directory | Used by |
|---|---|---|
bp_member_blog_load_template( 'posts.php' ) |
yourtheme/bp-member-blog/ |
The 3.x profile templates: posts.php, draft-posts.php, pending-posts.php, edit.php. Directory name is filterable through bp_member_blog_template_dir. |
bpmb_get_template_part( 'parts/post-card' ) and the v4 archive takeovers |
yourtheme/buddypress-member-blog/ |
Everything added in 4.0.0: the template parts, writer-profile.php, topic-hub.php, and the PeepSo templates. |
Both check the child theme first, then the parent, then fall back to the plugin’s own
templates/ copy.
Template list
Section titled “Template list”Profile templates - override in yourtheme/bp-member-blog/
Section titled “Profile templates - override in yourtheme/bp-member-blog/”Loaded by bp_member_blog_load_template() from public/class-buddypress-member-blog-public.php.
| Plugin file | Rendered on |
|---|---|
templates/posts.php |
The member’s published posts list. |
templates/draft-posts.php |
The member’s drafts tab. |
templates/pending-posts.php |
The member’s pending tab. |
templates/edit.php |
The post submission and edit form. |
Example: to restyle the published list,
cp wp-content/plugins/buddypress-member-blog/templates/posts.php \ wp-content/themes/yourtheme/bp-member-blog/posts.phpTo move that directory somewhere else in your theme:
add_filter( 'bp_member_blog_template_dir', function () { return 'templates/member-blog'; // -> yourtheme/templates/member-blog/posts.php} );Template parts - override in yourtheme/buddypress-member-blog/
Section titled “Template parts - override in yourtheme/buddypress-member-blog/”Loaded by bpmb_get_template_part(). These are the parts that every surface shares: the profile
loop on page load, Pro’s category filter when it replaces that loop over AJAX, and the block. Because
all three render the same part, an override applies to all three.
| Plugin file | Notes |
|---|---|
templates/parts/post-card.php |
One post card. Must be called inside the loop with the global $post set. Fires bpmb_post_card_can_act ( bool $can_act, int $post_id ) to decide whether the Edit / Publish / Delete buttons show. |
templates/parts/empty-state.php |
The “no posts” state. Receives $user_id, $is_my_own, $is_filtered. $is_filtered is what lets the filtered case say “no posts match this filter” instead of the loop’s “hasn’t posted anything yet”, which after a filter is untrue. |
cp wp-content/plugins/buddypress-member-blog/templates/parts/post-card.php \ wp-content/themes/yourtheme/buddypress-member-blog/parts/post-card.phpTo rewrite the resolved path for a part in code rather than by copying a file:
add_filter( 'bpmb_template_part_path', function ( $located, $part, $args ) { if ( 'parts/post-card' === $part ) { return get_stylesheet_directory() . '/member-blog/cards/default.php'; }
return $located;}, 10, 3 );Archive templates - override in yourtheme/buddypress-member-blog/
Section titled “Archive templates - override in yourtheme/buddypress-member-blog/”| Plugin file | Rendered on |
|---|---|
templates/writer-profile.php |
/author/{nick}/ |
templates/topic-hub.php |
/category/{slug}/, /tag/{slug}/ |
Both are resolved by locate_template() before the plugin’s copy, so a theme can restyle them
without switching the takeover off. See the takeover section below.
PeepSo templates - override in yourtheme/buddypress-member-blog/peepso/
Section titled “PeepSo templates - override in yourtheme/buddypress-member-blog/peepso/”Loaded by load_peepso_template(). Same four surfaces as the profile templates, in PeepSo’s chrome.
| Plugin file |
|---|
templates/peepso/posts.php |
templates/peepso/draft-posts.php |
templates/peepso/pending-posts.php |
templates/peepso/edit.php |
cp wp-content/plugins/buddypress-member-blog/templates/peepso/posts.php \ wp-content/themes/yourtheme/buddypress-member-blog/peepso/posts.phpThe v4 archive takeovers
Section titled “The v4 archive takeovers”4.0.0 takes over two core archive URLs.
/author/{nick}/ becomes the writer profile
Section titled “/author/{nick}/ becomes the writer profile”src/Frontend/AuthorArchive.php filters template_include (priority 20) and swaps in
templates/writer-profile.php.
The author archive was chosen rather than minting a new /writers/{nick}/ URL because
/author/{nick}/ exists on every WordPress site whether the plugin uses it or not. A new URL would
compete with it rather than replace it: two pages for one person, splitting whatever ranking either
had. The plugin was already linking every post byline there; the bug was that nothing rendered a
writer at the destination.
With BuddyPress active both surfaces render, and the BuddyPress member tab stays canonical:
AuthorArchive::canonical() emits <link rel="canonical"> pointing at it from /author/{nick}/.
There is no redirect - a plugin does not get to 301 a core route out of existence, particularly one
that also serves posts the plugin knows nothing about.
Switching it off. The admin has a setting (Content -> Writer profiles, key
writer_profile_archive). The code-level escape hatch is:
/** * Disable the writer-profile takeover on /author/{nick}/. * * @param bool $enabled Defaults to the admin setting. * @param int $user_id The writer being viewed. */add_filter( 'bpmb_takeover_author_archive', '__return_false' );Return false and the theme’s author.php takes over again. Nothing else changes -
bpmb_writer_url() keeps pointing at the same URL either way.
Per-writer, if you only want the plugin’s profile for members of a given role:
add_filter( 'bpmb_takeover_author_archive', function ( $enabled, $user_id ) { return $enabled && user_can( $user_id, 'author' );}, 10, 2 );Hooks inside the template:
| Hook | Signature |
|---|---|
bpmb_writer_profile_per_page (filter) |
( int $per_page = 12, int $user_id ) |
bpmb_before_writer_profile (action) |
( int $user_id ) |
bpmb_after_writer_profile (action) |
( int $user_id ) |
Pagination comes off the main query’s paged var, so /author/jane/page/2/ already works.
/category/{slug}/ and /tag/{slug}/ become the topic hub
Section titled “/category/{slug}/ and /tag/{slug}/ become the topic hub”src/Frontend/TermArchive.php does the same for term archives.
This takeover is a bigger imposition than the author one and it is gated accordingly. A category
archive is a page themes invest in, sites rely on, and it lists posts this plugin knows nothing about
(a site’s own editorial content sitting alongside its members’). So it ships behind a real switch in
the admin (Content -> Topic hub, key topic_hub_archive), default on, and a filter for everyone
else.
/** * Disable the topic-hub takeover on term archives. * * @param bool $enabled Defaults to the admin setting. * @param \WP_Term $term The term being viewed. */add_filter( 'bpmb_takeover_term_archive', '__return_false' );Or keep the hub for categories and hand tags back to the theme:
add_filter( 'bpmb_takeover_term_archive', function ( $enabled, $term ) { return $enabled && 'category' === $term->taxonomy;}, 10, 2 );Which taxonomies get the hub is filterable. See the recipe in extending.md.
add_filter( 'bpmb_topic_hub_taxonomies', function ( $taxonomies ) { $taxonomies[] = 'bpmb_series'; return $taxonomies;} );Hooks inside the template:
| Hook | Signature |
|---|---|
bpmb_topic_hub_per_page (filter) |
( int $per_page = 12, \WP_Term $term ) |
bpmb_before_topic_hub (action) |
( \WP_Term $term ) |
bpmb_after_topic_hub (action) |
( \WP_Term $term ) |
Hooks in the profile templates
Section titled “Hooks in the profile templates”The 3.x templates carry their own extension points. These are frozen; Pro builds on them.
| Hook | Type | Fired in |
|---|---|---|
bp_member_blog_posts_per_page |
filter | posts.php, draft-posts.php, pending-posts.php |
bp_member_blog_posts_query_args |
filter, ( array $args, int $user_id, string $status ) |
the same three |
bp_member_blog_before_posts |
action, ( string $status ) |
the same three |
bp_post_before_title, bp_post_after_title |
action, ( int $post_id ) |
edit.php |
bp_post_before_content, bp_post_after_content |
action, ( int $post_id ) |
edit.php |
bp_post_before_category, bp_post_after_category |
action, ( int $post_id ) |
edit.php |
bp_post_before_tag, bp_post_after_tag |
action, ( int $post_id ) |
edit.php |
bp_post_before_featured_image, bp_post_after_featured_image |
action, ( int $post_id ) |
edit.php |
bp_post_before_seo, bp_post_after_seo |
action, ( int $post_id ) |
edit.php |
bp_post_after_publishing_options |
action, ( int $post_id ) |
edit.php |
bp_post_before_submit_button |
action, ( int $post_id ) |
edit.php |
Add a field to the submission form without touching edit.php at all:
add_action( 'bp_post_after_content', function ( $post_id ) { $value = $post_id ? get_post_meta( $post_id, '_my_subtitle', true ) : ''; ?> <div class="bp-member-blog-field"> <label for="my-subtitle"><?php esc_html_e( 'Subtitle', 'my-plugin' ); ?></label> <input type="text" id="my-subtitle" name="my_subtitle" value="<?php echo esc_attr( $value ); ?>" /> </div> <?php} );The full list, with parameters and the exact file and line each is fired from, is in the Hook Reference.
Rules for an override
Section titled “Rules for an override”- Do not draw your own post card markup. Draw through
bpmb_post_cards()(grid(),dashboard_card()) or throughbpmb_get_template_part( 'parts/post-card' ). Three hand-rolled copies of a card is exactly what 4.0.0 collapsed, and they had drifted in ways that reached users: different image sizes, different classes, and action buttons gated on the wrong capability. - Do not re-derive a rule. If your template needs to know whether the viewer may edit a post,
ask
bpmb_access()->can_edit( $post_id ). Do not re-implement the check. - Use
bpmb_writer_url( $user_id )for any link to a writer. Notget_author_posts_url(), not a hand-built BuddyPress path. There used to be three of those and they sent the same person to three different pages. - Keep the class names. Component geometry is enforced from
bpmb-ui.cssagainst.bpmb-follow-btn,.bpmb-clap-btnand.bpmb-bookmark-btn. Rename them in your override and you lose the armour that keeps them intact on a hostile theme. Restyle through the CSS custom properties instead - see the CSS contract. - Do not query. Templates receive data; they do not fetch it. Ask the service that owns the
data instead - the same rule the plugin now holds itself to. As of 4.0.0 Pro’s group listing
calls
Buddypress_Member_Blog_Pro_Groups::group_posts_query( $group_id ), which returns aWP_Querywith the member scope applied, thebpmb_group_posts_query_argsfilter fired and the author caches primed. Before that the template built the SQL itself, which meant every theme override silently inherited the membership query, the access rules deciding which post statuses a viewer may see, and the N+1 fix - and lost all three the moment ours changed.

