Skip to content

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.

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

To 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.php

To 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.php

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 )

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.

  1. Do not draw your own post card markup. Draw through bpmb_post_cards() (grid(), dashboard_card()) or through bpmb_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.
  2. 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.
  3. Use bpmb_writer_url( $user_id ) for any link to a writer. Not get_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.
  4. Keep the class names. Component geometry is enforced from bpmb-ui.css against .bpmb-follow-btn, .bpmb-clap-btn and .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.
  5. 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 a WP_Query with the member scope applied, the bpmb_group_posts_query_args filter 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.