A paid community is a different moderation problem than a blog’s comment section:
higher volume, higher stakes for a wrongly-flagged member, and content that doesn’t
always live in WordPress’s own wp_comments table. This lesson extends Course 6’s
classify-and-recommend pattern so it works across whatever forum or community plugin a
membership site actually runs, without ever auto-deleting a member’s post.
Course 6’s Project: Content Moderation Tool already built and working. A community or forum feature active on your membership site, whichever plugin provides it.
Step 1: Why one moderation log needs to cover several content types
Course 6’s project logs a recommendation to comment meta, which works because the
content being classified is always a WordPress comment. A membership site’s community
content often isn’t: bbPress stores topics and replies as their own post types,
BuddyPress activity updates aren’t posts at all, and a custom-built community feature
might store replies in its own table entirely. Rather than write a different logging
mechanism per plugin, this lesson uses one generic table keyed by a content type label
and an ID, so the same classifier and the same review screen work regardless of where
the content actually lives.
// File: community-moderator/class-moderation-log.php
function community_moderator_create_table() {
global $wpdb;
$table = $wpdb->prefix . 'community_moderation_log';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
content_type VARCHAR(32) NOT NULL,
content_id BIGINT UNSIGNED NOT NULL,
classification VARCHAR(32) NOT NULL,
reason TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending_review',
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY content_lookup (content_type, content_id)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
Step 2: Reuse Course 6’s classifier, tuned for community content
// File: community-moderator/class-moderation-log.php
function community_moderator_classify_content( string $content_type, int $content_id, string $author_name, string $content_text ) {
$prompt = <<<PROMPT
Classify this community post into exactly one category: "acceptable", "likely_spam", or "policy_violation".
This is a paid membership community, policy violations include harassment, hate speech,
and threats. Spam includes promotional links, repeated gibberish, or content unrelated
to the community's topic. Respond in exactly this format:
Classification: <category>
Reason: <one sentence>
Author: {$author_name}
Content: {$content_text}
PROMPT;
$response = wp_ai_client_prompt( $prompt )
->using_model_preference( 'anthropic/claude', 'openai/gpt' )
->generate_text();
if ( empty( $response ) ) {
return new WP_Error( 'empty_response', 'No classification returned, defaulting to manual review.' );
}
$classification = 'acceptable';
$reason = '';
if ( preg_match( '/Classification:\s*(\w+)/i', $response, $m ) ) {
$classification = strtolower( trim( $m[1] ) );
}
if ( preg_match( '/Reason:\s*(.+)/i', $response, $m ) ) {
$reason = trim( $m[1] );
}
if ( ! in_array( $classification, array( 'acceptable', 'likely_spam', 'policy_violation' ), true ) ) {
$classification = 'acceptable'; // Safest default, same discipline as Course 6.
}
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'community_moderation_log', array(
'content_type' => $content_type,
'content_id' => $content_id,
'classification' => $classification,
'reason' => $reason,
'status' => 'pending_review',
'created_at' => current_time( 'mysql' ),
) );
return array( 'classification' => $classification, 'reason' => $reason );
}
This is Course 6’s prompt structure with two changes: the framing explicitly names a “paid membership community” so the model weighs context accordingly, and the result writes to the content-type-agnostic log table from Step 1 instead of comment meta.
Step 3: Wire it to a real trigger
WordPress’s own comment_post action is the trigger Course 6 already uses, and it
applies directly if your community feature stores its replies as ordinary comments on a
custom post type, a common, real pattern for lightweight membership communities:
// File: community-moderator/class-moderation-log.php
add_action( 'comment_post', function ( $comment_id, $approved ) {
$comment = get_comment( $comment_id );
community_moderator_classify_content(
'comment',
$comment_id,
$comment->comment_author,
$comment->comment_content
);
}, 10, 2 );
If you’re running bbPress, BuddyPress, or another dedicated community plugin, that
plugin fires its own action when a topic, reply, or activity update is created. This
lesson doesn’t guess at those hook names or argument orders here, since they belong to
that plugin, not WordPress core, and they vary by plugin and version. Check that
plugin’s own hook reference, then call community_moderator_classify_content() from
inside it with the appropriate content_type label (‘forum_reply’, ‘activity_update’,
and so on) and the real content text that hook provides. The classifier and the log
table in Steps 1 and 2 don’t change, only the trigger does.
Step 4: A moderator review screen queries one table, regardless of source
// File: community-moderator/class-moderation-log.php
function community_moderator_get_pending(): array {
global $wpdb;
return $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}community_moderation_log WHERE status = 'pending_review' ORDER BY created_at DESC",
ARRAY_A
);
}
A moderator sees every flagged item across every content type in one list, and the actual moderation action, whatever “remove,” “warn,” or “dismiss” means for a given content type, is a separate, explicitly human-triggered function this lesson doesn’t call automatically, exactly Course 6’s discipline carried forward.
Test it
wp-env run cli wp comment create --comment_post_ID=1 --comment_content="Check out my crypto opportunity at totally-legit-crypto.example" --comment_author="Spammer"
wp-env run cli wp eval '
print_r( community_moderator_get_pending() );
'
Confirm the flagged comment appears with classification of likely_spam, status of
pending_review, and that no comment status changed as a side effect of running the
classifier.
A busy membership community generates far more flagged content per day than a typical blog’s comment section, which makes “just auto-remove the obvious ones” a tempting shortcut once a moderator’s queue starts backing up. Don’t. A wrongly-removed post in a paid community is a paying member’s contribution silently deleted, with no record they can point to and no easy way to know it happened. Scale the moderation queue and the people reviewing it, not the automation acting on it.
Recap
Community and forum content on a membership site rarely all lives in one storage shape,
so this lesson logs moderation recommendations to a generic, content-type-agnostic
table instead of comment meta alone. The classifier itself is Course 6’s pattern, framed
for a paid community’s specific risks, and it’s wired to real WordPress comment hooks
directly, with an explicit note to confirm any dedicated forum plugin’s own hook names
rather than guessing at them. Every flagged item stays pending_review until a human
acts on it, no matter which plugin the content came from. The final lesson recaps this
whole course.
Resources & further reading
- Project: Content Moderation Tool, Course 6
- comment_post action reference, developer.wordpress.org
- PHP AI Client SDK repository, GitHub