get('sample_pages', []); $robots = (array) $ctx->get('robots', []); $robots_status = (int) ($robots['status_code'] ?? 0); $robots_body = (string) ($robots['body_sample'] ?? ''); $llms = (array) $ctx->get('llms', []); $llms_status = (int) ($llms['status_code'] ?? 0); $llms_body = trim((string) ($llms['body_sample'] ?? '')); // v1.0.88: Agent-discovery surface signals. // Note: HttpCollector stores content-type inside the headers array as // headers['content-type'] (lowercase). We extract it via a small helper. $extract_ctype = static function (array $res): string { $headers = isset($res['headers']) && is_array($res['headers']) ? $res['headers'] : []; $ct = ''; if (isset($headers['content-type'])) { $ct = is_array($headers['content-type']) ? (string) end($headers['content-type']) : (string) $headers['content-type']; } elseif (isset($headers['Content-Type'])) { $ct = is_array($headers['Content-Type']) ? (string) end($headers['Content-Type']) : (string) $headers['Content-Type']; } return strtolower(trim((string) $ct)); }; $api_catalog = (array) $ctx->get('api_catalog', []); $api_catalog_status = (int) ($api_catalog['status_code'] ?? 0); $api_catalog_ctype = $extract_ctype($api_catalog); $agent_skills = (array) $ctx->get('agent_skills', []); $agent_skills_status = (int) ($agent_skills['status_code'] ?? 0); $agent_skills_ctype = $extract_ctype($agent_skills); $markdown_neg = (array) $ctx->get('markdown_negotiation', []); $markdown_neg_status = (int) ($markdown_neg['status_code'] ?? 0); $markdown_neg_ctype = $extract_ctype($markdown_neg); // ─── 1. robots.txt — AI bot access ──────────────────────────────────────── if ($robots_status >= 200 && $robots_status < 300 && $robots_body !== '') { // v3.3.48 — detect Content-signal vs. AI-bot Disallow conflict. // Round-3 review (Claude) flagged sites that ship both // `Content-signal: ai-input=yes` AND `User-agent: GPTBot \n // Disallow: /`. The two directives are mutually contradictory: // one promises AI may consume the page, the other forbids the // most prominent AI crawler from fetching it. We flag the // conflict so the customer can pick a consistent policy. $has_ai_input_yes = preg_match('/^content-signal\s*:\s*[^\n]*ai-input\s*=\s*yes/im', $robots_body) === 1; $disallowed_ai_bots_for_conflict = $this->detect_ai_bot_disallows($robots_body); if ($has_ai_input_yes && !empty($disallowed_ai_bots_for_conflict)) { $findings[] = $this->finding( 'ai_robots_policy_conflict_ai_input_vs_disallow', I18n::tr('robots.txt conflict — Content-signal allows AI, but User-agent blocks the bots', 'تعارض في ملف robots.txt: إشارة المحتوى تسمح للذكاء الاصطناعي بينما قواعد الزواحف تحجبه'), 'AI-Friendly', 'med', -5, [ 'source' => 'http', 'details' => I18n::tr('The robots.txt file has `Content-signal: ai-input=yes` (allowing AI input) while at the same time it has `User-agent: GPTBot \\n Disallow: /` (or similar). AI engines read this conflict as a manual block — the same negative effect as Disallow.', 'يحتوي ملف robots.txt على إشارة تسمح باستخدام المحتوى (`Content-signal: ai-input=yes`)، ويحتوي في الوقت نفسه على قاعدة تحجب زاحفًا للذكاء الاصطناعي مثل `User-agent: GPTBot \n Disallow: /`. تتعامل خدمات الذكاء الاصطناعي مع هذا التعارض كحجب متعمد.'), 'samples' => [ 'content_signal' => 'ai-input=yes', 'conflicting_blocks' => array_slice($disallowed_ai_bots_for_conflict, 0, 5), ], ], I18n::tr('Content-signal positions itself as a future-facing standard for allowing AI, but it does not override the classic User-agent block. Crawlers prefer the stricter rule. The result: mixed intent with no real gain.', 'إشارة المحتوى معيار حديث للسماح للذكاء الاصطناعي، لكنها لا تلغي قاعدة الحجب التقليدية. تلتزم الزواحف بالقاعدة الأشد، فلا يتحقق أي مكسب.'), [ I18n::tr('Decide on one consistent policy: if you will allow AI, remove the Disallow / User-agent: GPTBot block.', 'اختر سياسة واحدة: إذا كنت تريد السماح لخدمات الذكاء الاصطناعي فاحذف قاعدة الحجب (User-agent: GPTBot ثم Disallow).'), I18n::tr('If you will block, remove Content-signal: ai-input=yes (AI engines treat it as misleading).', 'وإذا كنت تريد الحجب فاحذف إشارة السماح (Content-signal: ai-input=yes) حتى لا تكون الرسالة متناقضة.'), I18n::tr('Usually cleanest: keep Disallow only on sensitive endpoints (/wp-admin/, /private/), and unblock public content if you want AI Overviews visibility.', 'الخيار الأنظف غالبًا: احجب المسارات الحساسة فقط مثل /wp-admin/، واسمح بالمحتوى العام إذا كنت تريد الظهور في إجابات الذكاء الاصطناعي.'), ], [ 'how' => I18n::tr('After updating robots.txt, run a new audit.', 'بعد تعديل ملف robots.txt شغّل فحصًا جديدًا.'), 'pass_condition' => I18n::tr('One consistent policy — either block AI entirely, or allow it without conflict.', 'سياسة موحدة — إما يحجب AI بالكامل، أو يسمح بدون تناقض.'), ], $this->id() ); } $blocked_ai_bots = $this->detect_ai_bot_disallows($robots_body); if (!empty($blocked_ai_bots)) { $severity = count($blocked_ai_bots) >= 3 ? 'high' : (count($blocked_ai_bots) >= 2 ? 'med' : 'low'); $impact = $severity === 'high' ? -8 : ($severity === 'med' ? -5 : -2); $findings[] = $this->finding( 'ai_robots_blocks_major_bots', I18n::tr('robots.txt blocks major AI bots', 'ملفات robots.txt تحجب بوتات AI رئيسية'), 'AI-Friendly', $severity, $impact, [ 'source' => 'http', 'details' => I18n::tr('Disallow detected for AI crawler user-agents in robots.txt', 'ملف robots.txt يحجب زواحف الذكاء الاصطناعي'), 'samples' => $blocked_ai_bots, ], I18n::tr('Blocking AI bots stops the site from appearing in answers from ChatGPT, Perplexity, Google AI, and others.', 'حجب زواحف الذكاء الاصطناعي يمنع موقعك من الظهور في إجابات ChatGPT وPerplexity وجوجل وغيرها.'), [ I18n::tr('Review the user-agent groups in robots.txt for the blocked bots.', 'راجع قواعد الزواحف المحجوبة في ملف robots.txt.'), I18n::tr('If the block is unintended: remove or edit the relevant lines.', 'إن كان الحجب غير مقصود: احذف أو عدّل السطور المعنية.'), I18n::tr('Example to allow GPTBot: User-agent: GPTBot | Allow: /', 'مثال على السماح لزاحف ChatGPT: User-agent: GPTBot ثم Allow: /'), ], [ 'how' => I18n::tr('Run a new audit after updating robots.txt.', 'شغّل فحصًا جديدًا بعد تعديل ملف robots.txt.'), 'pass_condition' => I18n::tr('No AI crawler is blocked from public content by mistake.', 'لا يوجد حجب غير مقصود لزواحف الذكاء الاصطناعي عن المحتوى العام.'), ], $this->id() ); } } // ─── 2. llms.txt ────────────────────────────────────────────────────────── if ($llms_status !== 200 && $llms_status !== 0) { $findings[] = $this->finding( 'ai_llms_txt_missing_or_unreachable', I18n::tr('The llms.txt file is missing (optional)', 'ملف llms.txt غير موجود (اختياري)'), 'AI-Friendly', 'info', 0, [ 'source' => 'http', 'details' => sprintf(I18n::tr('The llms.txt file answered with HTTP status %s.', 'أعاد ملف llms.txt رمز الحالة %s.'), (string) $llms_status), 'samples' => [['url' => home_url('/llms.txt'), 'status' => $llms_status]], ], I18n::tr('llms.txt is an optional, proposed convention that gives AI tools a short summary of the site and its key pages. No search engine requires it, and Google says its AI features need no special AI text file.', 'ملف llms.txt اقتراح اختياري يقدّم لأدوات الذكاء الاصطناعي ملخصًا قصيرًا عن الموقع وأهم صفحاته. لا يشترطه أي محرك بحث، وتذكر جوجل أن ميزات الذكاء الاصطناعي لديها لا تحتاج ملفًا خاصًا.'), [ I18n::tr('Create /llms.txt containing: a site description, the main topics, the important pages, and a usage policy.', 'أنشئ /llms.txt يحتوي على: وصف الموقع، المواضيع الرئيسية، الصفحات المهمة، وسياسة الاستخدام.'), I18n::tr('Example: # Site Context\\n> site_desc: store...\\n## Key Pages\\n- /about\\n- /services', 'مثال: # Site Context\n> site_desc: متجر...\n## Key Pages\n- /about\n- /services'), ], [ 'how' => I18n::tr('Check /llms.txt in the browser.', 'تحقق من /llms.txt في المتصفح.'), 'pass_condition' => I18n::tr('llms.txt returns 200 with useful content.', 'llms.txt يرجع 200 مع محتوى مفيد.'), ], $this->id() ); } elseif ($llms_body === '') { $findings[] = $this->finding( 'ai_llms_txt_empty', I18n::tr('llms.txt present but empty', 'llms.txt موجود لكن فارغ'), 'AI-Friendly', 'info', -1, [ 'source' => 'http', 'details' => I18n::tr('The llms.txt file exists but is empty.', 'ملف llms.txt موجود لكنه فارغ.'), 'samples' => [['url' => home_url('/llms.txt')]], ], I18n::tr('An empty llms.txt file gives AI models nothing useful.', 'ملف llms.txt فارغ لا يفيد نماذج الذكاء الاصطناعي شيئاً.'), [ I18n::tr('Add content: a site description, services, important pages, and sitemap links.', 'أضف محتوى: وصف الموقع، الخدمات، الصفحات المهمة، وروابط sitemap.'), ], [ 'how' => I18n::tr('Check /llms.txt after updating.', 'تحقق من /llms.txt بعد التحديث.'), 'pass_condition' => I18n::tr('llms.txt contains useful descriptive content.', 'llms.txt يحتوي محتوى تعريفي مفيد.'), ], $this->id() ); } elseif (strlen($llms_body) < 100) { $findings[] = $this->finding( 'ai_llms_txt_too_short', I18n::tr('llms.txt too short and needs more content', 'llms.txt قصير جداً ويحتاج محتوى أكثر'), 'AI-Friendly', 'info', 0, [ 'source' => 'http', 'details' => sprintf(I18n::tr('Characters in llms.txt: only %d (at least 100 expected).', 'عدد الأحرف في ملف llms.txt: %d فقط، والمتوقع 100 على الأقل.'), strlen($llms_body)), 'snippet' => $llms_body, ], I18n::tr('An effective llms.txt file contains enough information to help AI models understand the site.', 'ملف llms.txt الفعّال يحتوي على معلومات كافية تساعد نماذج AI على فهم الموقع.'), [ I18n::tr('Add: a site description, keywords, the main pages, and related entities.', 'أضف: وصف الموقع، الكلمات المفتاحية، الصفحات الرئيسية، والكيانات المرتبطة.'), ], [ 'how' => I18n::tr('Re-run the audit after updating llms.txt.', 'شغّل فحصًا جديدًا بعد تحديث ملف llms.txt.'), 'pass_condition' => I18n::tr('llms.txt contains more than 100 characters.', 'llms.txt يحتوي أكثر من 100 حرف.'), ], $this->id() ); } // ─── Per-page analysis ──────────────────────────────────────────────────── $any_jsonld = false; $types_all = []; $has_faq = false; $has_howto = false; $has_org = false; $has_same_as = false; $has_author_schema = false; $structure_total = 0; $word_total = 0; $dl_total = 0; $h2_total = 0; $h3_total = 0; $og_missing = []; $twitter_missing = []; $no_h2_pages = []; $no_author_pages = []; $no_freshness_pages = []; $no_outbound_pages = []; $low_heading_depth = []; $has_breadcrumb_schema = false; $has_breadcrumb_nav = false; $has_publisher_schema = false; $no_breadcrumb_pages = []; // v3.3.52 P1.1 — judge AI-friendliness only on real, indexable, parsed // content pages. Error / redirect / noindex / unparsed rows would // otherwise inflate every "missing OG / JSON-LD / author" ratio into a // false finding even when the indexable content is actually fine. $content_pages = $this->indexable_parsed_sample_pages($pages); // Threshold-specific denominators so each ratio divides by the pages // the rule actually applies to, not by the whole sample. $content_pages_300 = 0; // indexable pages with >= 300 words. $content_pages_500 = 0; // indexable pages with >= 500 words. foreach ($content_pages as $p) { if (!is_array($p)) { continue; } $p_url = (string) ($p['url'] ?? ''); $wc = (int) ($p['word_count'] ?? 0); if ($wc >= 300) { $content_pages_300++; } if ($wc >= 500) { $content_pages_500++; } $h1 = (int) ($p['h1_count'] ?? 0); $h2 = (int) ($p['h2_count'] ?? 0); $h3 = (int) ($p['h3_count'] ?? 0); $word_total += $wc; $structure_total += (int) ($p['list_count'] ?? 0) + (int) ($p['table_count'] ?? 0); $dl_total += (int) ($p['dl_count'] ?? 0); $h2_total += $h2; $h3_total += $h3; // H2/H3 structure — passage indexing readiness if ($h1 > 0 && $wc >= 300 && $h2 === 0) { $no_h2_pages[] = ['url' => $p_url, 'word_count' => $wc, 'h1' => $h1, 'h2' => 0]; } if ($h1 > 0 && $wc >= 600 && ($h2 + $h3) < 2) { $low_heading_depth[] = ['url' => $p_url, 'word_count' => $wc, 'h2' => $h2, 'h3' => $h3]; } // Open Graph $og_title = (string) ($p['og_title'] ?? ''); $og_image = (string) ($p['og_image'] ?? ''); if ($og_title === '' || $og_image === '') { $og_missing[] = [ 'url' => $p_url, 'og_title' => $og_title !== '' ? 'ok' : 'missing', 'og_image' => $og_image !== '' ? 'ok' : 'missing', ]; } // Twitter Card $tw_card = (string) ($p['twitter_card'] ?? ''); if ($tw_card === '') { $twitter_missing[] = ['url' => $p_url]; } // Author signals $author_name = (string) ($p['author_name'] ?? ''); $has_auth_link = !empty($p['has_author_link']); if ($wc >= 300 && $author_name === '' && !$has_auth_link) { $no_author_pages[] = ['url' => $p_url, 'word_count' => $wc]; } // Content freshness $pub_time = (string) ($p['article_published_time'] ?? ''); $mod_time = (string) ($p['article_modified_time'] ?? ''); if ($wc >= 300 && $pub_time === '' && $mod_time === '') { $no_freshness_pages[] = ['url' => $p_url, 'word_count' => $wc]; } // Outbound authority links $outbound = (int) ($p['outbound_links_count'] ?? 0); if ($wc >= 500 && $outbound === 0) { $no_outbound_pages[] = ['url' => $p_url, 'word_count' => $wc]; } // JSON-LD $types = isset($p['jsonld_types']) && is_array($p['jsonld_types']) ? $p['jsonld_types'] : []; if (!empty($types)) { $any_jsonld = true; foreach ($types as $t) { if (!is_string($t)) { continue; } $tt = strtolower($t); $types_all[$tt] = 1; if ($tt === 'faqpage') { $has_faq = true; } if ($tt === 'howto') { $has_howto = true; } if (in_array($tt, ['organization', 'localbusiness', 'person'], true)) { $has_org = true; } } } // Check raw JSON-LD for sameAs and author $raw_blocks = isset($p['jsonld_raw']) && is_array($p['jsonld_raw']) ? $p['jsonld_raw'] : []; foreach ($raw_blocks as $block) { if (!is_array($block)) { continue; } if ($this->jsonld_has_key($block, 'sameAs')) { $has_same_as = true; } if ($this->jsonld_has_key($block, 'author')) { $has_author_schema = true; } if ($this->jsonld_has_key($block, 'publisher')) { $has_publisher_schema = true; } } // v1.0.58: also accept "Person" entity in @graph as proof of author // signal. Yoast/Rank Math emit Article with `"author": {"@id": ...}` // pointing to a separate Person node — the recursive `author` key // search above usually catches it now, but a standalone Person is // also a valid E-E-A-T signal. $jsonld_types = isset($p['jsonld_types']) && is_array($p['jsonld_types']) ? $p['jsonld_types'] : []; foreach ($jsonld_types as $jt) { if (strcasecmp((string) $jt, 'Person') === 0) { $has_author_schema = true; break; } } // Breadcrumb signals if (!empty($p['has_breadcrumb_schema'])) { $has_breadcrumb_schema = true; } if (!empty($p['has_breadcrumb_nav'])) { $has_breadcrumb_nav = true; } if (empty($p['has_breadcrumb_schema']) && empty($p['has_breadcrumb_nav'])) { $no_breadcrumb_pages[] = ['url' => $p_url]; } } $page_count = count($content_pages); // v3.3.52 P1.1 — indexable+parsed only. // ─── 3. JSON-LD Schema missing entirely ─────────────────────────────────── if (!$any_jsonld && $page_count > 0) { $findings[] = $this->finding( 'ai_jsonld_missing', I18n::tr('The checked pages have no structured data', 'لا توجد بيانات منظمة (JSON-LD) في الصفحات المفحوصة'), 'AI-Friendly', 'med', -6, [ 'source' => 'html', 'details' => I18n::tr('No JSON-LD structured-data script tags in checked pages', 'لا توجد وسوم JSON-LD للبيانات المنظمة في صفحات العينة'), 'samples' => array_map(static function ($p) { return ['url' => (string) ($p['url'] ?? '')]; }, array_slice($content_pages, 0, 5)), ], I18n::tr('JSON-LD schema helps search engines and AI models understand the site\'s entities and content directly.', 'البيانات المنظمة تساعد محركات البحث والذكاء الاصطناعي على فهم موقعك ومحتواه مباشرة.'), [ I18n::tr('Use your SEO plugin to add structured data automatically.', 'استخدم إضافة تحسين محركات البحث لديك لإضافة البيانات المنظمة تلقائيًا.'), I18n::tr('Add at least: Organization/WebSite on the homepage, WebPage/Article on articles.', 'أضف على الأقل: Organization/WebSite في الصفحة الرئيسية، WebPage/Article في المقالات.'), I18n::tr('Validate the markup at: https://validator.schema.org/', 'تحقق من صحة البيانات عبر: https://validator.schema.org/'), ], [ 'how' => I18n::tr('Run the audit after adding schema.', 'شغّل فحصًا جديدًا بعد إضافة البيانات المنظمة.'), 'pass_condition' => I18n::tr('At least one checked page has structured data (JSON-LD).', 'وجود jsonld_types غير فارغ في صفحة عينة واحدة على الأقل.'), ], $this->id() ); } // ─── 4. Organization/LocalBusiness schema missing ───────────────────────── if ($any_jsonld && !$has_org) { $findings[] = $this->finding( 'ai_org_schema_not_detected', I18n::tr('Organization entity schema (Organization/LocalBusiness) not detected', 'لا توجد بيانات تعريف بالمنشأة (Organization أو LocalBusiness)'), 'AI-Friendly', 'med', -4, [ 'source' => 'html', 'details' => I18n::tr('Detected types: ', 'الأنواع المرصودة: ') . implode(', ', array_keys($types_all)), ], I18n::tr('Organization schema states the business name, logo and profiles in a form search engines can read. It is standard structured data, not a guarantee of being mentioned in AI answers.', 'بيانات المنشأة تذكر اسم النشاط وشعاره وحساباته بصيغة تقرؤها محركات البحث. هي بيانات منظمة معتادة ولا تضمن ذكر الموقع في إجابات الذكاء الاصطناعي.'), [ I18n::tr('Add Organization schema on the homepage containing: name, url, logo, description.', 'أضف بيانات المنشأة (Organization) في الصفحة الرئيسية، وتشمل الاسم والرابط والشعار والوصف.'), I18n::tr('Example in JSON-LD: {"@type":"Organization","name":"Company name","url":"...","logo":"..."}', 'مثال في JSON-LD: {"@type":"Organization","name":"اسم الشركة","url":"...","logo":"..."}'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('The homepage has Organization or LocalBusiness data.', 'تحتوي الصفحة الرئيسية على بيانات Organization أو LocalBusiness.'), ], $this->id() ); } // ─── 5. sameAs links missing (Brand entity disambiguation) ──────────────── if ($any_jsonld && $has_org && !$has_same_as) { $findings[] = $this->finding( 'ai_org_same_as_missing', I18n::tr('sameAs field missing from the Organization schema', 'بيانات المنشأة لا تربطها بحساباتها الرسمية (sameAs)'), 'AI-Friendly', 'low', -2, [ 'source' => 'html', 'details' => I18n::tr('Organization schema detected but no sameAs property found', 'توجد بيانات المنشأة لكن دون روابط حساباتها الرسمية (sameAs)'), ], I18n::tr('The sameAs field links the organization to its profiles on the Google Knowledge Graph and social media, strengthening entity disambiguation.', 'روابط الحسابات الرسمية تربط منشأتك بصفحتها في جوجل وبحساباتها في مواقع التواصل، فيتأكد محرك البحث أنها الكيان نفسه.'), [ I18n::tr('Add sameAs to the Organization schema with links: Google Business, LinkedIn, Facebook, Twitter/X.', 'أضف روابط الحسابات الرسمية في بيانات المنشأة: ملف جوجل التجاري ولينكدإن وفيسبوك وإكس.'), I18n::tr('Example: "sameAs": ["https://www.facebook.com/yourpage", "https://twitter.com/yourhandle"]', 'مثال: "sameAs": ["https://www.facebook.com/yourpage", "https://twitter.com/yourhandle"]'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('Organization data includes valid links to the official profiles.', 'تحتوي بيانات المنشأة على روابط صحيحة لحساباتها الرسمية.'), ], $this->id() ); } // ─── 6. Author schema missing (E-E-A-T) ─────────────────────────────────── if ($any_jsonld && !$has_author_schema && $page_count > 0) { $article_pages = array_filter($content_pages, static function ($p) { // v3.3.52 P1.1 if (!is_array($p)) { return false; } $types = isset($p['jsonld_types']) && is_array($p['jsonld_types']) ? $p['jsonld_types'] : []; foreach ($types as $t) { $t = strtolower((string) $t); if (in_array($t, ['article', 'newsarticle', 'blogposting', 'technicalarticle'], true)) { return true; } } return false; }); if (!empty($article_pages)) { $findings[] = $this->finding( 'ai_author_schema_missing', I18n::tr('No author in article schema (E-E-A-T)', 'بيانات المقالات لا تذكر الكاتب'), 'AI-Friendly', 'med', -4, [ 'source' => 'html', 'details' => I18n::tr('Article schema detected but no author property found', 'توجد بيانات المقال (Article) لكن دون اسم الكاتب'), 'samples' => array_map(static function ($p) { return ['url' => (string) ($p['url'] ?? '')]; }, array_slice(array_values($article_pages), 0, 5)), ], I18n::tr('The author field in schema establishes the writer\'s identity and supports E-E-A-T, which Google and LLMs rely on to assess trustworthiness.', 'اسم الكاتب في بيانات المقال يثبت من كتبه، ويدعم تقييم الخبرة والموثوقية الذي تعتمد عليه جوجل ونماذج الذكاء الاصطناعي.'), [ I18n::tr('Add author to the Article schema: {"@type":"Person","name":"Author name","url":"Author page URL"}', 'أضف الكاتب إلى بيانات المقال: {"@type":"Person","name":"اسم الكاتب","url":"رابط صفحة الكاتب"}'), I18n::tr('Make sure every author has a clear author page with a short bio.', 'تأكد أن كل كاتب لديه صفحة مؤلف واضحة مع سيرة ذاتية مختصرة.'), ], [ 'how' => I18n::tr('Run the audit after adding author to the Article schema.', 'شغّل فحصًا جديدًا بعد إضافة الكاتب.'), 'pass_condition' => I18n::tr('Article data includes the author.', 'تحتوي بيانات المقالات على اسم الكاتب.'), ], $this->id() ); } } // ─── 7. No H2 headings (Passage indexing readiness) ─────────────────────── if (!empty($no_h2_pages)) { $findings[] = $this->finding( 'ai_missing_h2_structure', I18n::tr('Content lacks H2 headings (Passage Indexing)', 'محتوى يفتقر لعناوين H2 (Passage Indexing)'), 'AI-Friendly', 'med', -4, [ 'source' => 'html', 'details' => I18n::tr('Pages with H1 + 300+ words but no H2 found', 'صفحات فيها H1 وأكثر من 300 كلمة بلا أي H2'), 'samples' => array_slice($no_h2_pages, 0, 8), ], I18n::tr('H2/H3 headings split content into parts that AI models can extract as \'passages\' and direct answers. Without them, accurate extraction is hard.', 'العناوين الفرعية (H2 وH3) تقسم المحتوى إلى أجزاء يستطيع الذكاء الاصطناعي اقتباسها كإجابات مباشرة، ومن دونها يصعب الاقتباس الدقيق.'), [ I18n::tr('Split every long piece of content (300+ words) into sections with clear H2 headings.', 'قسّم كل محتوى طويل (300+ كلمة) إلى أقسام بعناوين H2 واضحة.'), I18n::tr('Make each H2 answer a specific question: "How to do X" or "What is X".', 'اجعل عنوان H2 يجيب على سؤال محدد: "كيف تفعل X" أو "ما هو X".'), I18n::tr('Use H3 under H2 for details — this helps extract precise answers.', 'H3 تحت H2 للتفاصيل — هذا يساعد على استخراج إجابات دقيقة.'), ], [ 'how' => I18n::tr('Run the audit after adding H2s.', 'شغّل فحصًا جديدًا بعد إضافة العناوين الفرعية.'), 'pass_condition' => I18n::tr('H2 present on long-content pages.', 'وجود H2 في صفحات المحتوى الطويل.'), ], $this->id() ); } // ─── 8. Low heading depth on long content ───────────────────────────────── if (!empty($low_heading_depth) && empty($no_h2_pages)) { $findings[] = $this->finding( 'ai_shallow_heading_depth', I18n::tr('Long content needs more subheadings (H2/H3)', 'المحتوى الطويل يحتاج عناوين فرعية أكثر (H2/H3)'), 'AI-Friendly', 'info', -1, [ 'source' => 'html', 'details' => I18n::tr('Pages 600+ words with fewer than 2 H2/H3 headings', 'صفحات تتجاوز 600 كلمة وفيها أقل من عنوانين H2/H3'), 'samples' => array_slice($low_heading_depth, 0, 5), ], I18n::tr('Clear H2/H3 headings split long content into sections that readers and search engines can scan and link to.', 'العناوين الفرعية الواضحة (H2 وH3) تقسّم المحتوى الطويل إلى أقسام يسهل على القارئ ومحركات البحث تصفحها والوصول إليها.'), [ I18n::tr('Every 300-400 words there should ideally be a new H2 or H3.', 'كل 300-400 كلمة يفضل أن يكون هناك H2 أو H3 جديد.'), I18n::tr('Make headings accurately describe the section that follows.', 'اجعل العناوين تصف الجزء الذي يليها بدقة.'), ], [ 'how' => I18n::tr('Run the audit after improving the heading structure.', 'شغّل فحصًا جديدًا بعد تحسين ترتيب العناوين.'), 'pass_condition' => I18n::tr('Pages with 600+ words include 2+ H2/H3.', 'صفحات 600+ كلمة تحتوي 2+ H2/H3.'), ], $this->id() ); } // ─── 9. FAQ/HowTo schema ────────────────────────────────────────────────── if (!$has_faq && !$has_howto && $page_count > 0) { $findings[] = $this->finding( 'ai_faq_howto_not_detected', I18n::tr('No FAQ or how-to data on the checked pages (optional)', 'لا توجد بيانات أسئلة شائعة أو خطوات في الصفحات المفحوصة (اختياري)'), 'AI-Friendly', 'info', 0, [ 'source' => 'html', 'details' => I18n::tr('No FAQPage or HowTo structured data found', 'لم يُرصد FAQPage/HowTo ضمن jsonld_types'), ], I18n::tr('FAQPage and HowTo data describe questions or steps that are already visible on the page. They are optional: Google shows FAQ rich results only for a few site types and says its AI features need no special schema.', 'بيانات الأسئلة الشائعة والخطوات تصف أسئلة أو خطوات ظاهرة فعلًا في الصفحة. هي اختيارية، فجوجل تعرض نتائج الأسئلة الشائعة لأنواع قليلة من المواقع وتذكر أن ميزات الذكاء الاصطناعي لا تحتاج بيانات خاصة.'), [ I18n::tr('On pages with questions/answers: add FAQPage schema.', 'في الصفحات التي تحتوي أسئلة وأجوبة أضف بيانات الأسئلة الشائعة (FAQPage).'), I18n::tr('On tutorial/step pages: add HowTo schema.', 'في الصفحات التعليمية أو صفحات الخطوات أضف بيانات الخطوات (HowTo).'), I18n::tr('Most SEO plugins let you add this data from the post editor.', 'أغلب إضافات تحسين محركات البحث تتيح إضافة هذه البيانات من محرر المقال.'), ], [ 'how' => I18n::tr('Run the audit after adding FAQ/HowTo schema.', 'شغّل فحصًا جديدًا بعد إضافة البيانات.'), 'pass_condition' => I18n::tr('A checked page has FAQPage or HowTo data.', 'تحتوي صفحة مفحوصة على بيانات FAQPage أو HowTo.'), ], $this->id() ); } // ─── 10. Open Graph missing — owned by TechnicalSeoCheck ────────────────── // Removed the duplicate ai_og_tags_missing: TechnicalSeoCheck emits // seo_og_tags_missing for the exact same condition, and it ships in BOTH // editions — so the two fired together (Technical SEO −4 + AI-Friendly −4) // for one root cause. TechnicalSeoCheck is the single owner now. // ─── 11. Twitter Card missing ───────────────────────────────────────────── $tw_missing_ratio = $page_count > 0 ? count($twitter_missing) / $page_count : 0; if ($tw_missing_ratio > 0.6 && $page_count > 1) { $findings[] = $this->finding( 'ai_twitter_card_missing', I18n::tr('Most pages have no X (Twitter) share card', 'أغلب الصفحات بلا بطاقة مشاركة لمنصة إكس (Twitter Card)'), 'AI-Friendly', 'info', -1, [ 'source' => 'html', 'details' => sprintf(I18n::tr('%.0f%% of the checked pages have no X (Twitter) share card (twitter:card).', '%.0f%% من الصفحات المفحوصة بلا بطاقة مشاركة لمنصة إكس (twitter:card).'), $tw_missing_ratio * 100), 'samples' => array_slice($twitter_missing, 0, 5), ], I18n::tr('Twitter Cards control the title, description and image shown when a link is shared on X.', 'بطاقة المشاركة تحدد العنوان والوصف والصورة التي تظهر عند مشاركة الرابط في منصة إكس.'), [ I18n::tr('Enable Twitter Cards in your SEO plugin — usually under the same Social settings.', 'فعّل بطاقات منصة إكس في إضافة تحسين محركات البحث، وتكون غالبًا ضمن إعدادات المشاركة الاجتماعية.'), I18n::tr('Use summary_large_image for the best display.', 'استخدم summary_large_image لأفضل عرض.'), ], [ 'how' => I18n::tr('Run the audit after enabling Twitter Cards.', 'شغّل فحصًا جديدًا بعد التفعيل.'), 'pass_condition' => I18n::tr('Most pages have an X (Twitter) card.', 'أغلب الصفحات تحتوي بطاقة مشاركة لمنصة إكس.'), ], $this->id() ); } // ─── 12. Author signals missing (E-E-A-T) ──────────────────────────────── // v3.3.52 P1.1 — divide by content pages (300+ words), the set the // author rule actually applies to (not the whole sample). $auth_missing_ratio = $content_pages_300 > 0 ? count($no_author_pages) / $content_pages_300 : 0; if ($auth_missing_ratio > 0.5 && $content_pages_300 > 1) { $findings[] = $this->finding( 'ai_author_signals_missing', I18n::tr('Most pages do not name their author', 'أغلب الصفحات لا تذكر كاتبها'), 'AI-Friendly', 'med', -4, [ 'source' => 'html', 'details' => sprintf(I18n::tr('%.0f%% of content pages (300+ words) show no author name or author link.', '%.0f%% من صفحات المحتوى (300 كلمة فأكثر) بلا اسم كاتب أو رابط لصفحته.'), $auth_missing_ratio * 100), 'samples' => array_slice($no_author_pages, 0, 5), ], I18n::tr('E-E-A-T (Experience/Expertise/Authoritativeness/Trustworthiness) is one of Google\'s most important criteria for evaluating content. A missing author weakens trust.', 'الخبرة والموثوقية من أهم ما تقيّم به جوجل المحتوى، وغياب اسم الكاتب يضعف الثقة فيه.'), [ I18n::tr('Add the author\'s name and link it to an author page on every article.', 'أضف اسم الكاتب وربطه بصفحة مؤلف في كل مقال.'), I18n::tr('The author page includes: a bio, expertise, and professional account links (LinkedIn).', 'صفحة المؤلف تحتوي: السيرة الذاتية، الخبرات، روابط الحسابات المهنية (LinkedIn).'), I18n::tr('In WordPress, show the author in your theme settings or with an author plugin.', 'في ووردبريس فعّل عرض الكاتب من إعدادات القالب أو بإضافة للكتّاب.'), ], [ 'how' => I18n::tr('Run the audit after adding author data.', 'شغّل فحصًا جديدًا بعد إضافة بيانات الكاتب.'), 'pass_condition' => I18n::tr('Content pages show an author name or author link.', 'تعرض صفحات المحتوى اسم الكاتب أو رابط صفحته.'), ], $this->id() ); } // ─── 13. Content freshness signals missing ──────────────────────────────── // v3.3.52 P1.1 — divide by content pages (300+ words). // Edition-aware: the Pro FreshnessCheck owns freshness (freshness_dates_missing // with richer staleness tiers). This lightweight signal only fires in the // free build where FreshnessCheck is absent — so the two never double-count. $fresh_missing_ratio = $content_pages_300 > 0 ? count($no_freshness_pages) / $content_pages_300 : 0; if ($fresh_missing_ratio > 0.6 && $content_pages_300 > 1 && !class_exists(FreshnessCheck::class)) { $findings[] = $this->finding( 'ai_content_freshness_signals_missing', I18n::tr('Pages do not show a publish or update date', 'الصفحات لا تذكر تاريخ النشر أو التحديث'), 'AI-Friendly', 'low', -2, [ 'source' => 'html', 'details' => sprintf(I18n::tr('%.0f%% of content pages show no publish or update date (article:published_time or dateModified).', '%.0f%% من صفحات المحتوى بلا تاريخ نشر أو تحديث (article:published_time أو dateModified).'), $fresh_missing_ratio * 100), 'samples' => array_slice($no_freshness_pages, 0, 5), ], I18n::tr('Visible publish and update dates tell readers and search engines how current the content is.', 'تاريخ النشر والتحديث الظاهر يوضح للقارئ ولمحركات البحث مدى حداثة المحتوى.'), [ I18n::tr('Make sure your SEO plugin adds article:published_time and article:modified_time in meta.', 'تأكد أن إضافة تحسين محركات البحث تضيف تاريخ النشر وتاريخ التحديث في بيانات الصفحة.'), I18n::tr('In the Article schema: add datePublished and dateModified.', 'أضف تاريخ النشر (datePublished) وتاريخ التحديث (dateModified) في بيانات المقال.'), I18n::tr('Update dateModified whenever you actually update the content.', 'حدّث تاريخ التحديث كلما حدّثت المحتوى فعلًا.'), ], [ 'how' => I18n::tr('Run the audit after adding the date data.', 'شغّل فحصًا جديدًا بعد إضافة التواريخ.'), 'pass_condition' => I18n::tr('Content pages show a publish or update date.', 'تعرض صفحات المحتوى تاريخ النشر أو التحديث.'), ], $this->id() ); } // ─── 14. No outbound authority links ───────────────────────────────────── // v3.3.52 P1.1 — divide by long content pages (500+ words). $outbound_missing_ratio = $content_pages_500 > 0 ? count($no_outbound_pages) / $content_pages_500 : 0; if ($outbound_missing_ratio > 0.7 && $content_pages_500 > 1) { $findings[] = $this->finding( 'ai_no_outbound_authority_links', I18n::tr('Long content lacks trusted outbound links', 'المحتوى الطويل يفتقر لروابط خارجية موثوقة'), 'AI-Friendly', 'info', -1, [ 'source' => 'html', 'details' => sprintf(I18n::tr('%.0f%% of long content pages (500+ words) link to no outside source.', '%.0f%% من صفحات المحتوى الطويلة (500 كلمة فأكثر) لا ترتبط بأي مصدر خارجي.'), $outbound_missing_ratio * 100), 'samples' => array_slice($no_outbound_pages, 0, 5), ], I18n::tr('Linking to trusted sources lets readers check the claims on the page.', 'الإشارة إلى مصادر موثوقة تتيح للقارئ التحقق من المعلومات الواردة في الصفحة.'), [ I18n::tr('In long articles, add links to trusted sources (studies, academic sources, official sites).', 'أضف في المقالات الطويلة: روابط لمصادر موثوقة (دراسات، أكاديميات، مواقع رسمية).'), I18n::tr('Use outbound linking purposefully — quality matters more than quantity.', 'استخدم الربط الخارجي بشكل هادف — الجودة أهم من الكمية.'), ], [ 'how' => I18n::tr('Run the audit after adding outbound links.', 'شغّل فحصًا جديدًا بعد إضافة الروابط الخارجية.'), 'pass_condition' => I18n::tr('Outbound links present in long articles.', 'وجود روابط خارجية في المقالات الطويلة.'), ], $this->id() ); } // ─── 15. Low structure density (lists/tables) ───────────────────────────── if ($word_total > 0 && $page_count >= 3) { $density = $structure_total / max(1, $page_count); if ($density < 2) { $findings[] = $this->finding( 'ai_low_structure_density', I18n::tr('Low use of lists and tables in content', 'انخفاض استخدام القوائم والجداول في المحتوى'), 'AI-Friendly', 'info', -2, [ 'source' => 'html', 'details' => sprintf(I18n::tr('Average lists or tables per page: %1$s (pages checked: %2$d).', 'متوسط القوائم والجداول في الصفحة: %1$s (عدد الصفحات المفحوصة: %2$d).'), (string) round($density, 2), (int) $page_count), ], I18n::tr('Lists and tables make information easier for AI models to extract in an orderly way (AEO - Answer Engine Optimization).', 'القوائم والجداول تسهّل استخراج المعلومات بشكل مرتب لنماذج AI (AEO - Answer Engine Optimization).'), [ I18n::tr('Use bullet lists for benefits/steps/comparisons.', 'استخدم قوائم نقاط للمزايا/الخطوات/المقارنات.'), I18n::tr('Tables are ideal for comparing products or features.', 'الجداول مثالية لمقارنة المنتجات أو الخصائص.'), I18n::tr('Every content page should contain at least 2-3 lists.', 'كل صفحة محتوى ينبغي أن تحتوي 2-3 قوائم على الأقل.'), ], [ 'how' => I18n::tr('Run the audit after improving the content structure.', 'شغّل فحصًا جديدًا بعد تحسين بنية المحتوى.'), 'pass_condition' => I18n::tr('On average 2 or more lists or tables per checked page.', 'في المتوسط قائمتان أو جدولان أو أكثر في كل صفحة مفحوصة.'), ], $this->id() ); } } // ─── 16. Definition blocks missing (AEO) ───────────────────────────────── // v1.0.58: removed — 99% of WordPress sites don't use
blocks and // raising an info finding on every site was pure noise. The signal // value of
tags is minor compared to FAQPage schema (which we // already check). Keep the data collection for future use but stop // emitting the finding. // ─── 17. Breadcrumb schema/nav missing ──────────────────────────────────── if (!$has_breadcrumb_schema && !$has_breadcrumb_nav && $page_count >= 2) { $findings[] = $this->finding( 'ai_breadcrumb_missing', I18n::tr('Pages have no breadcrumb trail', 'لا يوجد مسار تنقل (Breadcrumbs) في الصفحات'), 'AI-Friendly', 'med', -3, [ 'source' => 'html', 'details' => I18n::tr('No BreadcrumbList JSON-LD and no