Skip to content

Hooks: Feed and Content

BuddyNext overviewFree + ProGet BuddyNext →

The action and filter seams for the activity feed and everything members post into it: posts, comments, reactions, polls, shares, bookmarks, and the composer template. This page is for developers building gamification plugins, moderation integrations, notification bridges, or theme extensions that react to feed activity or modify content before it is written. Every hook below is fired or applied by BuddyNext Free, so it is available without Pro. Several of them are the documented Free-to-Pro extension seams (buddynext_post_pin_limit, buddynext_reaction_types, buddynext_reaction_meta).

The activity feed whose post, comment, reaction, and composer hooks are documented on this page

  • Actions are notifications, not callbacks. BuddyNext fires actions after the database write has committed. Listeners that need the full record should re-fetch by ID (for example buddynext_service( 'post_service' )->get( $post_id )); the actions pass IDs, not hydrated rows.
  • *_before_save filters transform or reject. They run after the built-in safeguard checks and receive the candidate data array. Return a modified array to alter what is written, or return a WP_Error to reject the write outright. Only known columns are persisted; extra keys are ignored.
  • Actor vs recipient. For engagement that targets someone else’s content, BuddyNext fires an actor-perspective event (who did the thing) and a recipient-mirror alongside it (the content author). Recipient mirrors fire only when the actor is not the author. Gamification systems usually award the recipient.
  • Idempotent events fire once. Bookmarks, shares, and votes use INSERT IGNORE against a uniqueness key, and their actions fire only when a row was actually created or changed. Duplicate calls are silent no-ops.
  • Render and _part_* template hooks echo at the call site. Filters that return HTML (for example buddynext_composer_tools) default to an empty string, so BuddyNext renders nothing unless a plugin hooks. Hooked plugins must return escaped HTML.
Hook Type Fired when Parameters
buddynext_hide_dead_reshares filter A reshare is rendered whose original post is gone (deleted, hidden, blocked, or its author suspended). Default false: the card collapses to a tombstone so the sharer’s action stays legible and the feed does not silently lose posts. Return true to drop it from the feed entirely. bool $hide, array $post
buddynext_post_before_save filter A post is about to be written on create or update, after safeguard checks array $data, int $user_id, int|null $post_id ($post_id is null on create)
buddynext_feed_allowed_post_types filter The feed resolves which post types it will store and render, so an add-on can register its own card/post type instead of being dropped as unknown string[] $types (defaults to the built-in set)
buddynext_post_created action A new post goes live (also fired for a share, with $type = 'share') int $post_id, int $user_id, string $type
buddynext_post_updated action A post is edited int $post_id, int $user_id, array $fields (columns written this update)
buddynext_post_deleted action A post is deleted by its owner int $post_id, int $user_id
buddynext_purge_page_cache action A feed change purged one of its pages from the page caches BuddyNext knows (WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache). Hook it to purge the same URL from a cache BuddyNext does not know, such as Cloudflare APO or a proxy. Fires once per URL per request. string $url (absolute)
buddynext_link_preview_max_redirects filter Fetching a link preview, capping how many redirect hops are followed (default 5; every hop is re-validated against SSRF rules before it is followed) int $max, string $url
buddynext_post_approved action A held post is approved by a moderator int $post_id, int $author
buddynext_post_rejected action A held post is rejected by a moderator int $post_id, int $author, string $reason
buddynext_post_pin_limit filter A user pins a post, to read the per-scope pin cap int $limit, int|null $space_id, int $user_id (default 1)
buddynext_user_mentioned action An @username mention in a post (or comment) resolves to a real user int $mentioned_user_id, int $mentioner_id, int $post_id

Note: buddynext_post_created is the catch-all “a post exists now” event and fires for every post type, including shares. If you specifically want the share action with the original post ID, listen to buddynext_post_shared instead.

Hook Type Fired when Parameters
buddynext_comment_before_save filter A comment is about to be written on create or update array $data, int $user_id, int|null $comment_id ($comment_id is null on create; on update $data carries only content)
buddynext_comment_created action A new comment is created int $comment_id, string $object_type, int $object_id, int $user_id
buddynext_post_comment_received action The author’s post receives a comment from someone else (recipient mirror) int $comment_id, int $post_id, int $author_id, int $commenter_id
buddynext_comment_updated action A comment is edited int $comment_id, int $user_id
buddynext_comment_deleted action A comment is deleted int $comment_id, int $user_id

$object_type is the thing the comment is attached to ('post', 'media', etc.). The post-counter and recipient-mirror logic only run when $object_type === 'post'.

