parsed_html_pages((array) $ctx->get('sample_pages', [])); $no_viewport = []; $zoom_disabled = []; $heavy_assets_pages = []; $no_lang_pages = []; // v3.3.46 — same root-cause guard as TechnicalSeoCheck: the lang // ratio is taken against pages that actually parsed, not every row, // so a failed crawl can't fabricate a "lang missing" finding. $lang_pages_parsed = 0; foreach ($pages as $p) { if (!is_array($p)) { continue; } $url = (string) ($p['url'] ?? ''); $viewport = (string) ($p['viewport'] ?? ''); // Check viewport meta tag if ($viewport === '' || stripos($viewport, 'width=device-width') === false) { $no_viewport[] = ['url' => $url, 'viewport' => $viewport]; } else { // Detect zoom-disabling viewport (anti-pattern hurting accessibility) $vp_lower = strtolower($viewport); $disables_zoom = false; if (strpos($vp_lower, 'user-scalable=no') !== false || strpos($vp_lower, 'user-scalable=0') !== false) { $disables_zoom = true; } if (preg_match('~maximum-scale\s*=\s*(\d+(?:\.\d+)?)~', $vp_lower, $m)) { if ((float) $m[1] <= 1.0) { $disables_zoom = true; } } if ($disables_zoom) { $zoom_disabled[] = ['url' => $url, 'viewport' => $viewport]; } } // Heavy asset count (mobile parse cost) — heuristic: too many CSS+JS // v3.3.48 — Round-3 review flagged that the metric mixed // external files with inline style/script blocks. Split // them so the customer can tell whether the issue is external // requests (defer/combine concern) vs inline weight (RUCSS / // critical-CSS concern). $assets = isset($p['asset_counts']) && is_array($p['asset_counts']) ? $p['asset_counts'] : []; $css = (int) ($assets['css_count'] ?? 0); $js = (int) ($assets['js_count'] ?? 0); $external_css = (int) ($assets['external_css_count'] ?? 0); $inline_css = (int) ($assets['inline_css_count'] ?? 0); if (($css + $js) >= 50) { $heavy_assets_pages[] = [ 'url' => $url, 'external_css' => $external_css ?: $css, 'inline_css_blocks'=> $inline_css, 'js_files' => $js, 'total' => $css + $js, ]; } // lang attribute (screen reader / accessibility on mobile) // v3.3.46 — only count pages whose HTML genuinely parsed. $page_parsed = (!isset($p['_html_parsed']) || !empty($p['_html_parsed'])) && (int) ($p['status_code'] ?? 200) === 200; if ($page_parsed) { $lang_pages_parsed++; $lang = (string) ($p['lang'] ?? ''); if ($lang === '') { $no_lang_pages[] = ['url' => $url]; } } } if (!empty($no_viewport)) { $findings[] = $this->finding( 'mobile_viewport_missing', I18n::tr('Viewport meta tag missing or incorrect', 'وسم ملاءمة الشاشة (viewport) غير موجود أو غير صحيح'), 'Mobile Usability', 'high', -12, [ 'source' => 'html', 'details' => I18n::tr('Missing or incorrect viewport meta tag on checked pages', 'وسم ملاءمة الشاشة (viewport) غير موجود أو غير صحيح في الصفحات المفحوصة'), 'samples' => array_slice($no_viewport, 0, 10), ], I18n::tr('The viewport meta tag is essential for mobile responsiveness. Without it the page renders as desktop on mobile.', 'هذا الوسم ضروري لتظهر الصفحة مناسبة على الجوال، ومن دونه تظهر كنسخة الحاسوب مصغرة.'), [ I18n::tr('Add in
', 'أضف داخل رأس الصفحة'), I18n::tr('Make sure the theme is responsive.', 'تأكد أن القالب متجاوب مع الجوال.'), ], [ 'how' => 'Check page source for viewport meta tag', 'pass_condition' => 'Viewport meta tag present with width=device-width', ], $this->id() ); } if (!empty($zoom_disabled)) { $findings[] = $this->finding( 'mobile_zoom_disabled', I18n::tr('Viewport prevents the user from zooming', 'الصفحة تمنع الزائر من التكبير'), 'Mobile Usability', 'med', -5, [ 'source' => 'html', 'details' => I18n::tr('The viewport tag blocks zooming (user-scalable=no or a maximum-scale of 1 or less).', 'وسم viewport يمنع التكبير (user-scalable=no أو قيمة maximum-scale تساوي 1 أو أقل).'), 'samples' => array_slice($zoom_disabled, 0, 10), ], I18n::tr('Blocking zoom hurts accessibility for users who need to zoom to read the content. Google lowers the ranking of pages that block zoom.', 'منع التكبير يضر إمكانية الوصول (Accessibility) للمستخدمين الذين يحتاجون التكبير لقراءة المحتوى. Google يخفض ترتيب الصفحات التي تمنع zoom.'), [ I18n::tr('Remove user-scalable=no and maximum-scale from the viewport.', 'احذف user-scalable=no وmaximum-scale من وسم viewport.'), I18n::tr('Recommended form: ', 'الصيغة الموصى بها: '), ], [ 'how' => I18n::tr('Run the audit after changing the viewport.', 'شغّل فحصًا جديدًا بعد تعديل الوسم.'), 'pass_condition' => I18n::tr('Every page allows zooming.', 'كل الصفحات تسمح بالتكبير.'), ], $this->id() ); } if (!empty($heavy_assets_pages)) { // v2.1.5 — mitigation gating same as JS / CSS+JS combo. The // mobile penalty for high asset count drops dramatically when // RUCSS + Delay JS + HTTP/3 are active. $mobile_mitigations = []; $mobile_mitigation_steps = 0; if (function_exists('get_option')) { $rocket = get_option('wp_rocket_settings'); if (is_array($rocket)) { if (!empty($rocket['remove_unused_css'])) { $mobile_mitigations[] = 'WP Rocket RUCSS active'; $mobile_mitigation_steps++; } if (!empty($rocket['delay_js'])) { $mobile_mitigations[] = 'WP Rocket Delay JS active'; $mobile_mitigation_steps++; } } } // Quick HTTP/3 check via wp.infrastructure $wp_ctx_local = (array) $ctx->get('wp', []); $infra = isset($wp_ctx_local['infrastructure']) && is_array($wp_ctx_local['infrastructure']) ? $wp_ctx_local['infrastructure'] : []; // v3.3.52 P1.3 — count this mitigation only for a true edge CDN or a // real HTTP/2-3 transport signal. A bare LiteSpeed/Nginx origin (old // behaviour via cdn_detected) is NOT a CDN and must not soften a real // mobile-weight finding. $edge_cdn_mobile = (string) ($infra['edge_cdn_detected'] ?? ''); $http_proto_mobile = strtolower((string) ($infra['http_protocol'] ?? '')); $has_modern_transport = $edge_cdn_mobile !== '' || stripos((string)($infra['server_software'] ?? ''), 'cloudflare') !== false || strpos($http_proto_mobile, 'http/2') !== false || strpos($http_proto_mobile, 'http/3') !== false; if ($has_modern_transport) { $mobile_mitigations[] = 'CDN/HTTP-2-or-3 active'; $mobile_mitigation_steps++; } $sev_ladder = ['med' => -4, 'low' => -2, 'info' => -1]; $sev_order = ['med', 'low', 'info']; $sev_idx = min(count($sev_order) - 1, $mobile_mitigation_steps); $mobile_sev = $sev_order[$sev_idx]; $mobile_impact = $sev_ladder[$mobile_sev]; $title_suffix3 = !empty($mobile_mitigations) ? ' (mitigated)' : ''; // v3.3.49 — round-4 review: don't call inline style blocks // "files". The metric mixes external requests (network cost) with // inline blocks (parse cost) — make the wording precise so the // customer knows whether to defer/combine or critical-CSS this. $findings[] = $this->finding( 'mobile_heavy_asset_count', I18n::tr('Pages with many external CSS/JS + inline style blocks (mobile impact)', 'صفحات فيها عدد كبير من ملفات التنسيق والسكربتات (يؤثر في الجوال)') . $title_suffix3, 'Mobile Usability', $mobile_sev, $mobile_impact, [ 'source' => 'html', 'details' => I18n::tr('Pages where external CSS files, inline style blocks and external scripts add up to 50 or more. External files are network requests; inline style blocks only cost parsing time. With unused-CSS removal and delayed scripts the real cost on phones is much lower than the raw count suggests.', 'صفحات يبلغ فيها مجموع ملفات التنسيق الخارجية وكتل التنسيق الداخلية وملفات السكربتات 50 أو أكثر. الملفات الخارجية طلبات إضافية، والكتل الداخلية تحتاج وقت معالجة فقط. ومع إزالة التنسيق غير المستخدم وتأخير السكربتات يكون الأثر الفعلي على الجوال أقل بكثير مما يوحي به العدد.'), 'samples' => array_slice($heavy_assets_pages, 0, 10), 'mitigation_notes' => $mobile_mitigations, 'metric_legend' => [ // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- Static description string in an audit finding's metric-legend; explains a pattern detected on the user's site, not an emitted stylesheet. 'external_css' => 'external stylesheet requests (network cost)', 'inline_css_blocks' => 'inline style blocks (parse cost, not files)', 'js_files' => 'external script file requests', ], ], I18n::tr('Many CSS/JS files affect mobile, but with mitigations like RUCSS + Delay JS the impact drops a lot in CrUX field data.', 'كثرة ملفات التنسيق والسكربتات تؤثر في الجوال، ويقل الأثر كثيرًا مع إزالة التنسيق غير المستخدم وتأخير السكربتات.') . (!empty($mobile_mitigations) ? ' ✓ Active: ' . implode(' / ', $mobile_mitigations) . '.' : ''), [ I18n::tr('Enable RUCSS if you have not — it inlines the used CSS and defers the rest.', 'فعّل إزالة التنسيق غير المستخدم إن لم تكن مفعلة، فهي تُبقي التنسيق المستخدم وتؤجل الباقي.'), I18n::tr('Enable Delay JS — it noticeably lowers INP.', 'فعّل تأخير تشغيل السكربتات، فهو يحسن استجابة الصفحة بوضوح.'), I18n::tr('Check PSI Mobile / CrUX before any global combine (Combine CSS with RUCSS causes problems).', 'تحقق من بيانات السرعة على الجوال قبل دمج الملفات، لأن الدمج مع إزالة التنسيق غير المستخدم يسبب مشكلات.'), ], [ 'how' => I18n::tr('Run a new audit and compare the mobile speed score.', 'شغّل فحصًا جديدًا وقارن نتيجة السرعة على الجوال.'), 'pass_condition' => 'PSI Mobile >= 90 OR css+js < 50.', ], $this->id() ); } // v3.3.46 — ratio + gate keyed on parsed pages; silent on degraded crawl. $lang_ratio = $lang_pages_parsed > 0 ? count($no_lang_pages) / $lang_pages_parsed : 0; if ($lang_ratio > 0.5 && $lang_pages_parsed > 1) { $findings[] = $this->finding( 'mobile_lang_attribute_missing', I18n::tr('lang attribute missing on (affects screen readers)', 'لغة الصفحة غير محددة في وسم html (يؤثر في قارئات الشاشة)'), 'Mobile Usability', 'med', -3, [ 'source' => 'html', 'details' => sprintf(I18n::tr('%.0f%% of checked pages set no language on the html tag.', '%.0f%% من الصفحات المفحوصة لا تحدد لغة الصفحة في وسم html.'), $lang_ratio * 100), 'samples' => array_slice($no_lang_pages, 0, 8), ], I18n::tr('The lang attribute is essential for screen readers to read content with correct pronunciation, and for search engines to detect the page language.', 'تحديد اللغة يجعل قارئات الشاشة تنطق المحتوى بشكل صحيح، ويساعد محركات البحث على معرفة لغة الصفحة.'), [ I18n::tr('Make sure the theme adds (or your site language).', 'تأكد أن القالب يضيف أو لغة موقعك.'), I18n::tr('In WordPress: use > in header.php.', 'في ووردبريس استخدم > في ملف header.php.'), ], [ 'how' => I18n::tr('Run the audit after editing the theme.', 'شغّل فحصًا جديدًا بعد تعديل القالب.'), 'pass_condition' => I18n::tr('Most pages set a language.', 'أغلب الصفحات تحدد لغتها.'), ], $this->id() ); } return $findings; } }