< private function image_fingerprint($src) { if (!$src) return ''; $src = preg_replace('/\?.*$/', '', $src); // strip query strings return md5(strtolower(trim($src))); } private function image_text_fingerprint($text) { $text = strtolower(trim((string)$text)); $text = preg_replace('/[^a-z0-9\s]+/i', ' ', $text); $text = preg_replace('/\s+/u', ' ', $text); if ($text === '') return ''; return md5($text); } ?php namespace SmartRankPro; if (!defined('ABSPATH')) { exit; } class Trust_Analyzer { public function analyze($post_id, $comparison_post_ids = []) { $html = Utils::get_post_content_html($post_id); $text = Utils::extract_text($html); $headings = Utils::extract_headings($html); $paras = Utils::extract_paragraphs($html); $sentences = Utils::split_sentences($text); $stats = Utils::sentence_length_stats($sentences); $phrase = Utils::repeated_phrases_ratio($text); $site_host = parse_url(home_url(), PHP_URL_HOST); $link_info = Utils::count_internal_links_in_body($html, $site_host); $anchor_ratio = Utils::anchors_identical_ratio($link_info['anchors']); $word_count = str_word_count($text); // Pillar scores (0-100), start from 100 then subtract. $structure = $this->score_structure($headings, $paras, $post_id); $human = $this->score_humanization($stats, $phrase, $paras); $semantic = $this->score_semantic($word_count, $headings, $paras); $meta = $this->score_meta($post_id, $comparison_post_ids); $media = $this->score_media($post_id, $comparison_post_ids); $links = $this->score_links($post_id, $headings, $link_info['count'], $anchor_ratio, $link_info['hub_only'], $link_info['pairs'] ?? []); $pillar_scores = [ 'structure' => $structure, 'humanization' => $human, 'semantic' => $semantic, 'meta' => $meta, 'media' => $media, 'internal_links' => $links, ]; $base = $this->weighted_base($pillar_scores); $automation = $this->automation_risk($post_id, $html, $text, $headings, $paras, $stats, $phrase, $comparison_post_ids); $penalty = $automation['penalty_points']; $final = max(0, min(100, round($base - $penalty))); // v1.4 tightened rules: when multiple automation-like patterns stack, apply trust caps $tc = (int)($automation['triggers_count'] ?? 0); if ($tc >= 5) $final = min($final, 54); else if ($tc >= 4) $final = min($final, 64); else if ($tc >= 3) $final = min($final, 74); $status = $this->status_band($final); $top_fixes = $this->top_fixes($pillar_scores, $automation, $word_count, $headings, $link_info); // Derived readiness $ai_readiness = round( (0.55*$human + 0.45*$semantic) ); $search_readiness = round( (0.45*$structure + 0.35*$links + 0.20*$meta) ); return [ 'schema_version' => 1, 'scan_timestamp' => Utils::current_time_iso(), 'post_id' => (int)$post_id, 'final_score' => (int)$final, 'status_band' => $status, 'automation_risk' => $automation['risk'], 'automation_penalty_points' => (int)$penalty, 'pillar_scores' => $pillar_scores, 'derived' => [ 'ai_readiness' => (int)$ai_readiness, 'search_readiness' => (int)$search_readiness, ], 'top_fixes' => $top_fixes, 'detected_patterns' => $automation['patterns'], 'debug' => [ 'word_count' => $word_count, 'sentence_stats' => $stats, 'phrase_ratio' => $phrase['ratio'], 'internal_links' => $link_info['count'], ], ]; } private function weighted_base($p) { // weights: structure 15, human 20, semantic 20, meta 10, media 10, links 15 => total 90. Normalize to 100. $w = [ 'structure' => 15, 'humanization' => 20, 'semantic' => 20, 'meta' => 10, 'media' => 10, 'internal_links' => 15, ]; $sum_w = array_sum($w); $acc = 0.0; foreach ($w as $k=>$wk) { $acc += ($p[$k] * $wk); } return round($acc / $sum_w); } private function status_band($score) { if ($score >= 85) return 'High Trust'; if ($score >= 70) return 'Trust Acceptable'; if ($score >= 50) return 'At Risk'; return 'Untrusted'; } private function score_structure($headings, $paras, $post_id) { $score = 100; $has_h2_or_h3 = false; foreach ($headings as $h) { if ($h['level'] === 2 || $h['level'] === 3) { $has_h2_or_h3 = true; break; } } if (!$has_h2_or_h3) $score -= 12; // Title repeated as first H2 $title = get_the_title($post_id); foreach ($headings as $h) { if ($h['level'] === 2) { if (mb_strtolower(trim($h['text'])) === mb_strtolower(trim($title))) $score -= 12; break; } } // Heading stacks: headings without paragraphs (template-ish) if (count($headings) >= 5 && count($paras) <= 2) $score -= 18; // Site-wide structure uniqueness (tightened in v1.4) $sig = $this->structure_signature($headings); if ($sig !== '') { $comparison_ids = Scanner::instance()->get_comparison_post_ids($post_id); $match = 0; $checked = 0; foreach (array_slice($comparison_ids, 0, 40) as $pid) { $h = Utils::extract_headings(Utils::get_post_content_html($pid)); $osig = $this->structure_signature($h); if ($osig === '') continue; $checked++; if (Utils::similarity_levenshtein_ratio($sig, $osig) > 0.88) $match++; } $prev = ($checked>0) ? ($match/$checked) : 0.0; if ($prev >= 0.30) $score -= 22; else if ($prev >= 0.18) $score -= 14; else if ($prev >= 0.10) $score -= 8; } return max(0, min(100, $score)); } private function score_humanization($stats, $phrase, $paras) { $score = 100; // Sentence stdev too low (stronger in v1.4) if ($stats['count'] >= 12) { if ($stats['stdev'] < 6) $score -= 30; else if ($stats['stdev'] < 10) $score -= 18; else if ($stats['stdev'] < 12) $score -= 10; } // Repeated phrase ratio if ($phrase['ratio'] > 0.08) $score -= 12; else if ($phrase['ratio'] > 0.05) $score -= 6; // Paragraph opener similarity $openers = []; foreach ($paras as $p) { $p = trim($p); if ($p === '') continue; $words = preg_split('/\s+/u', $p); $open = strtolower(implode(' ', array_slice($words, 0, 4))); if ($open !== '') $openers[] = $open; } if (count($openers) >= 8) { $counts = array_count_values($openers); arsort($counts); $top = reset($counts); $ratio = $top / count($openers); if ($ratio > 0.25) $score -= 18; } return max(0, min(100, $score)); } private function score_semantic($word_count, $headings, $paras) { $score = 100; if ($word_count < 300) $score -= 25; else if ($word_count < 450) $score -= 12; // meaningful sections: count H2/H3 $sections = 0; foreach ($headings as $h) { if ($h['level'] === 2 || $h['level'] === 3) $sections++; } if ($sections < 2) $score -= 12; // Semantic compression (lexical diversity too low) $body = implode(' ', (array)$paras); $ld = $this->lexical_diversity_ratio($body); if ($word_count >= 450) { if ($ld < 0.38) $score -= 22; else if ($ld < 0.43) $score -= 12; } return max(0, min(100, $score)); } private function score_meta($post_id, $comparison_post_ids) { $score = 100; $meta = Utils::get_meta_description($post_id); if (trim($meta) === '') $score -= 10; // Compare meta similarity across a sample and measure "boilerplate prevalence" $others = []; foreach ($comparison_post_ids as $pid) { $others[] = Utils::get_meta_description($pid); } $sim_threshold = 0.78; // tuned stricter than v1.3.0 $similar_count = 0; $checked = 0; $best = 0.0; foreach (array_slice($others, 0, 50) as $o) { if (!is_string($o) || trim($o) === '') continue; $checked++; $sim = Utils::similarity_levenshtein_ratio($meta, $o); if ($sim > $best) $best = $sim; if ($sim >= $sim_threshold) $similar_count++; } $prevalence = ($checked > 0) ? ($similar_count / $checked) : 0.0; // Meta boilerplate penalties: // - If your meta is very similar to many pages, this is a strong trust risk. if ($best > 0.85 && $prevalence >= 0.30) { $score -= 40; // heavy penalty for repeated boilerplate } else if ($best > 0.82 && $prevalence >= 0.15) { $score -= 28; } else if ($best > 0.80) { $score -= 15; } // Title duplicates/near duplicates $title = get_the_title($post_id); $titles = []; foreach ($comparison_post_ids as $pid) { $titles[] = get_the_title($pid); } $title_sim = $this->max_similarity($title, $titles); if ($title_sim > 0.90) $score -= 20; // Templated prefix/suffix detection $sample = array_slice($titles, 0, 20); $sample[] = $title; $ps = Utils::common_prefix_suffix_ratio($sample); if ($ps['prefix'] > 0.25 || $ps['suffix'] > 0.25) $score -= 10; // Meta cadence/length uniformity: if meta lengths too similar across sample $lens = []; foreach (array_slice($others, 0, 20) as $m) { $lens[] = mb_strlen(trim($m)); } $lens[] = mb_strlen(trim($meta)); if (count($lens) >= 8) { $mean = array_sum($lens)/count($lens); $var = 0.0; foreach ($lens as $l) $var += pow($l-$mean,2); $var /= count($lens); $stdev = sqrt($var); if ($stdev < 12) $score -= 8; } // Schema presence inside content can look templated if duplicated $html = Utils::get_post_content_html($post_id); $schema_scripts = 0; if (is_string($html) && $html !== '') { $schema_scripts = preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>/i', $html, $matches); } if ($schema_scripts >= 2) $score -= 10; return max(0, min(100, $score)); } private function score_media($post_id, $comparison_post_ids) { $score = 100; $html = Utils::get_post_content_html($post_id); if (!$html) return 70; preg_match_all('/]+>/i', $html, $imgs); $imgs = $imgs[0] ?? []; if (empty($imgs)) { $score -= 20; return max(0, min(100, $score)); } $site_imgs = []; $alt_dupes = 0; $title_dupes = 0; $img_dupes = 0; $checked = 0; foreach ($imgs as $img) { preg_match('/src=["\']([^"\']+)/i', $img, $m1); preg_match('/alt=["\']([^"\']*)/i', $img, $m2); preg_match('/title=["\']([^"\']*)/i', $img, $m3); $src = $m1[1] ?? ''; $alt = $m2[1] ?? ''; $title = $m3[1] ?? ''; $img_fp = $this->image_fingerprint($src); $alt_fp = $this->image_text_fingerprint($alt); $title_fp = $this->image_text_fingerprint($title); $checked++; foreach (array_slice($comparison_post_ids, 0, 25) as $pid) { $ohtml = Utils::get_post_content_html($pid); if (!$ohtml) continue; if ($img_fp && strpos($ohtml, $src) !== false) $img_dupes++; if ($alt_fp && strpos(strtolower($ohtml), strtolower($alt)) !== false) $alt_dupes++; if ($title_fp && strpos(strtolower($ohtml), strtolower($title)) !== false) $title_dupes++; } } // Penalties: reused assets are strong automation signals if ($checked > 0) { $img_ratio = $img_dupes / max(1, $checked); $alt_ratio = $alt_dupes / max(1, $checked); $title_ratio = $title_dupes / max(1, $checked); if ($img_ratio >= 1.0) $score -= 30; else if ($img_ratio >= 0.6) $score -= 20; else if ($img_ratio >= 0.3) $score -= 10; if ($alt_ratio >= 1.0) $score -= 20; else if ($alt_ratio >= 0.5) $score -= 10; if ($title_ratio >= 1.0) $score -= 10; } return max(0, min(100, $score)); } private function score_links($post_id, $headings, $internal_link_count, $anchor_identical_ratio, $hub_only, $pairs) { $score = 100; // Stronger penalties: lack of contextual internal links is a major trust blocker if ($internal_link_count <= 0) $score -= 35; else if ($internal_link_count === 1) $score -= 20; if ($anchor_identical_ratio > 0.50) $score -= 15; if ($hub_only) $score -= 15; // Anchor Semantic Quality (new in v1.3.5) // Goal: detect vague/generic anchors, partial-phrase anchors, and off-pillar anchors. $weak_anchors = [ 'click here','learn more','read more','more','here','this','this page','this post','this article', 'see more','see here','go here','visit','link','details' ]; $generic_nouns = ['tips','guide','info','article','post','page','resources','resource','blog']; $pillar_terms = $this->pillar_terms($post_id, $headings); $weak_count = 0; $generic_count = 0; $short_count = 0; $off_pillar_count = 0; $usable = 0; foreach ((array)$pairs as $pair) { $anchor = is_array($pair) ? ($pair['anchor'] ?? '') : ''; $anchor = trim(wp_strip_all_tags((string)$anchor)); if ($anchor === '') continue; $a = strtolower($anchor); $a = preg_replace('/\s+/u', ' ', $a); $usable++; // Hard weak anchors if (in_array($a, $weak_anchors, true)) { $weak_count++; continue; } // Tokenize $words = preg_split('/\s+/u', $a); $words = array_values(array_filter($words, fn($w)=>$w!=='')); // Very short anchors (1–2 words) are often automation-ish when repeated if (count($words) <= 2) { $short_count++; } // Generic nouns present foreach ($words as $w) { $w2 = preg_replace('/[^a-z0-9]+/i', '', $w); if ($w2 !== '' && in_array($w2, $generic_nouns, true)) { $generic_count++; break; } } // Off-pillar overlap check if (!empty($pillar_terms)) { $overlap = 0; foreach ($words as $w) { $w2 = strtolower(preg_replace('/[^a-z0-9]+/i', '', $w)); if ($w2 === '' || strlen($w2) < 3) continue; if (isset($pillar_terms[$w2])) $overlap++; } // If no overlap, treat as off-pillar (only for non-weak anchors) if ($overlap === 0) $off_pillar_count++; } } if ($usable > 0) { $weak_ratio = $weak_count / $usable; $generic_ratio = $generic_count / $usable; $short_ratio = $short_count / $usable; $off_ratio = $off_pillar_count / $usable; // Penalties (tuned to create meaningful drops on templated linking) if ($weak_ratio >= 0.20) $score -= 18; else if ($weak_ratio >= 0.10) $score -= 10; if ($generic_ratio >= 0.30) $score -= 12; else if ($generic_ratio >= 0.15) $score -= 6; if ($short_ratio >= 0.60) $score -= 10; else if ($short_ratio >= 0.40) $score -= 6; if ($off_ratio >= 0.50) $score -= 15; else if ($off_ratio >= 0.30) $score -= 8; } return max(0, min(100, $score)); } private function pillar_terms($post_id, $headings) { // Build a simple "pillar" term set from title + H2/H3 headings (stopword-filtered). $title = strtolower((string)get_the_title($post_id)); $parts = [$title]; foreach ((array)$headings as $h) { $lvl = (int)($h['level'] ?? 0); if ($lvl === 2 || $lvl === 3) { $parts[] = strtolower((string)($h['text'] ?? '')); } } $text = implode(' ', $parts); $text = preg_replace('/[^a-z0-9\s]+/i', ' ', $text); $text = preg_replace('/\s+/u', ' ', $text); $stop = [ 'the','and','or','for','to','of','in','on','with','a','an','is','are','be','your','you','how','what','why', 'from','by','at','as','it','this','that','these','those','into','about','best','tips','guide','lesson', 'story','step','stepping','journey' ]; $stop = array_flip($stop); $terms = []; foreach (preg_split('/\s+/u', trim($text)) as $w) { $w = trim($w); if ($w === '' || strlen($w) < 3) continue; if (isset($stop[$w])) continue; $terms[$w] = true; } return $terms; } private function lexical_diversity_ratio($text) { $text = strtolower((string)$text); $text = preg_replace('/[^a-z0-9\s]+/i', ' ', $text); $text = preg_replace('/\s+/u', ' ', trim($text)); if ($text === '') return 0.0; $words = preg_split('/\s+/u', $text); $words = array_values(array_filter($words, fn($w)=>$w!=='' && strlen($w)>=3)); $n = count($words); if ($n < 80) return 1.0; $uniq = count(array_unique($words)); return $uniq / max(1, $n); } private function editorial_intent_score($text) { $t = strtolower((string)$text); $first_person = preg_match_all('/\b(i|i\'m|i’ve|i\'ve|my|mine|we|our|us)\b/u', $t, $m1); $uncertainty = preg_match_all('/\b(maybe|often|sometimes|in my experience|it depends|you might|consider)\b/u', $t, $m2); $concretes = preg_match_all('/\b(\d+|minutes|hours|degrees|cups|tablespoons|teaspoons|lbs|pounds|ounces|oz)\b/u', $t, $m3); $quotes = preg_match_all('/“|”|"/u', (string)$text, $m4); $signals = 0; if ($first_person >= 2) $signals++; if ($uncertainty >= 2) $signals++; if ($concretes >= 3) $signals++; if ($quotes >= 2) $signals++; if ($signals == 0) return 55; if ($signals == 1) return 70; if ($signals == 2) return 82; return 92; } private function automation_risk($post_id, $html, $text, $headings, $paras, $stats, $phrase, $comparison_post_ids) { $triggers = 0; $patterns = []; // Post-internal triggers if ($stats['count'] >= 12 && $stats['stdev'] < 10) { $triggers++; $patterns[] = ['type'=>'sentence_rhythm_uniform', 'severity'=>'Medium', 'detail'=>'Sentence rhythm is unusually uniform.']; } if ($phrase['ratio'] > 0.08) { $triggers++; $patterns[] = ['type'=>'repeated_phrases', 'severity'=>'Medium', 'detail'=>'Repeated phrasing appears throughout the page.']; } // Predictable section patterns: lots of H2 but no H3 (template-ish) $h2 = 0; $h3 = 0; foreach ($headings as $h) { if ($h['level'] === 2) $h2++; if ($h['level'] === 3) $h3++; } if ($h2 >= 5 && $h3 === 0) { $triggers++; $patterns[] = ['type'=>'predictable_sections', 'severity'=>'Low', 'detail'=>'Section layout looks repetitive (many H2, no sub-sections).']; } // Genericness ratio: crude heuristic: many vague adjectives without specifics. $generic_words = ['easy','simple','important','helpful','great','best','quick','amazing','ultimate','perfect','effective']; $tokens = preg_split('/\s+/u', strtolower($text)); $g = 0; foreach ($tokens as $t) { $t = preg_replace('/[^a-z]+/','',$t); if (in_array($t,$generic_words,true)) $g++; } if (count($tokens) > 0) { $ratio = $g / count($tokens); if ($ratio > 0.02 && str_word_count($text) > 400) { $triggers++; $patterns[] = ['type'=>'generic_language', 'severity'=>'Low', 'detail'=>'Language is a bit generic; add concrete examples for trust.']; } } // Duplicate schema scripts in content (some automation workflows insert multiple JSON-LD blocks) $schema_scripts = 0; if (is_string($html) && $html !== '') { $schema_scripts = preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>/i', $html, $matches); } if ($schema_scripts >= 2) { // Editorial intent (v1.4): pages with zero editorial markers can look fully automated $intent = $this->editorial_intent_score($text); if ($intent <= 70) { $triggers++; $patterns[] = ['type'=>'low_editorial_intent', 'severity'=>'Medium', 'detail'=>'This page reads highly “template-like” and lacks natural editorial signals (examples, voice, context).']; } // Semantic compression trigger (v1.4) $ld = $this->lexical_diversity_ratio(implode(' ', (array)$paras)); if (str_word_count($text) >= 450 && $ld < 0.43) { $triggers++; $patterns[] = ['type'=>'semantic_compression', 'severity'=>'Medium', 'detail'=>'Wording is very uniform across the page, which can look templated.']; } $triggers++; $patterns[] = ['type'=>'schema_duplication', 'severity'=>'Medium', 'detail'=>'Multiple schema blocks were found inside the content.']; } // Image reuse footprints (v1.4.1) preg_match_all('/]+>/i', $html, $imgs2); $imgs2 = $imgs2[0] ?? []; if (!empty($imgs2)) { $reuse = 0; foreach ($imgs2 as $img) { preg_match('/src=["\']([^"\']+)/i', $img, $m1); $src = $m1[1] ?? ''; if (!$src) continue; foreach (array_slice($comparison_post_ids, 0, 20) as $pid) { $ohtml = Utils::get_post_content_html($pid); if ($ohtml && strpos($ohtml, $src) !== false) { $reuse++; break; } } } if ($reuse >= 1) { $triggers++; $patterns[] = ['type'=>'image_reuse', 'severity'=>'Medium', 'detail'=>'Images are reused across multiple posts (non-editorial footprint).']; } } // Anchor quality footprints (v1.3.5) // Weak/generic/off-pillar anchors can look templated even when links exist. $site_host = parse_url(home_url(), PHP_URL_HOST); $link_info = Utils::count_internal_links_in_body($html, $site_host); $pairs = $link_info['pairs'] ?? []; $pillar_terms = $this->pillar_terms($post_id, $headings); $weak_anchors = ['click here','learn more','read more','more','here','this','this page','this post','this article','link','details']; $generic_nouns = ['tips','guide','info','article','post','page','resources','resource','blog']; $usable = 0; $weak = 0; $generic = 0; $off = 0; $short = 0; foreach ((array)$pairs as $pair) { $anchor = trim(wp_strip_all_tags((string)($pair['anchor'] ?? ''))); if ($anchor === '') continue; $usable++; $a = strtolower(preg_replace('/\s+/u',' ', $anchor)); if (in_array($a, $weak_anchors, true)) { $weak++; continue; } $words = preg_split('/\s+/u', $a); $words = array_values(array_filter($words, fn($w)=>$w!=='')); if (count($words) <= 2) $short++; foreach ($words as $w) { $w2 = strtolower(preg_replace('/[^a-z0-9]+/i','',$w)); if ($w2 !== '' && in_array($w2, $generic_nouns, true)) { $generic++; break; } } if (!empty($pillar_terms)) { $overlap = 0; foreach ($words as $w) { $w2 = strtolower(preg_replace('/[^a-z0-9]+/i','',$w)); if ($w2 === '' || strlen($w2) < 3) continue; if (isset($pillar_terms[$w2])) $overlap++; } if ($overlap === 0) $off++; } } if ($usable >= 3) { $weak_ratio = $weak / $usable; $off_ratio = $off / $usable; $short_ratio = $short / $usable; if ($weak_ratio >= 0.10) { $triggers++; $patterns[] = ['type'=>'weak_anchor_text', 'severity'=>'Medium', 'detail'=>'Some internal links use vague wording (e.g., “learn more”, “click here”).']; } if ($off_ratio >= 0.30) { $triggers++; $patterns[] = ['type'=>'off_pillar_anchors', 'severity'=>'Medium', 'detail'=>'Some internal links don’t match the main topic of this page.']; } if ($short_ratio >= 0.60) { $triggers++; $patterns[] = ['type'=>'short_anchors', 'severity'=>'Low', 'detail'=>'Many internal links use very short anchor text (1–2 words), which can look templated.']; } } // Cross-post triggers (if enough data) $comp_count = count($comparison_post_ids); if ($comp_count >= 10) { // Structure signature similarity $sig = $this->structure_signature($headings); $sims = 0; foreach ($comparison_post_ids as $pid) { $h = Utils::extract_headings(Utils::get_post_content_html($pid)); $osig = $this->structure_signature($h); if ($osig !== '' && Utils::similarity_levenshtein_ratio($sig, $osig) > 0.85) $sims++; } if ($sims >= 5) { $triggers++; $patterns[] = ['type'=>'structure_similarity', 'severity'=>'Medium', 'detail'=>'Heading layout matches many other posts.']; } // HTML fingerprint similarity: compare first 400 chars of stripped HTML (very basic v1) $finger = substr(preg_replace('/\s+/',' ', strip_tags($html, '

    1. ')), 0, 400); $fsims = 0; foreach ($comparison_post_ids as $pid) { $ohtml = Utils::get_post_content_html($pid); $of = substr(preg_replace('/\s+/',' ', strip_tags($ohtml, '

        1. ')), 0, 400); if ($of !== '' && Utils::similarity_levenshtein_ratio($finger, $of) > 0.90) $fsims++; } if ($fsims >= 5) { $triggers++; $patterns[] = ['type'=>'html_fingerprint', 'severity'=>'High', 'detail'=>'This page shares a strong template signature with other pages.']; } // Schema template reuse across posts $curr_blocks = []; if (is_string($html) && $html !== '') { if (preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $html, $mm)) { foreach ($mm[1] as $blk) { $blk = trim(preg_replace('/\s+/u', ' ', $blk)); if ($blk !== '') $curr_blocks[] = $blk; } } } if (!empty($curr_blocks)) { $curr_hashes = array_map(function($b){ return md5($b); }, $curr_blocks); $reuse = 0; $checked = 0; foreach (array_slice($comparison_post_ids, 0, 25) as $pid) { $ohtml = Utils::get_post_content_html($pid); if (!is_string($ohtml) || $ohtml === '') continue; $checked++; if (preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $ohtml, $mx)) { foreach ($mx[1] as $oblk) { $oblk = trim(preg_replace('/\s+/u', ' ', $oblk)); if ($oblk === '') continue; $oh = md5($oblk); if (in_array($oh, $curr_hashes, true)) { $reuse++; break; } } } } if ($checked >= 8 && ($reuse / max(1, $checked)) >= 0.20) { $triggers++; $patterns[] = ['type'=>'schema_template_reuse', 'severity'=>'Medium', 'detail'=>'Schema blocks look reused across multiple pages (template signature).']; } } // Meta boilerplate across posts $meta = Utils::get_meta_description($post_id); $metas = []; foreach ($comparison_post_ids as $pid) { $metas[] = Utils::get_meta_description($pid); } $sim_threshold = 0.78; $similar_count = 0; $checked = 0; $best = 0.0; foreach (array_slice($metas, 0, 50) as $o) { if (!is_string($o) || trim($o) === '') continue; $checked++; $sim = Utils::similarity_levenshtein_ratio($meta, $o); if ($sim > $best) $best = $sim; if ($sim >= $sim_threshold) $similar_count++; } $prevalence = ($checked > 0) ? ($similar_count / $checked) : 0.0; if ($best > 0.85 && $prevalence >= 0.30) { $triggers += 2; // strong site-wide boilerplate $patterns[] = ['type'=>'meta_boilerplate', 'severity'=>'High', 'detail'=>'Page descriptions look heavily repeated across the site.']; } else if ($best > 0.82 && $prevalence >= 0.15) { $triggers++; $patterns[] = ['type'=>'meta_boilerplate', 'severity'=>'Medium', 'detail'=>'Page descriptions look too similar across the site.']; } else if ($best > 0.80) { $triggers++; $patterns[] = ['type'=>'meta_similarity', 'severity'=>'Low', 'detail'=>'Page description is somewhat similar to other pages.']; } } // Risk levels $risk = 'Low'; if ($triggers >= 4) $risk = 'High'; else if ($triggers >= 2) $risk = 'Medium'; $penalty = 0; if ($risk === 'Medium') $penalty = 10; if ($risk === 'High') $penalty = 20; // If <10 posts, note limited comparison if ($comp_count < 10) { $patterns[] = ['type'=>'limited_comparison', 'severity'=>'Info', 'detail'=>'Limited comparison data yet; risk is based mostly on this page alone.']; } return ['risk'=>$risk, 'penalty_points'=>$penalty, 'patterns'=>$patterns, 'triggers_count'=>$triggers]; } private function structure_signature($headings) { if (empty($headings)) return ''; $parts = []; foreach ($headings as $h) { $parts[] = 'H' . intval($h['level']); } return implode('-', $parts) . '|count=' . count($parts); } private function max_similarity($needle, $haystack) { $best = 0.0; foreach ($haystack as $h) { if (!is_string($h) || trim($h) === '') continue; $best = max($best, Utils::similarity_levenshtein_ratio($needle, $h)); } return $best; } private function top_fixes($pillar_scores, $automation, $word_count, $headings, $link_info) { // Pick the most impactful 3-5 fixes; novice-safe language. $fixes = []; // Structural $has_h2 = false; foreach ($headings as $h) { if ($h['level'] === 2) { $has_h2 = true; break; } } if (!$has_h2) { $fixes[] = ['impact'=>'High','message'=>'Add 2–4 short section headings so search engines and AI tools can follow your page.']; } // Links if ($link_info['count'] <= 0) { $fixes[] = ['impact'=>'High','message'=>'Add 1–2 helpful links inside your paragraphs to related pages on your site.']; } // Meta if ($pillar_scores['meta'] < 80) { $fixes[] = ['impact'=>'High','message'=>'Make your page description more unique (avoid repeating the same description across many pages).']; } // Semantic depth if ($word_count < 450) { $fixes[] = ['impact'=>'Medium','message'=>'Add a bit more helpful detail (examples or steps) to strengthen trust.']; } // Media if ($pillar_scores['media'] < 85) { $fixes[] = ['impact'=>'Medium','message'=>'Use a featured image and a unique image description (alt text) that fits this page.']; } // Automation risk if ($automation['risk'] !== 'Low') { $fixes[] = ['impact'=>'High','message'=>'Vary your structure and phrasing slightly so this page doesn’t look templated.']; } // De-dup & limit to 5 $seen = []; $out = []; foreach ($fixes as $f) { if (isset($seen[$f['message']])) continue; $seen[$f['message']] = true; $out[] = $f; if (count($out) >= 5) break; } return array_slice($out, 0, 5); } }
          Warning: Cannot modify header information - headers already sent by (output started at /home/trainpup/public_html/wp-content/plugins/smart-rank-pro/includes/class-smartrank-trust-analyzer.php:1) in /home/trainpup/public_html/wp-includes/pluggable.php on line 1531

          Warning: Cannot modify header information - headers already sent by (output started at /home/trainpup/public_html/wp-content/plugins/smart-rank-pro/includes/class-smartrank-trust-analyzer.php:1) in /home/trainpup/public_html/wp-includes/pluggable.php on line 1534