Hook Type Fired when Parameters
buddynext_reaction_added action A reaction is added to an object string $object_type, int $object_id, int $user_id, string $emoji
buddynext_post_reaction_received action The author’s post receives a reaction from someone else (recipient mirror) int $post_id, int $author_id, int $reactor_id, string $emoji
buddynext_reaction_removed action A reaction is removed from an object string $object_type, int $object_id, int $user_id, string $emoji
buddynext_reaction_types filter The allowed reaction-type list is resolved string[] $types (the owner-enabled subset of the canonical six)
buddynext_reaction_meta filter A reaction label/char/colour is resolved array $meta, string $slug ($meta has label, char, color)
buddynext_reactors_limit filter The reactors list for an object is queried int $max, string $object_type, int $object_id (default 100)

The canonical six reaction slugs are like, love, haha, wow, sad, angry. The site owner can disable a subset through Settings, and buddynext_reaction_types runs after that, so the list you receive is already the owner-enabled set. See the example below for adding a custom type.

Hook Type Fired when Parameters
buddynext_poll_vote_before_save filter A poll vote is about to be written array $data, int $user_id ($data has user_id, post_id, option_id)
buddynext_poll_voted action A poll vote is cast or switched (not on toggle-off) int $post_id, int $option_id, int $user_id
buddynext_share_before_save filter A share is about to be written array $data, int $user_id ($data has user_id, post_id, content)
buddynext_post_shared action A post is shared int $share_id, int $original_post_id, int $user_id
buddynext_post_bookmarked action A post is bookmarked (first time only) int $post_id, int $user_id
buddynext_post_unbookmarked action A bookmark is removed (only when a row was deleted) int $post_id, int $user_id

Note: buddynext_poll_voted does not fire when a member clicks the same option again to remove their vote (toggle-off returns early). A vote switch to a different option fires once, after the new vote is recorded.

Hook Type Fired when Parameters
buddynext_feed_query_args filter Before a feed query runs (home, profile, space, explore) array $args, string $scope, int $viewer_id ($args has per_page, cursor, user_id)
buddynext_feed_order_by filter The home feed ORDER BY clause is built string $order_by, int $user_id, array $query_args
buddynext_for_you_priority_days filter The For you feed is being ordered. Sets how many days a post by a connection, or in a space matching the member’s interests, stays above newer posts from everyone else. Return 0 for no limit. Since 1.2.5. int $days, int $user_id (default 7)
buddynext_feed_items filter A feed page is about to be returned array $items, string $scope, int $viewer_id, array $args
buddynext_post_impression action Each item on a served feed page is rendered int $post_id, int $viewer_id, string $surface (home_feed, profile_feed, space_feed, explore_feed)

Warning: buddynext_post_impression fires once per item per served page and can run dozens of times per request. Keep listeners cheap: defer counting work to Action Scheduler rather than writing to the database inside the hook.

The composer partial exposes wrapper hooks for adding tools and modals to the post box. These are template-level and run wherever the composer is rendered.

Hook Type Fired when Parameters
buddynext_part_composer_args filter The composer args are assembled array $args (user_id, space_id, and render flags)
buddynext_part_composer_before action Immediately before the composer markup array $args
buddynext_composer_tools filter The composer toolbar row is rendered string $html, array $args (return escaped HTML to append a tool button)
buddynext_composer_modals action After the composer, where tool modals belong array $args
buddynext_part_composer_after action Immediately after the composer markup array $args
buddynext_comment_descendant_cap filter One page of a comment thread is loaded, bounding how many descendant rows come with it int $cap, string $object_type, int $object_id
buddynext_explore_all_deck filter The blended Explore first-page deck is assembled, so an add-on can inject its own cards among the posts array $items, array $post_cards
buddynext_explore_excluded_post_types filter Explore’s deck and its pulse count are queried. Post types listed here are left off. Default array( 'document' ): a document post stays in its space feed, the space Files tab and the author’s profile. Return array() to show documents on Explore. Reshares are always left off and cannot be re-added (1.2.2) string[] $types
buddynext_post_link_meta_resolved action A queued link preview finished resolving and was stored on the post. The post was already visible without it, so anything that renders the preview should refresh here int $post_id, array $meta ($meta has title, description, thumbnail)
buddynext_scheduled_post_published action A scheduled post is published ahead of its schedule int $post_id

Note: Every feed template partial (post-actions, post-body, post-byline, post-comment-form, and so on) also exposes the standard buddynext_part_<name>_before/_after actions and buddynext_part_<name>_args/_classes filters. They follow the same convention as the composer hooks and are the safe seams for theme overlays. See Hooks: Overview for the template-part naming contract.

Narrower hooks around announcements, members-only posts, the feed templates, and avatar and cover storage. Same table shape as above.

Posts, announcements, and members-only content

