0 ) { return 'elearning'; } if ( ! self::has_products() ) { return 'content'; } return self::has_sales() ? 'shop' : 'catalog'; } public static function has_products() { return class_exists( 'WooCommerce' ) || post_type_exists( 'product' ); } public static function product_count() { if ( ! post_type_exists( 'product' ) ) { return 0; } $counts = wp_count_posts( 'product' ); return isset( $counts->publish ) ? (int) $counts->publish : 0; } public static function course_count() { if ( ! defined( 'ROOSTER_LMS_VERSION' ) && ! post_type_exists( 'rooster_course' ) ) { return 0; } if ( ! post_type_exists( 'rooster_course' ) ) { return 0; } $counts = wp_count_posts( 'rooster_course' ); return isset( $counts->publish ) ? (int) $counts->publish : 0; } private static $sales_cache = null; public static function has_sales() { if ( null !== self::$sales_cache ) { return self::$sales_cache; } if ( ! self::has_products() ) { self::$sales_cache = false; return false; } global $wpdb; $table_exists = static function ( $name ) use ( $wpdb ) { return (string) $wpdb->get_var( $wpdb->prepare( 'SELECT table_name FROM information_schema.TABLES WHERE table_schema = DATABASE() AND table_name = %s', $name ) ) === $name; }; $found = false; $sp = $wpdb->prefix . 'aieo_sales_predictor'; if ( $table_exists( $sp ) ) { $found = '' !== (string) $wpdb->get_var( "SELECT MAX(prediction_date) FROM `{$sp}` WHERE grain = 'parent'" ); } $vt = $wpdb->prefix . 'aieo_core_product_vitals'; if ( ! $found && $table_exists( $vt ) ) { $found = null !== $wpdb->get_var( "SELECT 1 FROM `{$vt}` WHERE product_latest_sale_date IS NOT NULL LIMIT 1" ); } $hpos = $wpdb->prefix . 'wc_orders'; if ( ! $found && $table_exists( $hpos ) ) { $found = null !== $wpdb->get_var( "SELECT 1 FROM `{$hpos}` WHERE type = 'shop_order' LIMIT 1" ); } if ( ! $found ) { $found = null !== $wpdb->get_var( "SELECT 1 FROM {$wpdb->posts} WHERE post_type = 'shop_order' LIMIT 1" ); } self::$sales_cache = (bool) $found; return self::$sales_cache; } public static function profile_labels() { return array( 'shop' => __( 'Shop (transactional)', 'ai-eshop-optimizer' ), 'catalog' => __( 'Catalog (products as content)', 'ai-eshop-optimizer' ), 'elearning' => __( 'E-learning (Rooster LMS)', 'ai-eshop-optimizer' ), 'content' => __( 'Content site (no WooCommerce, no Rooster LMS)', 'ai-eshop-optimizer' ), ); } public static function profile_short_labels() { return array( 'shop' => __( 'Shop', 'ai-eshop-optimizer' ), 'catalog' => __( 'Catalog', 'ai-eshop-optimizer' ), 'elearning' => __( 'E-learning', 'ai-eshop-optimizer' ), 'content' => __( 'Content', 'ai-eshop-optimizer' ), ); } const PROFILE_CACHE_TTL = 12 * HOUR_IN_SECONDS; const PROFILE_CACHE_KEY = 'aieo_detected_site_profile'; public static function detected_facts( $force = false ) { if ( ! $force ) { $hit = get_transient( self::PROFILE_CACHE_KEY ); if ( is_array( $hit ) && isset( $hit['profile'] ) && self::is_profile( $hit['profile'] ) ) { return $hit; } } $facts = array( 'profile' => self::detect_site_profile(), 'sales' => self::has_sales(), 'products' => self::product_count(), 'courses' => self::course_count(), ); set_transient( self::PROFILE_CACHE_KEY, $facts, self::PROFILE_CACHE_TTL ); return $facts; } public static function detect_site_profile_cached( $force = false ) { $facts = self::detected_facts( $force ); return (string) $facts['profile']; } public static function forget_detection() { self::$profile_cache = null; self::$sales_cache = null; delete_transient( self::PROFILE_CACHE_KEY ); } public static function profile_status() { $set = class_exists( 'AIEO_DMM_Settings' ) ? (string) AIEO_DMM_Settings::get( 'seo.site_profile', 'auto' ) : 'auto'; $pinned = self::is_profile( $set ); $facts = self::detected_facts(); $detected = (string) $facts['profile']; $profile = $pinned ? $set : $detected; $labels = self::profile_labels(); $shorts = self::profile_short_labels(); $also = array(); if ( 'elearning' !== $profile && (int) $facts['courses'] > 0 ) { $also[] = sprintf( _n( 'Rooster LMS: %d course', 'Rooster LMS: %d courses', (int) $facts['courses'], 'ai-eshop-optimizer' ), (int) $facts['courses'] ); } if ( in_array( $profile, array( 'elearning', 'content' ), true ) && (int) $facts['products'] > 0 ) { $also[] = sprintf( _n( 'WooCommerce: %1$d product, %2$s', 'WooCommerce: %1$d products, %2$s', (int) $facts['products'], 'ai-eshop-optimizer' ), (int) $facts['products'], ! empty( $facts['sales'] ) ? __( 'with sales', 'ai-eshop-optimizer' ) : __( 'no sales yet', 'ai-eshop-optimizer' ) ); } return array( 'profile' => $profile, 'detected' => $detected, 'pinned' => $pinned, 'mismatch' => ( $pinned && $detected !== $profile ), 'label' => isset( $labels[ $profile ] ) ? $labels[ $profile ] : $profile, 'short' => isset( $shorts[ $profile ] ) ? $shorts[ $profile ] : $profile, 'sales' => ! empty( $facts['sales'] ), 'products' => (int) $facts['products'], 'courses' => (int) $facts['courses'], 'also' => $also, ); } public static function public_post_types() { return (array) apply_filters( 'aieo_seo_public_post_types', AIEO_Content_Types::post_types() ); } public static function public_taxonomies() { return (array) apply_filters( 'aieo_seo_public_taxonomies', AIEO_Content_Types::taxonomies() ); } public static function compute( $object_type, $object_id ) { $object_type = (string) $object_type; $object_id = (int) $object_id; $row = class_exists( 'AIEO_SEO_Store' ) ? (array) AIEO_SEO_Store::get( $object_type, $object_id ) : array(); $checks = self::run_checks( $object_type, $object_id, $row ); $score = self::weighted_score( $checks ); $fix_count = 0; foreach ( $checks as $c ) { if ( 'pass' !== $c['status'] ) { $fix_count++; } } return array( 'score' => $score, 'breakdown' => $checks, 'fix_count' => $fix_count, 'demand' => self::demand_score( $object_type, $object_id ), ); } public static function build( $object_type, $object_id ) { $r = self::compute( $object_type, $object_id ); self::save( $object_type, $object_id, $r ); return $r; } public static function get( $object_type, $object_id ) { global $wpdb; $t = $wpdb->prefix . 'aieo_seo_scores'; $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$t}` WHERE object_type = %s AND object_id = %d", (string) $object_type, (int) $object_id ), ARRAY_A ); if ( $row && isset( $row['breakdown'] ) ) { $row['breakdown'] = json_decode( (string) $row['breakdown'], true ); } return $row ?: null; } private static function run_checks( $object_type, $object_id, $row ) { $is_post = post_type_exists( $object_type ); $post = $is_post ? get_post( $object_id ) : null; $title = trim( (string) ( $row['title'] ?? '' ) ); $has_title_override = ( '' !== $title ); if ( '' === $title ) { $title = $post ? (string) get_the_title( $post ) : ''; } $desc = trim( (string) ( $row['description'] ?? '' ) ); $has_desc_override = ( '' !== $desc ); if ( '' === $desc && class_exists( 'AIEO_SEO_Head_Builder' ) ) { $desc = trim( (string) AIEO_SEO_Head_Builder::effective_description( $object_type, $object_id ) ); } $keyphrase = trim( (string) ( $row['focus_keyphrase'] ?? '' ) ); $tlen = function_exists( 'mb_strlen' ) ? mb_strlen( $title ) : strlen( $title ); $dlen = function_exists( 'mb_strlen' ) ? mb_strlen( $desc ) : strlen( $desc ); $kp_lc = self::lc( $keyphrase ); $checks = array(); $checks[] = array( 'id' => 'title_len', 'label' => __( 'SEO title length', 'ai-eshop-optimizer' ), 'weight' => 20, 'status' => ( $tlen >= 30 && $tlen <= 60 ) ? 'pass' : ( ( $tlen >= 20 && $tlen <= 70 ) ? 'warn' : 'fail' ), 'hint' => sprintf( __( '%d chars (aim 30–60).', 'ai-eshop-optimizer' ), $tlen ) . ( $has_title_override ? '' : ' ' . __( '(using default format)', 'ai-eshop-optimizer' ) ), ); $checks[] = array( 'id' => 'desc_len', 'label' => __( 'Meta description', 'ai-eshop-optimizer' ), 'weight' => 15, 'status' => ( '' === $desc ) ? 'warn' : ( ( $dlen >= 120 && $dlen <= 160 ) ? 'pass' : 'warn' ), 'hint' => ( '' === $desc ) ? __( 'No description available.', 'ai-eshop-optimizer' ) : ( sprintf( __( '%d chars (aim 120–160).', 'ai-eshop-optimizer' ), $dlen ) . ( $has_desc_override ? '' : ' ' . __( '(using generated description — add a custom one for better CTR)', 'ai-eshop-optimizer' ) ) ), ); $kp_set = ( '' !== $keyphrase ); $checks[] = array( 'id' => 'keyphrase', 'label' => __( 'Focus keyphrase', 'ai-eshop-optimizer' ), 'weight' => 10, 'status' => $kp_set ? 'pass' : 'warn', 'hint' => $kp_set ? $keyphrase : __( 'Set the converting search term (AI Assist can suggest it).', 'ai-eshop-optimizer' ), ); $set_first = __( 'Set a focus keyphrase first.', 'ai-eshop-optimizer' ); $checks[] = array( 'id' => 'kp_in_title', 'label' => __( 'Keyphrase in title', 'ai-eshop-optimizer' ), 'weight' => 12, 'status' => ! $kp_set ? 'warn' : ( ( false !== strpos( self::lc( $title ), $kp_lc ) ) ? 'pass' : 'warn' ), 'hint' => ! $kp_set ? $set_first : __( 'The focus keyphrase should appear in the title.', 'ai-eshop-optimizer' ), ); if ( $post ) { $checks[] = array( 'id' => 'kp_in_slug', 'label' => __( 'Keyphrase in URL slug', 'ai-eshop-optimizer' ), 'weight' => 6, 'status' => ! $kp_set ? 'warn' : ( ( false !== strpos( self::lc( str_replace( '-', ' ', (string) $post->post_name ) ), $kp_lc ) ) ? 'pass' : 'warn' ), 'hint' => ! $kp_set ? $set_first : __( 'Including it in the slug reinforces relevance.', 'ai-eshop-optimizer' ), ); } $body = $post ? ( (string) $post->post_content . ' ' . (string) $post->post_excerpt ) : ''; $checks[] = array( 'id' => 'kp_in_body', 'label' => __( 'Keyphrase in content', 'ai-eshop-optimizer' ), 'weight' => 8, 'status' => ! $kp_set ? 'warn' : ( ( '' !== $body && false !== strpos( self::lc( wp_strip_all_tags( $body ) ), $kp_lc ) ) ? 'pass' : 'warn' ), 'hint' => ! $kp_set ? $set_first : __( 'Mention the keyphrase in the body copy.', 'ai-eshop-optimizer' ), ); $has_canon = ( '' !== trim( (string) ( $row['canonical_url'] ?? '' ) ) ) || ( '' !== trim( (string) ( $row['canonical_override'] ?? '' ) ) ); $checks[] = array( 'id' => 'canonical', 'label' => __( 'Canonical URL', 'ai-eshop-optimizer' ), 'weight' => 8, 'status' => $has_canon ? 'pass' : 'warn', 'hint' => $has_canon ? '' : __( 'Will be set on the next head rebuild.', 'ai-eshop-optimizer' ), ); if ( $post ) { $has_img = has_post_thumbnail( $object_id ) || ( '' !== trim( (string) ( $row['og_image_custom_url'] ?? '' ) ) ); $checks[] = array( 'id' => 'image', 'label' => __( 'Featured/social image', 'ai-eshop-optimizer' ), 'weight' => 6, 'status' => $has_img ? 'pass' : 'warn', 'hint' => $has_img ? '' : __( 'Add a featured image for richer social shares.', 'ai-eshop-optimizer' ), ); } $noindex = ! empty( $row['robots_noindex'] ) && ( isset( $row['robots_default'] ) && ! $row['robots_default'] ); $checks[] = array( 'id' => 'indexable', 'label' => __( 'Indexable', 'ai-eshop-optimizer' ), 'weight' => 5, 'status' => $noindex ? 'warn' : 'pass', 'hint' => $noindex ? __( 'This object is set to noindex — it will not rank.', 'ai-eshop-optimizer' ) : '', ); return $checks; } private static function weighted_score( $checks ) { $total = 0; $got = 0; foreach ( $checks as $c ) { $w = (int) $c['weight']; $total += $w; if ( 'pass' === $c['status'] ) { $got += $w; } elseif ( 'warn' === $c['status'] ) { $got += (int) round( $w / 2 ); } } return $total > 0 ? (int) round( $got / $total * 100 ) : 0; } public static function demand_score( $object_type, $object_id ) { $score = self::compute_demand_score( $object_type, $object_id ); $score = (int) apply_filters( 'aieo_seo_demand_score', $score, $object_type, $object_id ); return max( 0, min( 100, $score ) ); } private static function compute_demand_score( $object_type, $object_id ) { if ( 'product' !== $object_type ) { if ( ! class_exists( 'AIEO_Content_Vitals' ) ) { require_once AIEO_DIR . 'includes/class-aieo-content-vitals.php'; } return AIEO_Content_Vitals::demand( $object_type, $object_id ); } if ( 'shop' !== self::site_profile() ) { return self::views_demand( $object_id ); } global $wpdb; $sp = $wpdb->prefix . 'aieo_sales_predictor'; $vt = $wpdb->prefix . 'aieo_core_product_vitals'; if ( null === self::$anchor_cache ) { self::$anchor_cache = (string) $wpdb->get_var( "SELECT MAX(prediction_date) FROM `{$sp}` WHERE grain = 'parent'" ); } $anchor = self::$anchor_cache; $row = null; if ( $anchor ) { $row = $wpdb->get_row( $wpdb->prepare( "SELECT sp.chosen_items_per_day, sp.reliability_score, v.product_latest_sale_date, v.stock FROM `{$sp}` sp LEFT JOIN `{$vt}` v ON v.product_id = sp.product_id WHERE sp.grain = 'parent' AND sp.product_id = %d AND sp.prediction_date = %s", (int) $object_id, $anchor ), ARRAY_A ); } if ( ! $row ) { return self::recency_demand( $object_id ); } $vel = max( 0.0, (float) $row['chosen_items_per_day'] ); $vel_score = 100.0 * ( 1.0 - exp( -$vel / 1.5 ) ); $rel = min( 1.0, max( 0.0, (float) ( $row['reliability_score'] ?? 0 ) ) ); $temper = 0.5 + 0.5 * $rel; $recency = max( 0.35, self::recency_factor( $row['product_latest_sale_date'] ?? '' ) ); $in_stock = ( isset( $row['stock'] ) && (int) $row['stock'] > 0 ) ? 1.0 : 0.85; return (int) max( 0, min( 100, round( $vel_score * $temper * $recency * $in_stock ) ) ); } private static function views_demand( $object_id ) { global $wpdb; $pm = $wpdb->prefix . 'aieo_product_session_metrics'; static $has_pm = null; if ( null === $has_pm ) { $has_pm = ( (string) $wpdb->get_var( $wpdb->prepare( 'SELECT table_name FROM information_schema.TABLES WHERE table_schema = DATABASE() AND table_name = %s', $pm ) ) === $pm ); } if ( ! $has_pm ) { return 0; } $row = $wpdb->get_row( $wpdb->prepare( "SELECT unique_sessions_viewed, engagement_score FROM `{$pm}` WHERE product_id = %d", (int) $object_id ), ARRAY_A ); if ( ! $row ) { return 0; } $views = max( 0, (int) $row['unique_sessions_viewed'] ); if ( 0 === $views ) { return 0; } $reach = 100.0 * ( 1.0 - exp( -$views / 50.0 ) ); $eng = min( 1.0, max( 0.0, (float) $row['engagement_score'] / 100 ) ); $temper = 0.5 + 0.5 * $eng; return (int) max( 0, min( 100, round( $reach * $temper ) ) ); } private static function recency_demand( $object_id ) { global $wpdb; $vt = $wpdb->prefix . 'aieo_core_product_vitals'; $v = $wpdb->get_row( $wpdb->prepare( "SELECT product_latest_sale_date, stock FROM `{$vt}` WHERE product_id = %d", (int) $object_id ), ARRAY_A ); if ( ! $v ) { return 0; } $recency = self::recency_factor( $v['product_latest_sale_date'] ?? '' ); if ( $recency <= 0 ) { return 0; } $in_stock = ( isset( $v['stock'] ) && (int) $v['stock'] > 0 ) ? 1.0 : 0.85; return (int) max( 0, min( 100, round( 100 * $recency * $in_stock ) ) ); } private static function recency_factor( $latest_sale_date ) { if ( empty( $latest_sale_date ) ) { return 0.0; } $ts = strtotime( (string) $latest_sale_date . ' UTC' ); if ( ! $ts ) { return 0.0; } $days = max( 0, (int) floor( ( (int) current_time( 'timestamp', true ) - $ts ) / DAY_IN_SECONDS ) ); return pow( 0.5, $days / 90 ); } private static function save( $object_type, $object_id, $r ) { global $wpdb; $t = $wpdb->prefix . 'aieo_seo_scores'; $breakdown_json = (string) wp_json_encode( $r['breakdown'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); $existing = $wpdb->get_row( $wpdb->prepare( "SELECT id, score, fix_count, breakdown FROM `{$t}` WHERE object_type = %s AND object_id = %d", (string) $object_type, (int) $object_id ), ARRAY_A ); $id = $existing ? (int) $existing['id'] : 0; $unchanged = $id > 0 && (int) $existing['score'] === (int) $r['score'] && (int) $existing['fix_count'] === (int) $r['fix_count'] && (string) $existing['breakdown'] === $breakdown_json; if ( ! $unchanged ) { $data = array( 'score' => (int) $r['score'], 'breakdown' => $breakdown_json, 'fix_count' => (int) $r['fix_count'], 'last_calculated' => current_time( 'mysql', true ), ); $fmt = array( '%d', '%s', '%d', '%s' ); if ( $id > 0 ) { $wpdb->update( $t, $data, array( 'id' => $id ), $fmt, array( '%d' ) ); } else { $data['object_type'] = (string) $object_type; $data['object_id'] = (int) $object_id; $fmt[] = '%s'; $fmt[] = '%d'; $wpdb->insert( $t, $data, $fmt ); } } if ( class_exists( 'AIEO_SEO_Store' ) ) { $cur = (array) AIEO_SEO_Store::get( $object_type, $object_id ); $cur_demand = ( array_key_exists( 'demand_score', $cur ) && null !== $cur['demand_score'] ) ? (int) $cur['demand_score'] : -1; if ( $cur_demand !== (int) $r['demand'] ) { AIEO_SEO_Store::save( $object_type, $object_id, array( 'demand_score' => (int) $r['demand'] ) ); } } } public static function run_backfill( $limit = 1500, $cursor_in = null ) { $out = array( 'built' => 0, 'done' => true, 'cursor' => array( 'phase' => 0, 'last_id' => 0 ) ); if ( ! self::enabled() ) { return $out; } $budget = max( 1, (int) $limit ); $standalone = is_array( $cursor_in ); $cursor = $standalone ? $cursor_in : (array) get_option( self::CURSOR_OPTION, array() ); $phase = isset( $cursor['phase'] ) ? (int) $cursor['phase'] : 0; $last = isset( $cursor['last_id'] ) ? (int) $cursor['last_id'] : 0; $wrapped = false; $types = ( 0 === $phase ) ? array( 'product' ) : array_values( array_diff( array_keys( self::public_post_types() ), array( 'product' ) ) ); $rows = self::next_batch( $types, $last, $budget ); foreach ( $rows as $row ) { $last = (int) $row['id']; try { self::build( (string) $row['type'], (int) $row['id'] ); $out['built']++; } catch ( \Throwable $e ) { if ( function_exists( 'aieo_debug_log' ) ) { aieo_debug_log( 'AIEO_SEO score build failed for ' . $row['type'] . ':' . (int) $row['id'] . ' — ' . $e->getMessage() ); } } $budget--; } if ( empty( $rows ) ) { if ( 0 === $phase ) { $phase = 1; $last = 0; } else { $phase = 0; $last = 0; $wrapped = true; } } if ( ! $standalone ) { update_option( self::CURSOR_OPTION, array( 'phase' => $phase, 'last_id' => $last ), false ); } $out['done'] = $wrapped; $out['cursor'] = array( 'phase' => $phase, 'last_id' => $last ); return $out; } private static function next_batch( $types, $after, $limit ) { global $wpdb; $in = "'" . implode( "','", array_map( 'esc_sql', $types ) ) . "'"; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_type FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type IN ($in) AND ID > %d ORDER BY ID ASC LIMIT %d", (int) $after, (int) $limit ) ); $out = array(); foreach ( (array) $rows as $r ) { $out[] = array( 'type' => (string) $r->post_type, 'id' => (int) $r->ID ); } return $out; } private static function lc( $s ) { $s = (string) $s; return function_exists( 'mb_strtolower' ) ? mb_strtolower( $s, 'UTF-8' ) : strtolower( $s ); } } add_action( 'activated_plugin', array( 'AIEO_SEO_Score', 'forget_detection' ) ); add_action( 'deactivated_plugin', array( 'AIEO_SEO_Score', 'forget_detection' ) );