Skip to content
BuddyPress

AI Thread Summarization for WordPress Forums: TL;DR for Every Long Discussion

· · 8 min read

Discourse shipped AI thread summarization in 2024 as part of their AI plugin. Vanilla AI includes it. Khoros Aurora has it. The pattern is consistent: long threads get a machine-generated TL;DR pinned at the top, saving new readers from scrolling through 80 replies to find the answer.

WordPress with Jetonomy does not have this feature out of the box. But it takes about 60 lines of PHP to add it. This tutorial covers the full implementation: cron-triggered summarization using OpenAI’s GPT-4o-mini model, a post-meta cache strategy that avoids redundant API calls, display in a styled widget above the first reply, trust-level gating, and the hallucination guard you need before shipping this to users.

Why Thread Summarization Matters

A forum thread with 50 or more replies has a usability problem. The question is at the top. The accepted answer might be reply 23. The context that makes the answer make sense is scattered across replies 4, 11, and 31. A new reader who arrives via Google has to either scroll through everything or give up and leave.

A three-to-four sentence AI summary at the top of a long thread solves this. It tells the reader what problem the thread is about, what solutions were proposed, which one worked, and any important caveats. The reader can decide in 10 seconds whether to read the full thread or go straight to the accepted answer.

This is also an SEO signal. Threads with high dwell time rank better. A summary that helps readers quickly confirm they are in the right place increases the probability that they stay and read further rather than bouncing back to Google.

What You Need

  • WordPress with Jetonomy active
  • OpenAI API key with access to gpt-4o-mini
  • The API key stored as a constant in wp-config.php (same pattern as the AI moderation guide)
  • PHP 8.0+ for named arguments and match expressions used in the code below

GPT-4o-mini is the right model here. It is fast, accurate for summarization tasks, and costs roughly $0.15 per million input tokens. A forum thread with 50 replies and 200 words each is about 10,000 tokens. That is $0.0015 per summary, effectively free at community scale.

The Core Summarization Function

Create wp-content/mu-plugins/jetonomy-ai-summary.php:

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Generate an AI summary for a Jetonomy forum thread.
 * Returns the summary string or empty string on failure.
 */
