plugin_name = $plugin_name; $this->version = $version; $this->core = $core; if ( ! class_exists('AILM_Pool_Generator') ) { require_once dirname( __DIR__ ) . '/includes/class-ailm-pool-generator.php'; } $this->pool_generator = new AILM_Pool_Generator(); add_action( 'admin_notices', array( $this, 'display_promo_banner' ) ); } /** * Displays a promotional banner on the plugin's settings page. * The banner is dismissible on a per-session basis using sessionStorage. */ public function display_promo_banner() { // Only show on our plugin's pages $screen = get_current_screen(); if ( ! $screen || strpos( $screen->id, 'page_ailinkmaster-internal-auto-manager' ) === false ) { return; } ?>

Thank you for using AI Internal Links!
If you like the plugin, please consider leaving a review on WordPress.org. If you have questions, please visit our support forum.

plugin_name, array( $this, 'display_plugin_setup_page' ) ); } public function handle_admin_actions() { // This handles the dismiss notice link // phpcs:ignore WordPress.Security.NonceVerification.Recommended $ailm_action = isset($_GET['ailm_action']) ? sanitize_text_field(wp_unslash($_GET['ailm_action'])) : ''; if ( $ailm_action === 'dismiss_simulation' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $sim_id = isset( $_GET['simulation_id'] ) ? sanitize_key( $_GET['simulation_id'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_key( $_GET['_wpnonce'] ) : ''; if ( $sim_id && $nonce && wp_verify_nonce( $nonce, 'ailm_dismiss_' . $sim_id ) ) { delete_transient( $sim_id ); wp_safe_redirect( remove_query_arg( array('ailm_action', 'simulation_id', '_wpnonce') ) ); exit; } } if ( empty($_POST['submit_action']) ) return; $action = sanitize_key($_POST['submit_action']); $base_redirect_url = admin_url('options-general.php?page=' . $this->plugin_name); check_admin_referer('ailm_form_nonce'); if ( strpos( $action, 'ailm_delete_rule_' ) === 0 ) { $rule_id_to_delete = substr( $action, strlen( 'ailm_delete_rule_' ) ); if ( ! empty( $rule_id_to_delete ) ) { $all_rules = get_option( 'ailm_rules', [] ); $new_rules = []; foreach ( $all_rules as $rule ) { if ( isset( $rule['id'] ) && $rule['id'] === $rule_id_to_delete ) { continue; } $new_rules[] = $rule; } update_option( 'ailm_rules', $new_rules ); } wp_safe_redirect( add_query_arg( 'ailm_notice', 'rule_deleted', $base_redirect_url ) ); exit; } if ( $action === 'ailm_purge_cache' ) { $this->purge_cache(); wp_safe_redirect( add_query_arg( 'ailm_notice', 'cache_purged', $base_redirect_url ) ); exit; } if ( $action === 'ailm_reset_inbound' ) { $this->reset_inbound_counters(); wp_safe_redirect( add_query_arg( 'ailm_notice', 'counters_reset', $base_redirect_url ) ); exit; } if ( $action === 'ailm_unfreeze_all' ) { $this->unfreeze_all_links(); wp_safe_redirect( add_query_arg( 'ailm_notice', 'all_unfrozen', $base_redirect_url ) ); exit; } if ( $action === 'ailm_save_settings' || $action === 'ailm_run_shuffled_simulation' || $action === 'ailm_add_new_rule' ) { if ($action === 'ailm_save_settings' || $action === 'ailm_add_new_rule') { $this->save_settings( $action === 'ailm_add_new_rule' ); } $is_shuffled = ($action === 'ailm_run_shuffled_simulation'); $simulation_data = $this->run_simulation($is_shuffled); $transient_id = 'ailm_sim_' . wp_generate_password(12, false); set_transient($transient_id, $simulation_data, HOUR_IN_SECONDS); $redirect_url = add_query_arg( 'simulation_id', $transient_id, $base_redirect_url ); if ($action === 'ailm_save_settings') { $redirect_url = add_query_arg( 'ailm_notice', 'settings_saved', $redirect_url ); } wp_safe_redirect( $redirect_url ); exit; } if ( $action === 'ailm_start_freeze_job' ) { $this->core->start_freeze_job(); $redirect_url = add_query_arg( 'ailm_notice', 'freeze_started', $base_redirect_url ); wp_safe_redirect( $redirect_url ); exit; } } public function display_plugin_setup_page() { if ( ! current_user_can( 'manage_options' ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only if ( isset($_GET['ailm_notice']) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only $this->display_admin_notices(sanitize_key($_GET['ailm_notice'])); } $this->display_progress_notice(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only if ( isset($_GET['simulation_id']) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only $sim_id = sanitize_key($_GET['simulation_id']); $sim_data = get_transient($sim_id); if ($sim_data) { $this->display_simulation_notice($sim_data, $sim_id); } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab navigation $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings'; ?>

AI Internal Links (vversion ); ?>)

