, * qualitySignals: array{nameCount: int, urlCount: int, idCount: int}, * status: string * } */ public static function analyze_html(string $html): array { $blocks = 0; $valid = 0; $malformed = 0; $schema_org = 0; $types = []; $name_count = 0; $url_count = 0; $id_count = 0; if (!preg_match_all('/]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $html, $matches)) { return self::empty_result('absent'); } foreach ($matches[1] as $raw) { ++$blocks; $decoded = json_decode(html_entity_decode(trim((string) $raw), ENT_QUOTES | ENT_HTML5), true); if (!is_array($decoded)) { ++$malformed; continue; } ++$valid; if (self::has_recognized_schema_org_context($decoded)) { ++$schema_org; } self::collect_types_and_quality($decoded, $types, $name_count, $url_count, $id_count); } $types = array_values(array_unique(array_slice($types, 0, 8))); $status = 'absent'; if ($blocks > 0 && $valid === 0) { $status = 'malformed'; } elseif ($valid > 0 && $malformed > 0) { $status = 'mixed'; } elseif ($valid > 0 && $schema_org === 0) { $status = 'no_schema_org_context'; } elseif ($valid > 0) { $status = 'present'; } return [ 'blocks' => $blocks, 'validBlocks' => $valid, 'malformedBlocks' => $malformed, 'schemaOrgContext' => $schema_org, 'types' => $types, 'qualitySignals' => [ 'nameCount' => min($name_count, 8), 'urlCount' => min($url_count, 8), 'idCount' => min($id_count, 8), ], 'status' => $status, ]; } public static function is_recognized_schema_org_context_value(mixed $context): bool { if (is_string($context)) { return self::is_recognized_schema_org_context_string($context); } if (!is_array($context)) { return false; } foreach ($context as $key => $value) { if (is_string($key) && self::is_recognized_schema_org_context_string($key)) { return true; } if (is_string($value) && self::is_recognized_schema_org_context_string($value)) { return true; } } return false; } /** @param array $node */ private static function has_recognized_schema_org_context(array $node): bool { if (isset($node['@context']) && self::is_recognized_schema_org_context_value($node['@context'])) { return true; } foreach ($node as $child) { if (is_array($child) && self::has_recognized_schema_org_context($child)) { return true; } } return false; } private static function is_recognized_schema_org_context_string(string $value): bool { $trimmed = trim($value); if ($trimmed === 'schema.org') { return true; } return preg_match('#^https?://schema\.org/?$#i', $trimmed) === 1; } /** * @return array{ * blocks: int, * validBlocks: int, * malformedBlocks: int, * schemaOrgContext: int, * types: list, * qualitySignals: array{nameCount: int, urlCount: int, idCount: int}, * status: string * } */ private static function empty_result(string $status): array { return [ 'blocks' => 0, 'validBlocks' => 0, 'malformedBlocks' => 0, 'schemaOrgContext' => 0, 'types' => [], 'qualitySignals' => ['nameCount' => 0, 'urlCount' => 0, 'idCount' => 0], 'status' => $status, ]; } /** * @param array $node * @param list $types */ private static function collect_types_and_quality(array $node, array &$types, int &$name_count, int &$url_count, int &$id_count): void { if (isset($node['@type'])) { $value = $node['@type']; if (is_string($value)) { $types[] = $value; } elseif (is_array($value)) { foreach ($value as $item) { if (is_string($item)) { $types[] = $item; } } } } if (isset($node['name']) && is_string($node['name'])) { ++$name_count; } if (isset($node['url']) && is_string($node['url'])) { ++$url_count; } if (isset($node['@id']) && is_string($node['@id'])) { ++$id_count; } foreach ($node as $child) { if (is_array($child)) { self::collect_types_and_quality($child, $types, $name_count, $url_count, $id_count); } } } }