function jet_ai_summarize_thread( int $topic_id ): string {
    if ( ! defined( 'OPENAI_API_KEY' ) ) return '';

    // Collect the topic post and all replies
    $topic = get_post( $topic_id );
    if ( ! $topic ) return '';

    $replies = get_posts( [
        'post_type'      => 'jet_reply',   // adjust to your post type slug
        'post_parent'    => $topic_id,
        'posts_per_page' => 80,            // cap at 80 to stay within token budget
        'orderby'        => 'date',
        'order'          => 'ASC',
        'post_status'    => 'publish',
    ] );

    // Build conversation text
    $text  = "QUESTION: " . wp_strip_all_tags( $topic->post_title ) . "\n";
    $text .= wp_strip_all_tags( $topic->post_content ) . "\n\n";
    foreach ( $replies as $reply ) {
        $author = get_userdata( $reply->post_author );
        $name   = $author ? $author->display_name : 'Member';
        $text  .= "REPLY by {$name}: " . wp_strip_all_tags( $reply->post_content ) . "\n";
    }

    // Trim to ~12,000 tokens worth of characters (~48,000 chars)
    $text = mb_substr( $text, 0, 48000 );

    $response = wp_remote_post(
        'https://api.openai.com/v1/chat/completions',
        [
            'timeout' => 30,
            'headers' => [
                'Authorization' => 'Bearer ' . OPENAI_API_KEY,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode( [
                'model'      => 'gpt-4o-mini',
                'max_tokens' => 150,
                'messages'   => [
                    [
                        'role'    => 'system',
                        'content' => 'You summarize forum threads in 2-4 plain sentences. State the problem, the solution(s) discussed, and which one was confirmed to work. Be factual. If no solution was confirmed, say so. Do not invent information not in the thread.',
                    ],
                    [
                        'role'    => 'user',
                        'content' => $text,
                    ],
                ],
            ] ),
        ]
    );

    if ( is_wp_error( $response ) ) return '';

    $body    = json_decode( wp_remote_retrieve_body( $response ), true );
    $summary = $body['choices'][0]['message']['content'] ?? '';

    return sanitize_textarea_field( trim( $summary ) );
}

A few implementation notes. The reply cap at 80 keeps the token budget predictable. Threads longer than 80 replies get summarized on the first 80. This is acceptable because the most important context is usually in the early replies. The 48,000 character trim is a safety net for very long individual replies. The system prompt explicitly instructs the model not to invent information, this is the primary hallucination guard.

The Cache Strategy

Calling the API on every page load would be wasteful and slow. The right pattern is: generate the summary once, cache it in post meta, and regenerate it periodically for active threads or on demand when a moderator requests a refresh.

/**
 * Get the cached summary for a thread, generating it if absent or stale.
 */
function jet_get_thread_summary( int $topic_id ): string {
    $cached_at = (int) get_post_meta( $topic_id, '_ai_summary_generated_at', true );
    $summary   = get_post_meta( $topic_id, '_ai_summary', true );

    // Regenerate if: no summary yet, or older than 24 hours and thread has new replies
    $is_stale = $cached_at < ( time() - DAY_IN_SECONDS );
    $reply_count = get_post_meta( $topic_id, '_ai_summary_reply_count', true );
    $current_replies = (int) get_comments_number( $topic_id ); // adjust for Jetonomy

    if ( empty( $summary ) || ( $is_stale && (int) $reply_count !== $current_replies ) ) {
        $summary = jet_ai_summarize_thread( $topic_id );
        if ( ! empty( $summary ) ) {
            update_post_meta( $topic_id, '_ai_summary', $summary );
            update_post_meta( $topic_id, '_ai_summary_generated_at', time() );
            update_post_meta( $topic_id, '_ai_summary_reply_count', $current_replies );
        }
    }

    return (string) $summary;
}

The stale check is intentionally conservative. A summary is only regenerated if it is more than 24 hours old AND the reply count has changed. A thread with no new activity keeps its existing summary indefinitely. This keeps API costs near zero for inactive threads while keeping active threads current.

Cron-Triggered Background Generation

For high-traffic forums, generating summaries synchronously on page load adds latency. A better approach is to queue summarization jobs via WP-Cron and serve the cached summary (or nothing) while the job runs in the background.

// Schedule background summarization for threads over the reply threshold
add_action( 'save_post', function( int $post_id, WP_Post $post ) {
    // Only for Jetonomy reply post type saves (adjust slug as needed)
    if ( 'jet_reply' !== $post->post_type ) return;

    $topic_id = $post->post_parent;
    if ( ! $topic_id ) return;

    // Count replies, only summarize threads with 5+ replies
    $reply_count = count( get_posts( [
        'post_type'      => 'jet_reply',
        'post_parent'    => $topic_id,
        'posts_per_page' => -1,
        'fields'         => 'ids',
    ] ) );

    if ( $reply_count >= 5 ) {
        // Schedule a one-off event 60 seconds from now (avoids hammering on rapid replies)
        if ( ! wp_next_scheduled( 'jet_generate_summary', [ $topic_id ] ) ) {
            wp_schedule_single_event( time() + 60, 'jet_generate_summary', [ $topic_id ] );
        }
    }
}, 10, 2 );

add_action( 'jet_generate_summary', function( int $topic_id ) {
    jet_get_thread_summary( $topic_id ); // This will generate and cache if needed
} );

The 60-second delay prevents the API from being called on every rapid-fire reply during an active discussion. Only one scheduled event can exist for a given topic ID at a time, so multiple replies in quick succession result in one summarization job, not many.

Displaying the Summary Widget

Hook the display into Jetonomy's single-thread template. The hook name depends on your Jetonomy version and theme, look for a jet_before_replies or jet_topic_header action. Alternatively, add it via a template override.

add_action( 'jet_before_replies', function() {
    $topic_id = get_the_ID();
    $summary  = get_post_meta( $topic_id, '_ai_summary', true );

    // Only show for threads with at least 5 replies
    $reply_count = (int) get_post_meta( $topic_id, '_ai_summary_reply_count', true );
    if ( empty( $summary ) || $reply_count < 5 ) return;

    echo '<div class="jet-ai-summary" role="note" aria-label="AI-generated thread summary">';
    echo '<div class="jet-ai-summary__label">TL;DR &mdash; AI summary of ' . esc_html( $reply_count ) . ' replies</div>';
    echo '<p class="jet-ai-summary__text">' . esc_html( $summary ) . '</p>';
    echo '<div class="jet-ai-summary__disclaimer">Generated by AI. May not capture every detail. Read the full thread for complete context.</div>';
    echo '</div>';
} );

The disclaimer is not optional. It is the second hallucination guard. Even with a well-crafted system prompt, language models occasionally misattribute solutions or miss nuance. The disclaimer sets reader expectations and protects you legally if a summary leads someone to apply an incorrect fix.

Trust-Level Gating

You may want to show AI summaries only to members at a certain trust level, either as a perk of higher membership or as a way to limit API costs while the feature is being tested. Add a trust-level check to the display hook:

// Check trust level before showing summary
// Assumes your trust level is stored as user meta '_trust_level' (integer 0-4)
function jet_user_can_see_summary(): bool {
    if ( ! is_user_logged_in() ) return false;
    $trust_level = (int) get_user_meta( get_current_user_id(), '_trust_level', true );
    return $trust_level >= 1; // Level 1+ can see summaries
}

Wrap the display hook output in if ( jet_user_can_see_summary() ). New members at Level 0 do not see summaries and are incentivized to participate enough to reach Level 1. This creates a small engagement nudge that also limits API exposure to unverified new accounts.

CSS Styling for the Summary Widget

Add to your theme's stylesheet or a custom CSS block:

.jet-ai-summary {
    background: #f9fafb;
    border: 1px solid #e5e7eb;
    border-left: 3px solid #c2410c;
    border-radius: 8px;
    padding: 14px 18px;
    margin-bottom: 24px;
}

.jet-ai-summary__label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: #6b7280;
    margin-bottom: 8px;
}