render_settings_tab(); break; case 'stats': $this->render_stats_tab(); break; case 'scout': if ( ! class_exists( 'AILM_Scout_Admin' ) ) { require_once dirname( __DIR__ ) . '/modules/404-scout/class-ailm-scout-admin.php'; } $scout_admin = new AILM_Scout_Admin(); $scout_admin->render_tab(); break; } ?>
0 ? round(($processed / $total) * 100) : 0; ?>

Freeze Job in Progress...

The plugin is processing articles in the background. This notice will refresh automatically.

%

/ posts processed.

Freeze Job Complete!

All links have been processed and frozen. You can now see the results in the "Frozen Links Report" tab.

'dismiss_simulation', 'simulation_id' => $sim_id ]), 'ailm_dismiss_' . $sim_id ); ?>

Simulation Results

Based on your rules, here is a proposal for link distribution. This is a simulation, no links have been created yet.

Top 5 Categories Targeted: None'; } else { $cat_strings = []; foreach ($sim_data['top_cats'] as $cat => $count) { $cat_strings[] = esc_html($cat) . ' (' . esc_html( number_format_i18n( $count ) ) . ')'; } echo wp_kses_post( implode(', ', $cat_strings) ); } ?>


['class' => 'notice-success', 'message' => 'Settings saved! A new simulation has been generated below.'], 'cache_purged' => ['class' => 'notice-success', 'message' => 'Pool cache cleared!'], 'counters_reset' => ['class' => 'notice-warning', 'message' => 'Inbound link counters reset to 0!'], 'all_unfrozen' => ['class' => 'notice-warning', 'message' => 'ALL LINKS UNFROZEN! You can now apply a new plan.'], 'freeze_started' => ['class' => 'notice-info', 'message' => 'Freeze job started! This will run in the background. You can check the Frozen Links Report in a few minutes to see progress.'], 'rule_deleted' => ['class' => 'notice-success', 'message' => 'Rule deleted successfully.'], ]; if (isset($notices[$notice_key])) { $notice = $notices[$notice_key]; echo '

' . esc_html($notice['message']) . '

'; } } private function purge_cache() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_ailm_pool_%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_timeout_ailm_pool_%' ) ); } private function reset_inbound_counters() { delete_post_meta_by_key( '_ailm_inbound_count' ); } private function unfreeze_all_links() { delete_post_meta_by_key( '_ailm_frozen_map' ); delete_post_meta_by_key( '_ailm_inbound_count' ); } private function save_settings( $is_new_rule = false ) { // Explicitly sanitize and save options to satisfy static analysis and security checks. // phpcs:disable WordPress.Security.NonceVerification.Missing if ( isset( $_POST['ailm_mode'] ) ) update_option( 'ailm_mode', sanitize_text_field( wp_unslash( $_POST['ailm_mode'] ) ) ); if ( isset( $_POST['ailm_utm_source'] ) ) update_option( 'ailm_utm_source', sanitize_text_field( wp_unslash( $_POST['ailm_utm_source'] ) ) ); if ( isset( $_POST['ailm_utm_medium'] ) ) update_option( 'ailm_utm_medium', sanitize_text_field( wp_unslash( $_POST['ailm_utm_medium'] ) ) ); if ( isset( $_POST['ailm_utm_campaign'] ) ) update_option( 'ailm_utm_campaign', sanitize_text_field( wp_unslash( $_POST['ailm_utm_campaign'] ) ) ); if ( isset( $_POST['ailm_exclude_urls'] ) ) update_option( 'ailm_exclude_urls', sanitize_text_field( wp_unslash( $_POST['ailm_exclude_urls'] ) ) ); if ( isset( $_POST['ailm_hybrid_days'] ) ) update_option( 'ailm_hybrid_days', intval( wp_unslash( $_POST['ailm_hybrid_days'] ) ) ); if ( isset( $_POST['ailm_global_limit'] ) ) update_option( 'ailm_global_limit', intval( wp_unslash( $_POST['ailm_global_limit'] ) ) ); if ( isset( $_POST['ailm_min_chars'] ) ) update_option( 'ailm_min_chars', intval( wp_unslash( $_POST['ailm_min_chars'] ) ) ); if ( isset( $_POST['ailm_skip_chars'] ) ) update_option( 'ailm_skip_chars', intval( wp_unslash( $_POST['ailm_skip_chars'] ) ) ); if ( isset( $_POST['ailm_cache_hours'] ) ) update_option( 'ailm_cache_hours', intval( wp_unslash( $_POST['ailm_cache_hours'] ) ) ); if ( isset( $_POST['ailm_target_limit'] ) ) update_option( 'ailm_target_limit', intval( wp_unslash( $_POST['ailm_target_limit'] ) ) ); if ( isset( $_POST['ailm_pool_size'] ) ) update_option( 'ailm_pool_size', intval( wp_unslash( $_POST['ailm_pool_size'] ) ) ); update_option( 'ailm_debug_mode', isset( $_POST['ailm_debug_mode'] ) ? 1 : 0 ); update_option( 'ailm_enable_utm', isset( $_POST['ailm_enable_utm'] ) ? 1 : 0 ); update_option( 'ailm_count_manual_links', isset( $_POST['ailm_count_manual_links'] ) ? 1 : 0 ); // phpcs:enable WordPress.Security.NonceVerification.Missing $clean_rules = []; if ( isset( $_POST['rules'] ) && is_array( $_POST['rules'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $rule_count = 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing foreach ( wp_unslash($_POST['rules']) as $r ) { if ($rule_count >= self::MAX_RULES) break; if (isset($r['delete'])) continue; if (empty($r['id']) && empty($r['keywords'])) continue; $raw_keywords = $r['keywords'] ?? ''; $keyword_lines = preg_split( '/\r\n|\r|\n/', $raw_keywords ); $limited_keywords = array_slice($keyword_lines, 0, self::MAX_KEYWORDS_PER_RULE); $sanitized_keywords = sanitize_textarea_field(implode("\n", $limited_keywords)); $mixes = []; if ( !empty( $r['mix'] ) && is_array( $r['mix'] ) ) { foreach ( $r['mix'] as $m ) { if ( !empty($m['cat']) && !empty($m['pct']) ) { $mixes[] = [ 'cat' => intval( $m['cat'] ), 'pct' => intval( $m['pct'] ) ]; } } } $clean_rules[] = [ 'id' => sanitize_text_field($r['id'] ?? uniqid()), 'cat_id' => intval($r['cat_id'] ?? 0), 'keywords' => $sanitized_keywords, 'donor_age' => intval($r['donor_age'] ?? 30), 'target_age' => intval($r['target_age'] ?? 7), 'limit_total' => intval($r['limit_total'] ?? 5), 'limit_per_word_default' => intval($r['limit_per_word_default'] ?? 1), 'mix_rules' => $mixes, ]; $rule_count++; } } if ( $is_new_rule && count($clean_rules) < self::MAX_RULES ) { $clean_rules[] = [ 'id' => uniqid(), 'cat_id' => 0, 'keywords' => '', 'donor_age' => 30, 'target_age' => 7, 'limit_total' => 5, 'limit_per_word_default' => 1, 'mix_rules' => [], ]; } update_option( 'ailm_rules', $clean_rules ); } private function render_settings_tab() { $options = [ 'ailm_mode' => get_option('ailm_mode', 'static'), 'ailm_hybrid_days' => get_option('ailm_hybrid_days', 30), 'ailm_debug_mode' => get_option('ailm_debug_mode', 0), 'ailm_exclude_urls' => get_option('ailm_exclude_urls', ''), 'ailm_global_limit' => get_option('ailm_global_limit', 10), 'ailm_target_limit' => get_option('ailm_target_limit', 10), 'ailm_min_chars' => get_option('ailm_min_chars', 2000), 'ailm_skip_chars' => get_option('ailm_skip_chars', 700), 'ailm_cache_hours' => get_option('ailm_cache_hours', 24), 'ailm_pool_size' => get_option('ailm_pool_size', 50), 'ailm_enable_utm' => get_option('ailm_enable_utm', 0), 'ailm_utm_source' => get_option('ailm_utm_source', 'auto_link'), 'ailm_utm_medium' => get_option('ailm_utm_medium', 'internal'), 'ailm_utm_campaign' => get_option('ailm_utm_campaign', 'seo_auto'), 'ailm_count_manual_links' => get_option('ailm_count_manual_links', 0), ]; $rules = get_option( 'ailm_rules', [] ); $categories = get_categories( [ 'hide_empty' => 0 ] ); ?>

Operation Mode


Best for performance.

Freeze Threshold: days.

Global Settings

['Max Incoming Links', 'Inbound limit per article'], 'ailm_pool_size' => ['Pool Size', 'For mixing'], 'ailm_cache_hours' => ['Refresh (Hybrid only)', 'hours (Cache for new posts)'], 'ailm_global_limit' => ['Max Outgoing Links', 'per post'], 'ailm_min_chars' => ['Min Content Length', 'characters'], 'ailm_skip_chars' => ['Skip Intro', 'characters (No links in the beginning)'], ]; foreach ($fields as $key => $texts) { printf( '', esc_html( $texts[0] ), esc_attr( $key ), esc_attr( $options[ $key ] ), esc_html( $texts[1] ) ); } ?>
%s %s
Debug Mode
Calculate manual links