Section titled “Posts, announcements, and members-only content”
Hook Type Fired when Parameters
buddynext_can_create_announcement filter A user tries to create an announcement. Default: a site admin, or a moderator of the target space. bool $can_announce, int $user_id, int $space_id ($space_id is 0 for a site-wide announcement)
buddynext_announcement_published action An announcement goes live, after the cached home feeds have been flushed so it shows on screens now. int $post_id, int $user_id, int $space_id (0 = site-wide)
buddynext_post_publicly_shareable filter BuddyNext decides whether a post may be shared outside the community (the share modal and public link). bool $shareable, array $post
buddynext_members_only_has_access filter A members-only post is rendered for a viewer who is not its author. Default: any logged-in viewer has access. Return false to lock it for plan-limited members. bool $has_access, int $viewer_id, array $post_ctx (id, user_id, space_id, members_only)
buddynext_members_only_cta filter The locked card for a members-only post builds its call to action. Default: a “Log in to view” link for guests, nothing for logged-in members. array $cta, int $viewer_id, array $post_ctx ($cta has label and url)
buddynext_members_only_teaser_fraction filter The teaser shown on a locked members-only post is cut. The default comes from the teaser-percent setting and is capped at 0.95 so something is always withheld. float $fraction
buddynext_render_post_body_{type} filter A post card renders the body of a post whose type is {type} (for example media, document, badge). Return the pre-escaped card HTML for that type; an empty string falls through to the default body. string $html, array $args ($args has the post, id, link meta, and content)
buddynext_scheduled_publish_failed action A scheduled post is abandoned after repeated publish failures and returned to draft. int $post_id, int $attempts
buddynext_site_tracking_publish filter A published WordPress post is about to post an activity card (site tracking). Runs last, so it has the final say per post. bool $publish, WP_Post $post, int $author_id
buddynext_partner_card_requires_verification filter A partner plugin’s activity card (a forum topic, an event, a course) is being saved for an author who has not verified their email. Return true to hold it, as a member’s own post is held. bool $required, int $user_id, array $data (default false)
buddynext_sync_blog_comments filter Comments on a published WordPress post and on its feed card are mirrored. Return false to stop the mirroring. bool $enabled (default true)
buddynext_post_preview_char_limit filter A post card decides how many characters show before “Show more” (default 300). int $limit, string $context (feed context the card renders in)
buddynext_post_preview_line_limit filter A post card decides how many line breaks show before “Show more” (default 6). int $limit, string $context
buddynext_bookmark_list_cap filter The “all my saved post ids” list is read (used by the native app). Caps the size of the returned set. int $cap, int $user_id
buddynext_reaction_choices filter The Engagement settings build the list of reactions the owner can switch on: the built-in six plus any an extension adds. buddynext_reaction_types then receives the owner’s pick. string[] $types
buddynext_feed_viewer_state_primed action Core viewer state has been primed for a page of feed items, so an add-on can batch-load its own per-author data in one query. array $items, int $viewer_id

Runnable snippet: letting another membership plugin decide who reads members-only posts (buddynext_members_only_has_access, buddynext_members_only_cta) is in buddynext/buddynext-snippets at membership/unlock-members-only-posts-with-another-plugin.php. Its header says what to change and how it was checked.