.jet-ai-summary__text {
    font-size: 14px;
    line-height: 1.65;
    color: #374151;
    margin: 0 0 10px;
}

.jet-ai-summary__disclaimer {
    font-size: 11px;
    color: #9ca3af;
    font-style: italic;
}

Adjust the border color to match your community theme. The left border accent makes the widget visually distinct from regular thread content without being intrusive.

When AI Summaries Go Wrong

Language models hallucinate. They are particularly prone to making plausible-sounding errors on technical content, inverting a solution, attributing a fix to the wrong user, or omitting a critical caveat. There are four things that limit this risk in the implementation above.

The system prompt explicitly instructs the model to be factual and to acknowledge when no solution was confirmed. This does not eliminate hallucination but it reduces the model's tendency to fill gaps with invented content.

The reply cap at 80 means the model always has the full early context. Summaries that are based on truncated threads are more error-prone; capping at 80 replies rather than at a character limit ensures the model reads whole replies rather than cut-off fragments.

The disclaimer on every summary sets reader expectations. No reader should take a summary as authoritative without checking the actual thread.

Finally, moderators at trust Level 3 or higher should have an admin link to delete or regenerate a summary they know is wrong. Add a nonce-protected handler that clears the _ai_summary meta and schedules a fresh generation job.

Cost Estimate at Scale

Monthly Active ThreadsAvg RepliesSummaries GeneratedApprox. API Cost
10020~100/month<$0.05
1,00025~800/month~$0.20
10,00030~6,000/month~$1.50
100,00035~50,000/month~$12

The stale-while-revalidate cache strategy is what keeps costs this low. A thread that gets no new replies after its first summarization never triggers another API call. Only active threads regenerate, and only once per 24-hour window with new activity.

What This Does for Your Community

Thread summarization is one of the features that makes a forum feel modern rather than dated. Users who arrive from search and find a clear TL;DR are more likely to stay, read the full thread, and come back. Users who return to a thread they participated in months ago can catch up in seconds instead of minutes. Maintainers can scan the summary queue to identify threads that need accepted answers marked.

The implementation above is a starting point. Extensions worth building on top of it: a weekly digest email that includes the TL;DR for the most active threads, a search index that includes summary text alongside full thread content, and a summary-based "related threads" widget that surfaces similar discussions.

If you want the full implementation including the admin UI for moderators, the digest email integration, and the related-threads widget, that is a custom development project. Get in touch to discuss what makes sense for your community's scale and use case.