$id, 'title' => $title, 'category' => $category, 'severity' => $severity, 'score_impact' => (int) $score_impact, 'status' => 'open', 'location' => $location, 'evidence' => $evidence, 'why_it_matters' => $why, 'recommendation_steps' => $steps, 'retest' => $retest, 'check_id' => $check_id, ]; } /** * v3.3.52 P1.1/P2.4 — shared indexable + parsed gate for content / AI / * schema / E-E-A-T quality checks. A page qualifies only when it is a real, * indexable, successfully-parsed HTML page: * • HTTP 2xx (200-class) → not an error / redirect / unreachable page * • not robots noindex → Google would actually index it * • _html_parsed !== false → the DOM really parsed (a degraded crawl * returns default signals: 0 JSON-LD, no OG, * etc., which would otherwise inflate every * "missing field" ratio into a false finding) * * Availability / crawl-error checks must NOT use this — they specifically * want the non-2xx rows. This is for "is the CONTENT good?" style checks so * that a transient 403/520 or a noindex utility page never pushes a * missing-field ratio up (e.g. ai_jsonld_missing, ai_og_tags_missing). * * @param array $pages * @return array> */ protected function indexable_parsed_sample_pages(array $pages): array { $out = []; foreach ($pages as $p) { if (is_array($p) && $this->is_indexable_parsed_page($p)) { $out[] = $p; } } return $out; } /** * @param array $p */ protected function is_parsed_html_page(array $p): bool { // 4.0.17: HTML-derived signals (viewport, OG, images, assets) exist only // on a 2xx response whose DOM parsed. A redirect, error or unreachable // row carries empty defaults and must never become a "missing" finding. // Unlike is_indexable_parsed_page(), noindex pages still count here. $code = (int) ($p['status_code'] ?? 0); if ($code < 200 || $code >= 300) { return false; } if (array_key_exists('html_analyzed', $p) && empty($p['html_analyzed'])) { return false; } if (!empty($p['password_protected'])) { return false; // 4.0.18: body hidden behind a password form. } if (array_key_exists('_html_parsed', $p) && $p['_html_parsed'] === false) { return false; } return true; } /** * @param array $pages * @return array> */ protected function parsed_html_pages(array $pages): array { $out = []; foreach ($pages as $p) { if (is_array($p) && $this->is_parsed_html_page($p)) { $out[] = $p; } } return $out; } /** * Single-page predicate behind indexable_parsed_sample_pages(). * * @param array $p */ protected function is_indexable_parsed_page(array $p): bool { $code = (int) ($p['status_code'] ?? 0); if ($code < 200 || $code >= 300) { return false; // error / redirect / unreachable — not real content. } if (!empty($p['robots_noindex'])) { return false; // explicitly de-indexed — not part of the index. } if (!empty($p['password_protected'])) { return false; // 4.0.18: body hidden behind a password form. } // _html_parsed is a strict bool from the collector. Absent key (legacy // rows / fixtures that predate the flag) is treated as parsed so a // shape change never silently drops every page. if (array_key_exists('_html_parsed', $p) && $p['_html_parsed'] === false) { return false; // looked like HTML but the DOM failed to parse. } return true; } }