URL Exclusions:


UTM (Internal)

Src: Med: Cmp:

$rule ) : $idx = intval($index); ?>

Rule #

MAIN CATEGORY:
Mix Rules:
Remaining percentage goes to Main Category.
Mix : %:
'Older (days)', 'target_age' => 'Newer (days)', 'limit_total' => 'Limit Total', 'limit_per_word_default' => 'Per Word']; foreach ($rule_fields as $key => $label) { printf( '%s: ', esc_html($label), esc_attr($idx), esc_attr($key), esc_attr($rule[$key]) ); } ?>

Keys (max ):

Maximum number of rules () reached. More rules available in PRO version.

render_frozen_links_report(); ?>

Frozen Links Report

'; $frozen_posts = get_posts([ 'post_type' => 'any', 'posts_per_page' => -1, 'meta_key' => '_ailm_frozen_map', 'fields' => 'ids', 'no_found_rows' => true, ]); if ( empty( $frozen_posts ) ) { echo '

No frozen links found in the database. Links are frozen when a user or bot visits a page for the first time after rules are applied.

'; return; } $stats = ['urls' => [], 'cats' => [], 'tags' => [], 'total_links' => 0]; foreach ( $frozen_posts as $pid ) { $map = get_post_meta( $pid, '_ailm_frozen_map', true ); if ( !is_array($map) || empty($map) ) continue; foreach($map as $link_item) { $url = strtok($link_item['url'], '?'); if (empty($url)) continue; if ( !isset($stats['urls'][$url]) ) { $stats['urls'][$url] = 0; } $stats['urls'][$url]++; $stats['total_links']++; } } $final_stats = []; foreach ($stats['urls'] as $url => $count) { $id = url_to_postid($url); $final_stats[] = ['url' => $url, 'title' => $id ? get_the_title($id) : $url, 'count' => $count]; if (!$id) continue; $terms = get_the_terms($id, 'category'); if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $stats['cats'][$t->name] = ($stats['cats'][$t->name] ?? 0) + $count; $terms = get_the_terms($id, 'post_tag'); if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $stats['tags'][$t->name] = ($stats['tags'][$t->name] ?? 0) + $count; } usort( $final_stats, function ( $a, $b ) { return $b['count'] - $a['count']; } ); arsort( $stats['cats'] ); arsort( $stats['tags'] ); echo '
'; echo '
'; printf( '

