Skip to content

Developer Guide

WB Member Blog 4.0.0 is a plugin pair: WB Member Blog (Free) and WB Member Blog Pro.

The rule that governs the pair is worth stating before anything else, because everything in these pages follows from it:

Free is the platform. Pro is an extension built entirely on Free’s public hooks.

Pro does not reach into Free’s internals. It registers on the same actions and filters that this guide documents, calls the same services through the same public accessors, and renders through the same component factory. That is not a stylistic preference, it is a hard boundary: it means anything Pro does to Free, your code can do too. Restricting who may post, adding a dashboard tab, changing what a post becomes on submit, adding a surface to the writer profile - Pro does all of it through hooks that are listed in the Hook Reference, and so can you.

If you find yourself needing to patch a Free class to build something, that is a bug in the hook surface. File it.

Page What it covers
Hook Reference Every action and filter fired by Free and by Pro, with parameters and the file that fires it. Generated from source - do not hand-edit.
REST API Reference Every route, its parameters, its permission callback, and the auth it demands.
Templates and Overrides How a theme overrides a plugin template, the template list, and the v4 archive takeovers.
Extending the Plugin Copy-pasteable recipes: restrict posting, force moderation, add a dashboard tab, restyle the components.

The hook reference is regenerated from the source docblocks:

Terminal window
php bin/generate-hooks-reference.php # rebuild hooks-reference.md
php bin/generate-hooks-reference.php --undocumented # list hooks still missing a docblock

If a hook has no description in that file, the fix is to add a docblock above the do_action() / apply_filters() call in the source and regenerate. Do not write the description into the markdown.

The plugin runs two code layers side by side. This is deliberate and it is not a migration that stalled.

New code lives here. It is autoloaded by src/Core/Autoloader.php, a dependency-free PSR-4 loader registered from the main plugin file’s scope (not from the boot sequence - Pro’s main file is parsed before Free’s, so the map has to exist the moment Free is parsed). There is no Composer vendor directory and no build step.

Directory Responsibility
src/Core/ Autoloader (live, registered from the main file’s scope), plus the module-gating contract: Module_Requirements, ServiceProvider and Registry. The gate that is actually driven today is the helper bpmb_module_requirements_met(), which resolves platform capability tokens against Member_Blog_Compat::supports(). A feature whose requirements are unmet must register no hooks at all, so it is absent rather than degraded on a site without BuddyPress.
src/Services/ The rules, transport-free. PostSubmissionService, DashboardService, PostQuery, FollowService, BookmarkService, ReactionService, FeaturedService, MediaService, ViewTracking, WriterProfile, DigestService, EmailDispatcher, and others. No $_POST, no redirects, no markup.
src/Rendering/ The markup. PostCardRenderer, WriterProfileRenderer, TopicHubRenderer, FollowRenderer, BookmarkRenderer and ReactionRenderer are each self-contained final classes — they compose fragments from PostCardRenderer rather than inheriting a base. Components is the shared component factory offered to extenders via bpmb_components(); it has no first-party caller and exists as a published extension surface. Every method returns a string; nothing echoes.
src/Rest/ The REST controllers, plus RestGate, which makes core’s POST /wp/v2/posts obey the plugin’s own submission rules. See rest-api.md.
src/Frontend/ The v4 archive takeovers: AuthorArchive (/author/{nick}/) and TermArchive (/category/{slug}/, /tag/{slug}/).
src/Support/ Small shared primitives: WriterUrl, EngagementStore, ViewCountStore, MetaLock.
src/Admin/ FeaturedAdmin (the Featured column and row action on the posts list) and ModerationAdmin (the review queue).
src/Blocks/ BlockRegistry derives a block’s attribute schema and its render_callback from the shortcode it wraps, so a block and its shortcode cannot disagree.

includes/, admin/, public/ - legacy, still load-bearing

Section titled “includes/, admin/, public/ - legacy, still load-bearing”

The 3.x procedural and WPPB-style code is still here and still runs. includes/class-bp-member-blog-access-control.php is the access gate the whole pair asks. public/class-buddypress-member-blog-public.php owns the submission form, the dashboard shortcode, the AJAX handlers and the image-upload REST routes. includes/buddypress-member-blog-functions.php is the public function surface and the wiring file: it declares every service accessor and boots the src/ services.

Do not port things out of includes/ opportunistically. The boundary moves in planned phases.

Everything in src/Services is reachable through a one-line accessor declared in includes/buddypress-member-blog-functions.php. These are the supported entry points. Call these, not new \Wbcom\MemberBlog\Services\Whatever().

Accessor Returns
bpmb_access() The access-control singleton: can_create(), can_edit(), can_publish(), can_delete(), can_manage_categories().
bpmb_submission() PostSubmissionService: resolve_status(), sanitize_content(), enforce_excluded_categories().
bpmb_dashboard() DashboardService: get_posts(), edit_url(), dashboard_url().
bpmb_post_query() PostQuery: paginate(), by_author(), related(), featured(), find_many().
bpmb_post_cards() PostCardRenderer: grid(), dashboard_card(), ajax_pagination().
bpmb_components() Components, the shared component factory. No first-party caller — every method is here for extenders, so a third-party renderer can draw a button, card or badge that matches ours instead of inventing a lookalike.
bpmb_bookmarks(), bpmb_follows(), bpmb_reactions() The engagement services.
bpmb_featured() FeaturedService: feature(), unfeature(), is_featured(), user_can_feature().
bpmb_media() MediaService: allowed_mime_types(), max_upload_bytes().
bpmb_views() ViewTracking: totals(), author_total().
bpmb_writer_url( $user_id ) The canonical writer URL. Use this, never get_author_posts_url().
bpmb_writer_profile(), bpmb_writer_profile_renderer() The writer profile service and its renderer.
bpmb_notification_prefs() NotificationPrefs.
bpmb_registry() The shared Registry. Pro adds its providers to this same instance.
Wbcom\MemberBlog\Core\Uninstaller The removal policy: removes_data(), REMOVE_DATA_SETTING, SETTINGS_OPTION. Static; there is no accessor. See extending.md.
bpmb_mail() The EmailDispatcher class name, for static calls. One recipient goes inline; many are queued and drained on cron.

The reason the services are transport-free is that three front doors reach the same rules: the classic form POST, the REST API, and a block or mobile client. Where a rule is copied instead of called, the doors disagree - a post goes live over REST while an identical submission sits in moderation behind the form. RestGate exists precisely because that happened.

So when you extend: call the service, or filter the hook the service fires. Do not re-derive the rule in your own code.

No custom post types. Member posts are ordinary post objects.

Custom tables (installed by src/Support/EngagementStore.php and src/Support/ViewCountStore.php) back follows, claps and view counts. Do not query them directly - bpmb_follows(), bpmb_reactions() and bpmb_views() are the read and write path, and they do the atomic capping and the cache invalidation.

Settings live in one option, bp_member_blog_gen_stngs. Read it through bp_member_blog_get_settings( $key, $default ).