scan_posts();
$rows = $data['rows'];
$site_status_level = $data['site_status_level'];
$checklist = $data['checklist'];
// 2. Render Site-Level Indicator
$status_config = [
'healthy' => ['icon' => 'yes', 'color' => '#46b450', 'text' => 'Healthy', 'desc' => 'No known issues.'],
'attention' => ['icon' => 'warning', 'color' => '#ffb900', 'text' => 'Attention Needed', 'desc' => 'Some pages rely on global defaults.'],
'action' => ['icon' => 'no', 'color' => '#dc3232', 'text' => 'Action Required', 'desc' => 'Blocking issues detected on some pages.']
];
$current_status = $status_config[$site_status_level];
?>
|
|
|
|
|
|
= 300 && $code < 400) ? '#ffb900' : '#dc3232');
if ($code === 0) {
$code = '-';
$c_color = '#999';
}
echo '' . esc_html($code) . '';
?>
|
|
post_title); ?>
|
|
|
activetwist.com'
);
echo wp_kses_post($about_link);
?>
true], 'names');
$recent_posts = get_posts([
'post_type' => $post_types,
'posts_per_page' => 50,
'post_status' => 'publish'
]);
$rows = [];
$site_status_level = 'healthy';
// Checklist State (default healthy)
$checklist = [
'meta_desc' => ['label' => 'Meta descriptions present', 'status' => 'healthy'],
'og_image' => ['label' => 'Social images available', 'status' => 'healthy'],
'canonical' => ['label' => 'Canonical URLs set', 'status' => 'healthy'],
'canonical_conflict' => ['label' => 'Canonical Status (Observed)', 'status' => 'healthy'], // New Phase 4.1
'indexability_status' => ['label' => 'Indexability Config', 'status' => 'healthy'],
'http_status' => ['label' => 'HTTP Status (Loopback)', 'status' => 'healthy'],
'robots' => ['label' => 'Robots directives valid', 'status' => 'healthy'],
'jsonld' => ['label' => 'Structured data valid', 'status' => 'healthy'],
'local_business' => ['label' => 'Local Business Schema', 'status' => 'healthy'],
'author_schema' => ['label' => 'Author Schema details', 'status' => 'healthy'],
'breadcrumbs' => ['label' => 'Breadcrumbs enabled', 'status' => 'healthy'],
'xml_sitemap' => ['label' => 'XML Sitemap enabled', 'status' => 'healthy'],
'robots_txt' => ['label' => 'Robots.txt managed', 'status' => 'healthy'],
'llms_txt' => ['label' => 'LLMs.txt enabled', 'status' => 'healthy'],
];
// Global Checks
// 1. Local Business
if (\ActiveTwist\SEO\Settings\Options::get('local_business_enabled')) {
$lb_name = \ActiveTwist\SEO\Settings\Options::get('local_business_name') ?: \ActiveTwist\SEO\Settings\Options::get('org_name');
if (empty($lb_name)) {
$checklist['local_business']['status'] = 'blocking';
$site_status_level = 'action';
}
} else {
$checklist['local_business']['label'] .= ' (Disabled)';
}
// 2. XML Sitemap
if (!get_option('permalink_structure')) {
$checklist['xml_sitemap']['status'] = 'blocking';
$checklist['xml_sitemap']['label'] .= ' (Requires Permalinks)';
$site_status_level = 'action';
}
// 3. Robots.txt
$robots_mode = \ActiveTwist\SEO\Settings\Options::get('robots_txt_mode', 'auto');
if ($robots_mode === 'custom') {
$checklist['robots_txt']['label'] .= ' (Custom)';
}
// 4. LLMs.txt
// Default considered enabled (auto).
foreach ($recent_posts as $post) {
$row = [
'post' => $post,
'status_level' => 'healthy',
'issues' => []
];
// --- Observability Inspection (Phase 4.1) ---
// Perform loopback request to specific post permalink
$permalink = get_permalink($post->ID);
$inspection = \ActiveTwist\SEO\Utils\Inspector::inspect_url($permalink);
// Check Inspection Errors (e.g. timeout, connection refused)
// Check Inspection Errors (e.g. timeout, connection refused)
if (!empty($inspection['error'])) {
$row['issues'][] = ['type' => 'warning', 'message' => 'Inspection Failed: ' . $inspection['error']];
if ($row['status_level'] !== 'action') {
$row['status_level'] = 'attention';
}
if ($checklist['http_status']['status'] !== 'blocking')
$checklist['http_status']['status'] = 'warning';
$row['http_code'] = 0;
$row['indexability'] = ['status' => 'unknown', 'reason' => 'Scan Failed'];
} else {
// Canonical Conflict Detection
$expected_canonical = Helpers::get_canonical_url($post);
$observed_canonicals = $inspection['canonicals'];
if (count($observed_canonicals) > 1) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'Multiple Configured Canonicals'];
$row['status_level'] = 'action';
$checklist['canonical_conflict']['status'] = 'blocking';
} elseif (count($observed_canonicals) === 1) {
$observed = $observed_canonicals[0];
if ($observed !== $expected_canonical) {
$row['issues'][] = ['type' => 'warning', 'message' => 'Canonical Override Detected'];
// Non-blocking but noteworthy
if ($row['status_level'] !== 'action') {
$row['status_level'] = 'attention';
}
if ($checklist['canonical_conflict']['status'] !== 'blocking') {
$checklist['canonical_conflict']['status'] = 'warning';
}
}
} else {
// Count 0
$row['issues'][] = ['type' => 'blocking', 'message' => 'Missing Canonical Tag (Rendered)'];
$row['status_level'] = 'action';
$checklist['canonical_conflict']['status'] = 'blocking';
}
// Indexability Evaluation (Phase 4.1)
$indexability = \ActiveTwist\SEO\Utils\IndexabilityEvaluator::evaluate($inspection, $permalink);
$row['indexability'] = $indexability;
$row['http_code'] = $inspection['code']; // Explicit capture
// HTTP Status Checklist Logic
if ($inspection['code'] !== 200) {
if ($inspection['code'] >= 300 && $inspection['code'] < 400) {
if ($checklist['http_status']['status'] !== 'blocking')
$checklist['http_status']['status'] = 'warning';
} else {
// 404, 500
$checklist['http_status']['status'] = 'blocking';
$row['status_level'] = 'action';
}
}
if ($indexability['status'] === \ActiveTwist\SEO\Utils\IndexabilityEvaluator::STATUS_INDETERMINATE) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'Indexability Conflict: ' . $indexability['reason']];
$row['status_level'] = 'action';
$checklist['indexability_status']['status'] = 'blocking';
} elseif ($indexability['status'] === \ActiveTwist\SEO\Utils\IndexabilityEvaluator::STATUS_BLOCKED) {
// Blocked by robots.txt
$row['issues'][] = ['type' => 'warning', 'message' => 'Blocked from Indexing: ' . $indexability['reason']];
} elseif ($indexability['status'] === \ActiveTwist\SEO\Utils\IndexabilityEvaluator::STATUS_NOINDEX) {
// Check if this is an error or intentional
if ($inspection['code'] !== 200) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'HTTP ' . $inspection['code'] . ': ' . $indexability['reason']];
$row['status_level'] = 'action';
} else {
$row['issues'][] = ['type' => 'warning', 'message' => 'Not Indexable: ' . $indexability['reason']];
}
}
}
// --- Check Meta Description ---
$desc_data = Helpers::get_seo_description_with_source($post);
if ($desc_data['source'] === Helpers::SOURCE_MISSING) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'Missing Meta Description'];
$row['status_level'] = 'action';
$checklist['meta_desc']['status'] = 'blocking';
} elseif ($desc_data['source'] === Helpers::SOURCE_INHERITED) {
$row['issues'][] = ['type' => 'warning', 'message' => 'Uses global defaults'];
if ($row['status_level'] !== 'action') {
$row['status_level'] = 'attention';
}
if ($checklist['meta_desc']['status'] !== 'blocking') {
$checklist['meta_desc']['status'] = 'warning';
}
}
// --- Check Social Image ---
$img_data = Helpers::get_og_image_with_source($post);
if ($img_data['source'] === Helpers::SOURCE_MISSING) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'Missing Social Image'];
$row['status_level'] = 'action';
$checklist['og_image']['status'] = 'blocking';
} elseif ($img_data['source'] === Helpers::SOURCE_INHERITED) {
$row['issues'][] = ['type' => 'warning', 'message' => 'Uses global defaults'];
if ($row['status_level'] !== 'action') {
$row['status_level'] = 'attention';
}
if ($checklist['og_image']['status'] !== 'blocking') {
$checklist['og_image']['status'] = 'warning';
}
}
// --- Check JSON-LD ---
$meta = Helpers::get_post_meta($post->ID);
$mode = !empty($meta['jsonld_mode']) ? $meta['jsonld_mode'] : 'auto';
if ($mode === 'custom') {
if (empty($meta['jsonld_custom'])) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'JSON-LD Custom Mode Empty'];
$row['status_level'] = 'action';
$checklist['jsonld']['status'] = 'blocking';
} elseif (json_decode($meta['jsonld_custom']) === null) {
$row['issues'][] = ['type' => 'blocking', 'message' => 'Invalid JSON-LD Syntax'];
$row['status_level'] = 'action';
$checklist['jsonld']['status'] = 'blocking';
}
}
// Breadcrumbs check
if ($mode === 'none') {
$checklist['breadcrumbs']['status'] = 'blocking';
$checklist['breadcrumbs']['label'] = 'Breadcrumbs (Disabled)';
} elseif ($mode === 'custom') {
$checklist['breadcrumbs']['label'] = 'Breadcrumbs (Custom)';
}
// --- Check Author Schema ---
$author_job = get_the_author_meta('at_seo_job_title', $post->post_author);
if (empty($author_job)) {
$row['issues'][] = ['type' => 'warning', 'message' => 'Author missing Job Title'];
if ($row['status_level'] !== 'action') {
$row['status_level'] = 'attention';
}
if ($checklist['author_schema']['status'] !== 'blocking') {
$checklist['author_schema']['status'] = 'warning';
}
}
// Update Site Status based on Row Status
if ($row['status_level'] === 'action') {
$site_status_level = 'action';
} elseif ($row['status_level'] === 'attention' && $site_status_level !== 'action') {
$site_status_level = 'attention';
}
if (!empty($row['issues'])) {
$rows[] = $row;
}
}
return [
'rows' => $rows,
'site_status_level' => $site_status_level,
'checklist' => $checklist
];
}
public function handle_manual_refresh()
{
check_admin_referer('at_seo_manual_refresh');
// Logic to trigger re-scan
// For now, we update the timestamp and state.
// In a real async worker system, this would queue a job.
// Since our scan is deterministic and mostly on-the-fly or triggered here,
// we can just update the timestamp to indicate "freshness" of the manual action.
// The actual "scan" happens when rendering the page (scan_posts) currently,
// unless we move to a background process model.
// Phase 5 requirements say "Clears derived data caches, Re-runs discovery".
$options = Options::get_all();
$options['last_scan_timestamp'] = current_time('timestamp');
$options['scan_state'] = 'idle'; // It runs essentially instantly in this MVP architecture
update_option(Options::OPTION_NAME, $options);
// Redirect back
wp_safe_redirect(admin_url('admin.php?page=active-twist-seo-status&refreshed=1'));
exit;
}
}