Top Recipients (from Frozen Links)

Total frozen links: %s  |  Total unique articles linked to: %s

', esc_html( number_format_i18n( $stats['total_links'] ) ), esc_html( count( $stats['urls'] ) ) ); echo ''; foreach (array_slice($final_stats, 0, 100) as $row) { printf( '', esc_url($row['url']), esc_html($row['title']), esc_html($row['url']), esc_html(number_format_i18n($row['count'])) ); } echo '
ArticleLinks
%s
%s
%s
'; echo '

By Category

'; foreach(array_slice($stats['cats'], 0, 20, true) as $name => $cnt) echo ''; echo '
CategoryLinks
'.esc_html($name).''.esc_html(number_format_i18n($cnt)).'
'; echo '

By Tag

'; foreach(array_slice($stats['tags'], 0, 20, true) as $name => $cnt) echo ''; echo '
TagLinks
'.esc_html($name).''.esc_html(number_format_i18n($cnt)).'
'; echo '
'; $all_post_ids = get_posts(['numberposts' => -1, 'post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids']); $total_posts = count($all_post_ids); $orphan_count = 0; $orphan_list_html = ''; $recipient_urls = array_keys($stats['urls']); foreach ($all_post_ids as $pid) { $post_url = get_permalink($pid); if (!in_array($post_url, $recipient_urls)) { $orphan_count++; if ($orphan_count <= 100) { $orphan_list_html .= sprintf( '
  • %s
  • ', esc_url($post_url), esc_html(get_the_title($pid)) ); } } } echo '
    '; echo '

    Orphans (No Inbound Links)

    '; printf( '

    Total Articles: %s | Orphan Articles: %s

    ', esc_html( number_format_i18n( $total_posts ) ), esc_html( number_format_i18n( $orphan_count ) ) ); if ($orphan_count > 0) { // The content of $orphan_list_html is escaped during creation, wp_kses_post provides an extra layer of security. echo '
    '; } else { echo '

    No orphans found based on existing frozen links.

    '; } echo '
    '; echo '
    Back'; } private function run_simulation($is_shuffled = false) { $rules = get_option( 'ailm_rules', [] ); $simulation_stats = ['urls' => [], 'cats' => [], 'total_links' => 0, 'simulated_inbound' => []]; if ( empty( $rules ) ) { $all_post_ids = get_posts(['post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1]); return [ 'total_links' => 0, 'total_recipients' => 0, 'orphan_count' => count($all_post_ids), 'top_cats' => [] ]; } $global_limit = get_option( 'ailm_global_limit', 10 ); $target_limit = get_option( 'ailm_target_limit', 10 ); $min_chars = get_option( 'ailm_min_chars', 2000 ); $skip_chars = get_option( 'ailm_skip_chars', 700 ); $pool_size = get_option( 'ailm_pool_size', 50 ); $prepared_rules = []; foreach ( $rules as $rule ) { if ( empty( trim( $rule['keywords'] ) ) ) continue; $target_urls = $this->pool_generator->generate_pool( $rule, $pool_size, true, $is_shuffled ); if ( empty( $target_urls ) ) continue; $rule['target_pool'] = $target_urls; $rule['pool_size'] = count($target_urls); $raw_lines = preg_split( '/\r\n|\r|\n/', $rule['keywords'] ); $word_limit_map = []; $clean_kw_regex = []; $default_limit = $rule['limit_per_word_default'] ?? 1; foreach ( $raw_lines as $line ) { $parts = explode( '|', $line ); $word_raw = trim( $parts[0] ); if ( empty($word_raw) ) continue; $key = mb_strtolower( $word_raw, 'UTF-8' ); if ( !isset( $word_limit_map[$key] ) ) { $word_limit_map[$key] = isset($parts[1]) ? intval(trim($parts[1])) : $default_limit; $clean_kw_regex[] = preg_quote( $word_raw, '/' ); } } usort($clean_kw_regex, function($a, $b) { return strlen($b) - strlen($a); }); if ( ! empty( $clean_kw_regex ) ) { $rule['regex_pattern'] = sprintf( '/\b(%s)\b/iu', implode( '|', $clean_kw_regex ) ); $rule['word_limits'] = $word_limit_map; $prepared_rules[] = $rule; } } if ( empty( $prepared_rules ) ) { $all_post_ids = get_posts(['post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1]); return [ 'total_links' => 0, 'total_recipients' => 0, 'orphan_count' => count($all_post_ids), 'top_cats' => [] ]; } $min_donor_age = min(array_column($prepared_rules, 'donor_age')); $donors = get_posts(['post_type' => 'post', 'posts_per_page' => -1, 'post_status' => 'publish', 'date_query' => [['before' => $min_donor_age . ' days ago']]]); foreach ($donors as $post) { $content_text = wp_strip_all_tags( $post->post_content ); if ( mb_strlen( $content_text, 'UTF-8' ) < $min_chars ) continue; $post_age_days = floor( ( time() - strtotime( $post->post_date ) ) / 86400 ); $cats_ids = wp_get_post_categories( $post->ID ); $content_text = mb_substr( $content_text, $skip_chars ); $post_total_links = 0; foreach ($prepared_rules as $rule) { if ( $post_total_links >= $global_limit ) break; if ( $rule['cat_id'] != 0 && !in_array( $rule['cat_id'], $cats_ids ) ) continue; if ( $post_age_days < $rule['donor_age'] ) continue; $counter_total = 0; $counter_words = []; $limit_total = $rule['limit_total'] ?? 5; if ( preg_match_all( $rule['regex_pattern'], $content_text, $matches ) ) { foreach ( $matches[0] as $found_word ) { if ( $counter_total >= $limit_total || $post_total_links >= $global_limit ) break; $word_lower = mb_strtolower( $found_word, 'UTF-8' ); if ( ($counter_words[$word_lower] ?? 0) >= ($rule['word_limits'][$word_lower] ?? 1) ) continue; if (empty($rule['target_pool'])) continue; $pool_index = ( $post->ID + $counter_total ) % $rule['pool_size']; $target_url = $rule['target_pool'][ $pool_index ]; $t_id = url_to_postid( $target_url ); if ( $t_id ) { $real_db_count = (int) get_post_meta( $t_id, '_ailm_inbound_count', true ); $sim_count = $simulation_stats['simulated_inbound'][$t_id] ?? 0; if ( ($real_db_count + $sim_count) >= $target_limit ) continue; $simulation_stats['simulated_inbound'][$t_id] = $sim_count + 1; } $simulation_stats['urls'][$target_url] = ($simulation_stats['urls'][$target_url] ?? 0) + 1; $simulation_stats['total_links']++; $counter_total++; $counter_words[$word_lower] = ($counter_words[$word_lower] ?? 0) + 1; $post_total_links++; } } } } $all_post_ids = get_posts(['numberposts' => -1, 'post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids']); $orphan_count = 0; foreach ($all_post_ids as $pid) { $post_url = get_permalink($pid); if (!isset($simulation_stats['urls'][$post_url]) && !isset($simulation_stats['simulated_inbound'][$pid])) { $orphan_count++; } } $cats_by_count = []; foreach ($simulation_stats['urls'] as $url => $count) { $id = url_to_postid($url); if (!$id) continue; $terms = get_the_terms($id, 'category'); if ($terms && !is_wp_error($terms)) foreach ($terms as $t) $cats_by_count[$t->name] = ($cats_by_count[$t->name] ?? 0) + $count; } arsort($cats_by_count); return [ 'total_links' => $simulation_stats['total_links'], 'total_recipients' => count($simulation_stats['urls']), 'orphan_count' => $orphan_count, 'top_cats' => array_slice($cats_by_count, 0, 5, true), ]; } }