-1, 'post_status' => 'publish', 'post_type' => ['post', 'page'], ]); $summary = [ 'posts_scanned' => 0, 'issues_found' => 0, 'avg_score' => 0, 'results' => [], ]; $total_score = 0; foreach ($posts as $post) { $result = $this->audit_post($post); // Defensive guard: never trust $result blindly if (!is_array($result) || !isset($result['score'], $result['issues'])) { $result = ['score' => 0, 'issues' => [], 'word_count' => 0]; } if (!is_array($result['issues'])) { $result['issues'] = []; } $score = isset($result['score']) ? intval($result['score']) : 0; // Save results to postmeta update_post_meta($post->ID, '_shadowsync_seo_audit_result', $result); update_post_meta($post->ID, '_shadowsync_seo_score', $score); $total_score += $score; $summary['issues_found'] += count($result['issues']); $summary['results'][] = [ 'id' => $post->ID, 'title' => get_the_title($post->ID), 'url' => get_permalink($post->ID), 'score' => $score, 'issues' => $result['issues'], ]; $summary['posts_scanned']++; } if ($summary['posts_scanned'] > 0) { $summary['avg_score'] = round($total_score / $summary['posts_scanned']); } // Save summary update_option('_shadowsync_seo_last_scan_summary', $summary); update_option('_shadowsync_seo_last_scan_time', current_time('mysql')); wp_send_json_success($summary); } private function audit_post($post) { // Guard against unexpected input if (!is_object($post) || !isset($post->ID)) { return ['score' => 0, 'issues' => [], 'word_count' => 0]; } $issues = []; $score = 100; $content = isset($post->post_content) ? (string) $post->post_content : ''; // The title search engines show: the SEO title when one is set, // otherwise the post title, as the front end prints it. $title = (string) get_post_meta($post->ID, '_shadowsync_seo_meta_title', true); if ('' === trim($title)) { $title = (string) get_the_title($post->ID); } // --- CHECK 1: Title length --- $title_len = strlen($title); if ($title_len < 30) { $issues[] = ['type' => 'title_short', 'message' => 'Title is too short (' . $title_len . ' chars). Aim for 30–60.']; $score -= 15; } elseif ($title_len > 60) { $issues[] = ['type' => 'title_long', 'message' => 'Title is too long (' . $title_len . ' chars). Keep under 60.']; $score -= 10; } // --- CHECK 2: Meta description --- $meta_desc = get_post_meta($post->ID, '_shadowsync_seo_meta_description', true); if (empty($meta_desc)) { $issues[] = ['type' => 'missing_meta_desc', 'message' => 'Meta description is missing.']; $score -= 15; } elseif (strlen($meta_desc) < 50) { $issues[] = ['type' => 'meta_desc_short', 'message' => 'Meta description is too short. Aim for 50–160 chars.']; $score -= 8; } elseif (strlen($meta_desc) > 160) { $issues[] = ['type' => 'meta_desc_long', 'message' => 'Meta description is too long. Keep under 160 chars.']; $score -= 5; } // --- CHECK 3: H1 headings in content --- // Themes print the post title as the page's H1, so content without an // H1 is correct, matching the editor's SEO Analysis tab. Only more // than one is a problem. $h1_count = preg_match_all('/