Hook Type Fired when Parameters
buddynext_feed_home_before / buddynext_feed_home_after action Before and after the home feed’s inner content. int $current_user_id
buddynext_feed_explore_before / buddynext_feed_explore_after action Before and after the Explore feed’s inner content. int $current_user_id
buddynext_explore_grid_cards action After the Explore grid cards on first paint. Pro appends its own cards here. string $explore_filter, int $current_user_id
buddynext_can_view_explore filter The Explore deck is about to be built for a viewer. Return false to withhold it (Pro does this for a plan without the Explore entitlement). bool $can, int $current_user_id
buddynext_explore_locked_title filter The title of the empty state shown when buddynext_can_view_explore returned false. string $title, int $current_user_id
buddynext_explore_locked_body filter The body text of that same empty state. string $body, int $current_user_id
buddynext_bookmarks_before / buddynext_bookmarks_after action Before and after the bookmarks hub renders. int $current_user_id
buddynext_single_post_thread action After the single-post card, so a bridge can render an alternative thread UI. int $post_id, array $post
buddynext_feed_client_pagination filter A feed, Explore, or bookmarks template decides whether “Load more” swaps in place through the client router. Return false to fall back to a plain link. bool $enabled (default true)
buddynext_feed_new_count_interval filter The home feed sets how often the “N new posts” pill polls, in seconds (default 60). int $seconds
buddynext_share_networks filter The share modal builds the list of networks for sharing a public post. Return an empty array to offer only “Copy link”. array $networks (key => icon, label, url template with {url} and {text})
Hook Type Fired when Parameters
buddynext_image_stored action An avatar or cover and all its variations have been written to disk. A CDN or offload integration pushes the files from here. string $kind, string $owner, int $id, array $written ($kind is avatar or cover, $owner is user or space, $written maps variation to absolute path)
buddynext_image_deleted action Just before an owner’s image folder is removed, so a CDN integration can purge its remote copies. string $kind, string $owner, int $id, string $dir
buddynext_stored_image_url filter The public URL of a stored avatar or cover is resolved. Rewrite it onto a CDN host here. string $url, string $kind, string $owner, int $id
buddynext_replaced_image_grace filter An avatar or cover is being replaced. Sets how long the old file stays on disk (so pages held in a page cache still show an image) before it is removed. Return 0 to remove it at once. int $seconds, string $kind, string $owner, int $id (default one day)
buddynext_feed_image_variant filter A feed card or link preview is choosing which MediaVerse image size to show. Return thumb_medium for lighter pages; the single-image 2x source (the full original) is then left out too. The lightbox still opens the full image. Since 1.2.5. string $variant (default thumb_large; also thumb_medium, thumb_thumb)
buddynext_trending_topics_hours filter The Trending Topics sidebar card is being built. Sets how far back it looks; a slower community can widen it. int $hours (default 168, seven days)
buddynext_announce_space_album_upload filter Photos are added to a space album. Return false to stop the upload posting an activity card to that space. bool $announce, int $space_id, int $album_id
buddynext_media_max_bytes filter The composer and Media tab compute the upload ceiling they offer (the lower of the MediaVerse limit and the server limit). int $max_bytes, int $user_id

buddynext_reaction_types adds the slug; buddynext_reaction_meta supplies its label (and optional emoji char / colour). The slug must be lowercase alphanumeric, and for a complete UI it needs an icon at assets/icons/reaction-{slug}.svg and a --bn-reaction-{slug} colour token. Adding the slug without the icon and CSS leaves the picker broken, so ship both.

// 1. Register the slug.
add_filter( 'buddynext_reaction_types', function ( array $types ): array {
$types[] = 'celebrate';
return $types;
} );
// 2. Give it a human label (and optionally a fallback emoji char + colour).
add_filter( 'buddynext_reaction_meta', function ( array $meta, string $slug ): array {
if ( 'celebrate' === $slug ) {
$meta['label'] = __( 'Celebrate', 'my-addon' );
$meta['char'] = '🎉';
$meta['color'] = '#f5a623';
}
return $meta;
}, 10, 2 );

Free allows one pinned post per profile (pinning is profile-only). Return a higher integer to lift the cap. $space_id is always null.

add_filter( 'buddynext_post_pin_limit', function ( int $limit, ?int $space_id, int $user_id ): int {
// Members in the 'premium' role may pin up to five posts per scope.
if ( user_can( $user_id, 'bn_premium_member' ) ) {
return 5;
}
return $limit;
}, 10, 3 );

Award the post author when their post is reacted to

Section titled “Award the post author when their post is reacted to”

Listen to the recipient mirror, not the actor event, so you award the author rather than the reactor.

add_action( 'buddynext_post_reaction_received', function ( int $post_id, int $author_id, int $reactor_id, string $emoji ): void {
my_gamification_award_points( $author_id, 5, 'reaction_received' );
}, 10, 4 );

Return a WP_Error from buddynext_post_before_save to block the write. The error surfaces in the REST response body.

add_filter( 'buddynext_post_before_save', function ( $data, int $user_id, ?int $post_id ) {
if ( null === $post_id && str_contains( (string) ( $data['content'] ?? '' ), 'banned-phrase' ) ) {
return new WP_Error( 'blocked', __( 'That phrase is not allowed.', 'my-addon' ), [ 'status' => 422 ] );
}
return $data;
}, 10, 3 );
  • Free vs Pro. Every hook on this page is fired by Free. The Pro-facing extension seams are buddynext_post_pin_limit (raise pin caps for plans), buddynext_reaction_types / buddynext_reaction_meta (add reaction types), and the buddynext_feed_query_args / buddynext_feed_order_by / buddynext_feed_items trio (plan-based feed filtering, reranking, sponsored injection).
  • Re-fetch, do not assume. Action listeners receive IDs. Hydrate via the relevant service (post_service, comment_service) instead of caching the data you wish had been passed.
  • buddynext_post_created fires for shares too. Branch on $type if you only want native posts; use buddynext_post_shared for the share-specific signal with the original post ID.
  • Counter writes already happen. BuddyNext updates reaction_count, comment_count, and share_count itself before firing these actions. Do not re-increment denormalised counters from a listener.