0) {
return $id;
}
// Fallback: try the rawurldecoded URL (Arabic slugs) and the URL
// without trailing slash.
$alt = rawurldecode($url);
if ($alt !== $url) {
$id = (int) url_to_postid($alt);
if ($id > 0) {
return $id;
}
}
$alt2 = rtrim($url, '/');
if ($alt2 !== $url) {
$id = (int) url_to_postid($alt2);
if ($id > 0) {
return $id;
}
}
return 0;
}
/**
* v2.1.2 — pages whose URL pattern indicates legitimately short content
* shouldn't be flagged as "thin". Contact pages, disclaimers, terms
* pages, login/cart pages, and small support/help pages are short by
* design.
*/
private function is_legitimately_short_path(string $url): bool
{
// v2.8.3 — moved to shared Utils\UrlClassifier so 4+ checks share
// one source of truth instead of each maintaining its own list.
return \AetosSeo\Utils\UrlClassifier::is_legitimately_short($url);
}
private function should_skip_non_html_asset_url(string $url): bool
{
$normalized = rawurldecode(trim(str_replace('\\', '/', $url)));
if ($normalized === '') {
return false;
}
$path = (string) wp_parse_url($normalized, PHP_URL_PATH);
$path = strtolower(trim($path));
$target = $path !== '' ? $path : strtolower($normalized);
if ($target === '') {
return false;
}
if (preg_match('~\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico|pdf|css|js|json|xml|kml|kmz|rss|atom|csv|txt|zip|rar|7z|mp4|mp3|wav|ogg|webm|avi|mov)$~i', $target) === 1) {
return true;
}
if (strpos($target, '/wp-json/') !== false || preg_match('~/feed/?$~i', $target) === 1) {
return true;
}
return strpos($target, '/wp-content/uploads/') !== false
&& preg_match('~\.[a-z0-9]{2,5}$~i', $target) === 1;
}
public function run(RunContext $ctx): array
{
$findings = [];
$pages = (array) $ctx->get('sample_pages', []);
$missing_title = [];
$short_title = [];
$long_title = [];
$title_map = [];
$missing_desc = [];
$short_desc = [];
$long_desc = [];
$desc_map = [];
$h1_missing = [];
$h1_multiple = [];
$no_h2_structure = [];
$thin = [];
$very_thin = [];
$heading_skip = [];
$low_text_ratio = [];
foreach ($pages as $p) {
if (!is_array($p)) {
continue;
}
$url = (string) ($p['url'] ?? '');
if ($url === '' || $this->should_skip_non_html_asset_url($url)) {
continue;
}
// v2.1.1 — non-200/noindex URLs should not be evaluated for missing
// H1/title/meta-desc/thin-content. They're not indexable, so
// missing element checks produce noise. The URL-graph checks
// (CrawlCheck) still see them.
// 4.0.17: unreachable (status 0) rows are skipped too; they carry
// empty defaults, not a missing title.
if (!$this->is_parsed_html_page($p)) {
continue;
}
if (!empty($p['robots_noindex'])) {
continue;
}
$title = (string) ($p['title'] ?? '');
$desc = (string) ($p['meta_desc'] ?? '');
$h1 = (int) ($p['h1_count'] ?? 0);
$h2 = (int) ($p['h2_count'] ?? 0);
$wc = (int) ($p['word_count'] ?? 0);
// ── Title ──
if ($title === '') {
$missing_title[] = ['url' => $url];
} else {
$len = function_exists('mb_strlen') ? mb_strlen($title) : strlen($title);
if ($len < 10) {
$short_title[] = ['url' => $url, 'len' => $len, 'title' => $title];
} elseif ($len > 60) {
$long_title[] = ['url' => $url, 'len' => $len, 'title' => $title];
}
$key = strtolower(trim($title));
if ($key !== '') {
$title_map[$key][] = $url;
}
}
// ── Meta description ──
if ($desc === '') {
$missing_desc[] = ['url' => $url];
} else {
$dlen = function_exists('mb_strlen') ? mb_strlen($desc) : strlen($desc);
if ($dlen < 50) {
$short_desc[] = ['url' => $url, 'len' => $dlen, 'desc' => $desc];
} elseif ($dlen > 165) {
$long_desc[] = ['url' => $url, 'len' => $dlen];
}
$dkey = strtolower(trim($desc));
if ($dkey !== '') {
$desc_map[$dkey][] = $url;
}
}
// ── H1 ──
if ($h1 === 0) {
$h1_missing[] = ['url' => $url];
} elseif ($h1 > 1) {
$h1_multiple[] = ['url' => $url, 'h1_count' => $h1];
}
// ── H2 structure for content pages ──
if ($h1 > 0 && $wc >= 400 && $h2 === 0) {
$no_h2_structure[] = ['url' => $url, 'word_count' => $wc];
}
// ── Heading hierarchy skip ──
$hier_valid = (bool) ($p['heading_hierarchy_valid'] ?? true);
if (!$hier_valid) {
$seq = (array) ($p['heading_sequence'] ?? []);
$skip_idx = (int) ($p['heading_skip_at'] ?? -1);
// v2.1.4 — pull the offending heading texts directly from
// the collector. Don't depend on the truncated sequence to
// identify levels — use stored texts even if skip is past
// position 30 (the sequence cap).
$skip_from_text = (string) ($p['heading_skip_from_text'] ?? '');
$skip_to_text = (string) ($p['heading_skip_to_text'] ?? '');
$sample = ['url' => $url, 'sequence' => array_slice($seq, 0, 30)];
// Show the exact break point. Use sequence levels when
// skip_idx falls within the cached slice; otherwise just
// show position + heading texts.
if ($skip_idx >= 1) {
if (isset($seq[$skip_idx], $seq[$skip_idx - 1])) {
$from_lvl = (int) $seq[$skip_idx - 1];
$to_lvl = (int) $seq[$skip_idx];
$sample['skip'] = sprintf(I18n::tr('H%1$d then H%2$d at heading %3$d', 'H%1$d ثم H%2$d عند العنوان رقم %3$d'), $from_lvl, $to_lvl, $skip_idx);
$sample['recommendation'] = sprintf(
I18n::tr('Change heading %1$d from H%2$d to H%3$d, or add an H%3$d before it.', 'غيّر العنوان رقم %1$d من H%2$d إلى H%3$d، أو أضف عنوان H%3$d قبله.'),
$skip_idx, $to_lvl, $from_lvl + 1
);
} else {
// Skip is past the sequence cap; still actionable
// via heading texts.
$sample['skip'] = sprintf(I18n::tr('Level jump at heading %d', 'قفزة في مستوى العناوين عند العنوان رقم %d'), $skip_idx);
$sample['recommendation'] = I18n::tr('Set this heading one level below the heading it belongs to.', 'اجعل مستوى هذا العنوان أقل بدرجة واحدة من العنوان الذي يتبعه.');
}
}
if ($skip_from_text !== '' || $skip_to_text !== '') {
$sample['offending_heading'] = [
'from_heading' => $skip_from_text,
'to_heading' => $skip_to_text,
];
}
// v2.1.1 — keep h2/h3 lists for context. Clipped to 5 each.
$h2_texts = isset($p['h2_texts']) && is_array($p['h2_texts']) ? $p['h2_texts'] : [];
$h3_texts = isset($p['h3_texts']) && is_array($p['h3_texts']) ? $p['h3_texts'] : [];
if (!empty($h2_texts) || !empty($h3_texts)) {
$sample['h2_texts'] = array_slice($h2_texts, 0, 5);
$sample['h3_texts'] = array_slice($h3_texts, 0, 5);
}
$post_id = $this->resolve_post_id_from_url($url);
if ($post_id > 0) {
$sample['post_id'] = $post_id;
$sample['edit_link'] = admin_url('post.php?post=' . $post_id . '&action=edit');
}
$heading_skip[] = $sample;
}
// ── Low text-to-HTML ratio ──
$ratio = (float) ($p['text_to_html_ratio'] ?? 0.0);
if ($wc > 50 && $ratio > 0.0 && $ratio < 0.05) {
$low_text_ratio[] = ['url' => $url, 'ratio' => round($ratio, 4), 'word_count' => $wc];
}
// ── Thin content ── v2.1.2 — skip pages where short content is
// legitimate by URL convention: contact/disclaimer/support/legal/
// login/cart/etc. These are short by design.
if ($wc > 0 && !$this->is_legitimately_short_path($url)) {
if ($wc < 150) {
$very_thin[] = ['url' => $url, 'word_count' => $wc];
} elseif ($wc < 300) {
$thin[] = ['url' => $url, 'word_count' => $wc];
}
}
}
// Duplicates
$title_dupes = [];
foreach ($title_map as $t => $urls) {
if (count($urls) >= 2) {
$title_dupes[] = ['title' => $t, 'urls' => $urls];
}
}
$desc_dupes = [];
foreach ($desc_map as $d => $urls) {
if (count($urls) >= 2) {
$desc_dupes[] = ['desc' => $d, 'urls' => $urls];
}
}
// ── Missing title ──
if (!empty($missing_title)) {
$findings[] = $this->finding(
'onpage_missing_title',
I18n::tr('Title missing on some checked pages', 'عنوان الصفحة (title) غير موجود في بعض الصفحات'),
'On-Page',
'high',
-7,
[
'source' => 'html',
'details' => I18n::tr('Missing
on checked pages', 'وسم عنوان الصفحة (title) غير موجود في الصفحات المفحوصة'),
'samples' => array_slice($missing_title, 0, 10),
],
I18n::tr('Title is a core element for CTR and ranking. Without it, Google and AI models will not know the page title.', 'عنوان الصفحة أساسي للظهور في نتائج البحث وجذب النقرات، ومن دونه لا يعرف جوجل ولا الذكاء الاصطناعي عنوان الصفحة.'),
[
I18n::tr('Add a unique, clear title to each page via your SEO plugin.', 'أضف عنوانًا واضحًا ومختلفًا لكل صفحة من إضافة تحسين محركات البحث.'),
I18n::tr('Make sure the theme prints correctly in .', 'تأكد أن القالب يضع وسم العنوان داخل رأس الصفحة بشكل صحيح.'),
],
[
'how' => I18n::tr('Run the audit again and confirm that no pages remain without a title.', 'شغّل الفحص ثم راجع هل بقيت صفحات بلا title.'),
'pass_condition' => I18n::tr('Every HTML page has a non-empty title.', 'كل صفحة لها عنوان غير فارغ.'),
],
$this->id()
);
}
// ── Short title ──
if (!empty($short_title)) {
$findings[] = $this->finding(
'onpage_short_title',
I18n::tr('Titles too short (under 10 characters)', 'عناوين صفحات قصيرة جدًا (أقل من 10 أحرف)'),
'On-Page',
'med',
-3,
[
'source' => 'html',
'details' => I18n::tr('Page titles shorter than 10 characters.', 'عناوين صفحات أقصر من 10 أحرف.'),
'samples' => Redactor::redact_array(array_slice($short_title, 0, 10)),
],
I18n::tr('A short title does not describe the page well enough for search engines and does not attract clicks.', 'العنوان القصير لا يصف الصفحة بما يكفي لمحركات البحث ولا يجذب النقر.'),
[
I18n::tr('Write a descriptive title of 30-60 characters containing the main keyword.', 'اكتب عنوانًا وصفيًا بين 30 و60 حرفًا يحتوي الكلمة الرئيسية للصفحة.'),
I18n::tr('Good example: "SEO services for e-commerce stores | Your store name"', 'مثال جيد: «خدمات تحسين الظهور للمتاجر الإلكترونية | اسم متجرك»'),
],
[
'how' => I18n::tr('Run the audit after updating the titles.', 'شغّل فحصًا جديدًا بعد تعديل العناوين.'),
'pass_condition' => I18n::tr('All titles are between 10 and 60 characters.', 'كل العناوين بين 10 و60 حرفًا.'),
],
$this->id()
);
}
// ── Long title ──
if (!empty($long_title)) {
$findings[] = $this->finding(
'onpage_long_title',
I18n::tr('Page titles longer than 60 characters', 'عناوين صفحات طويلة (أكثر من 60 حرفًا)'),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => I18n::tr('Page titles longer than 60 characters.', 'عناوين صفحات أطول من 60 حرفًا.'),
'samples' => Redactor::redact_array(array_slice($long_title, 0, 10)),
],
I18n::tr('Long titles get truncated in search results (SERP) and reduce message clarity.', 'العناوين الطويلة يقصّها جوجل في نتائج البحث فتضيع جزء من الرسالة.'),
[
I18n::tr('Shorten the title to 50-60 characters while keeping the main keyword at the start.', 'اختصر العنوان إلى 50 أو 60 حرفًا، وضع الكلمة الرئيسية في أوله.'),
I18n::tr('Put the site name at the end if needed: "Page title | Site name"', 'ضع اسم الموقع في النهاية إن لزم: "عنوان الصفحة | اسم الموقع"'),
],
[
'how' => I18n::tr('Run the audit after shortening the titles.', 'شغّل فحصًا جديدًا بعد تقصير العناوين.'),
'pass_condition' => I18n::tr('Most titles are within 60 characters.', 'أغلب العناوين في حدود 60 حرفًا.'),
],
$this->id()
);
}
// ── Duplicate titles ──
if (!empty($title_dupes)) {
$findings[] = $this->finding(
'onpage_duplicate_titles',
I18n::tr('Duplicate page titles', 'عناوين صفحات مكررة'),
'On-Page',
'med',
-4,
[
'source' => 'crawl',
'details' => I18n::tr('Duplicate titles within checked pages', 'عناوين مكررة بين صفحات العينة'),
'samples' => Redactor::redact_array(array_slice($title_dupes, 0, 5)),
],
I18n::tr('Duplicate titles weaken distinction between pages and can cause keyword cannibalization.', 'العنوان المكرر يجعل الصفحات متشابهة في نظر جوجل، وقد تتنافس على الكلمة نفسها.'),
[
I18n::tr('Make each page\'s title unique.', 'اجعل عنوان كل صفحة مختلفًا.'),
I18n::tr('Use a dynamic template: %post_title% - %site_name%', 'استخدم قالب ديناميكي: %post_title% - %site_name%'),
],
[
'how' => I18n::tr('Run the audit after updating the titles.', 'شغّل فحصًا جديدًا بعد تعديل العناوين.'),
'pass_condition' => I18n::tr('No duplicate titles on the affected pages.', 'لا توجد عناوين مكررة في الصفحات المتأثرة.'),
],
$this->id()
);
}
// ── Missing meta description ──
if (!empty($missing_desc)) {
$findings[] = $this->finding(
'onpage_missing_meta_desc',
I18n::tr('Some pages have no description', 'وصف الصفحة (meta description) غير موجود في بعض الصفحات'),
'On-Page',
'med',
-3,
[
'source' => 'html',
'details' => I18n::tr('Missing meta description on checked pages', 'وصف الصفحة (meta description) غير موجود في الصفحات المفحوصة'),
'samples' => array_slice($missing_desc, 0, 10),
],
I18n::tr('The meta description affects CTR in search results — missing it loses free clicks.', 'وصف الصفحة يظهر تحت العنوان في نتائج البحث ويؤثر في قرار النقر، وغيابه يضيع عليك نقرات مجانية.'),
[
I18n::tr('Add a unique meta description (120-155 characters) to each important page.', 'أضف وصفًا مختلفًا لكل صفحة مهمة بين 120 و155 حرفًا.'),
I18n::tr('It should describe the page and include a simple call-to-action.', 'يجب أن يصف الوصف الصفحة ويدعو الزائر إلى خطوة واضحة.'),
],
[
'how' => I18n::tr('Run the audit after adding the descriptions.', 'شغّل فحصًا جديدًا بعد إضافة الأوصاف.'),
'pass_condition' => I18n::tr('The main pages have a meta description.', 'الصفحات الأساسية تحتوي وصفًا.'),
],
$this->id()
);
}
// ── Short meta description ──
if (!empty($short_desc)) {
$findings[] = $this->finding(
'onpage_short_meta_desc',
I18n::tr('Meta descriptions too short (under 50 characters)', 'أوصاف صفحات قصيرة جدًا (أقل من 50 حرفًا)'),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => I18n::tr('Page descriptions shorter than 50 characters.', 'أوصاف صفحات أقصر من 50 حرفًا.'),
'samples' => Redactor::redact_array(array_slice($short_desc, 0, 8)),
],
I18n::tr('A short meta description does not give the user enough information in the SERP and may be replaced with page text.', 'الوصف القصير لا يعطي الباحث معلومة كافية، وقد يستبدله جوجل بجزء من نص الصفحة.'),
[
I18n::tr('Write a description of 120-155 characters that clearly explains the page\'s value.', 'اكتب وصفًا بين 120 و155 حرفًا يشرح فائدة الصفحة بوضوح.'),
],
[
'how' => I18n::tr('Run the audit after updating the descriptions.', 'شغّل فحصًا جديدًا بعد تعديل الأوصاف.'),
'pass_condition' => I18n::tr('Descriptions are between 50 and 165 characters.', 'الأوصاف بين 50 و165 حرفًا.'),
],
$this->id()
);
}
// ── Long meta description ──
if (!empty($long_desc)) {
$findings[] = $this->finding(
'onpage_long_meta_desc',
I18n::tr('Meta descriptions too long (over 165 characters)', 'أوصاف صفحات طويلة جدًا (أكثر من 165 حرفًا)'),
'On-Page',
'info',
-1,
[
'source' => 'html',
'details' => I18n::tr('Page descriptions longer than 165 characters, so Google cuts them in results.', 'أوصاف صفحات أطول من 165 حرفًا فيقصّها جوجل في نتائج البحث.'),
'samples' => array_slice($long_desc, 0, 8),
],
I18n::tr('A long meta description gets truncated in the SERP and will not show in full to users.', 'الوصف الطويل يقصّه جوجل في نتائج البحث فلا يظهر كاملًا.'),
[
I18n::tr('Trim the description to 120-155 characters, keeping the most important information at the start.', 'اختصر الوصف إلى 120 أو 155 حرفًا، وضع أهم المعلومات في أوله.'),
],
[
'how' => I18n::tr('Run the audit after trimming the descriptions.', 'شغّل فحصًا جديدًا بعد تقصير الأوصاف.'),
'pass_condition' => I18n::tr('Descriptions are within 165 characters.', 'الأوصاف في حدود 165 حرفًا.'),
],
$this->id()
);
}
// ── Duplicate meta descriptions ──
if (!empty($desc_dupes)) {
$findings[] = $this->finding(
'onpage_duplicate_meta_desc',
I18n::tr('Duplicate meta descriptions across checked pages', 'أوصاف صفحات مكررة'),
'On-Page',
'low',
-2,
[
'source' => 'crawl',
'details' => I18n::tr('Duplicate meta descriptions within checked pages', 'وصف الصفحة مكرر بين الصفحات المفحوصة'),
'samples' => Redactor::redact_array(array_slice($desc_dupes, 0, 5)),
],
I18n::tr('Duplicate meta descriptions make pages look similar in search results.', 'الوصف المكرر يجعل صفحاتك متشابهة في نتائج البحث.'),
[
I18n::tr('Write a unique description for each important page.', 'اكتب وصفًا مختلفًا لكل صفحة مهمة.'),
],
[
'how' => I18n::tr('Run the audit after updating the descriptions.', 'شغّل فحصًا جديدًا بعد تعديل الأوصاف.'),
'pass_condition' => I18n::tr('No duplicate descriptions on important pages.', 'لا توجد أوصاف مكررة في الصفحات المهمة.'),
],
$this->id()
);
}
// ── Missing H1 ──
if (!empty($h1_missing)) {
$findings[] = $this->finding(
'onpage_missing_h1',
I18n::tr('H1 missing on some checked pages', 'H1 مفقود في بعض صفحات العينة'),
'On-Page',
'med',
-4,
[
'source' => 'html',
'details' => I18n::tr('Checked pages with no main heading (H1).', 'صفحات بلا عنوان رئيسي (H1).'),
'samples' => array_slice($h1_missing, 0, 10),
],
I18n::tr('H1 defines the page\'s main topic and helps search engines and AI models understand the content.', 'H1 يحدد الموضوع الرئيسي للصفحة ويساعد محركات البحث ونماذج AI على فهم المحتوى.'),
[
I18n::tr('Add one clear H1 to each page including the main keyword.', 'أضف H1 واحداً واضحاً لكل صفحة يتضمن الكلمة المفتاحية الرئيسية.'),
I18n::tr('Make sure the theme does not use a div instead of H1 for styling reasons.', 'تأكد أن القالب لا يستخدم عنصرًا عاديًا بدل العنوان الرئيسي (H1) لأسباب شكلية.'),
],
[
'how' => I18n::tr('Run the audit after adding H1.', 'شغّل فحصًا جديدًا بعد إضافة العنوان الرئيسي.'),
'pass_condition' => I18n::tr('One H1 on every HTML page.', 'وجود H1 واحد في كل صفحة HTML.'),
],
$this->id()
);
}
// ── Multiple H1 ──
if (!empty($h1_multiple)) {
$findings[] = $this->finding(
'onpage_multiple_h1',
I18n::tr('Some pages have more than one main heading (H1)', 'بعض الصفحات فيها أكثر من عنوان رئيسي (H1)'),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => I18n::tr('Checked pages with more than one main heading (H1).', 'صفحات فيها أكثر من عنوان رئيسي (H1).'),
'samples' => array_slice($h1_multiple, 0, 10),
],
I18n::tr('Multiple H1s confuse the page\'s semantic structure.', 'العنوان الرئيسي يعرّف موضوع الصفحة، وتكراره يجعل الموضوع غير واضح لمحركات البحث.'),
[
I18n::tr('Keep only one H1, and use H2 for subheadings.', 'احتفظ بـH1 واحد فقط، واستخدم H2 للعناوين الفرعية.'),
I18n::tr('Check that the theme does not add an extra H1 in the site header.', 'تأكد أن القالب لا يضيف عنوانًا رئيسيًا (H1) آخر في رأس الموقع.'),
],
[
'how' => I18n::tr('Run the audit after the fix.', 'شغّل فحصًا جديدًا بعد التصحيح.'),
'pass_condition' => I18n::tr('Only one H1 on the affected pages.', 'H1 واحد فقط في الصفحات المتأثرة.'),
],
$this->id()
);
}
// ── Missing H2 structure ──
if (!empty($no_h2_structure)) {
$findings[] = $this->finding(
'onpage_missing_h2_structure',
I18n::tr('Content pages (400+ words) without H2', 'صفحات محتوى (400+ كلمة) بدون H2'),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => I18n::tr('Long pages (400+ words) with no H2 subheadings', 'صفحات فيها H1 وأكثر من 400 كلمة لكن h2_count=0'),
'samples' => array_slice($no_h2_structure, 0, 8),
],
I18n::tr('H2 splits content into clear sections that help readability and more accurate crawling by search engines.', 'H2 يقسم المحتوى لأقسام واضحة تساعد على قراءة أفضل وزحف أدق من محركات البحث.'),
[
I18n::tr('Split every 400+ word piece of content with descriptive H2 headings.', 'قسّم كل محتوى 400+ كلمة بعناوين H2 وصفية.'),
I18n::tr('Each H2 answers a specific aspect of the page topic.', 'كل H2 يجيب على جانب محدد من موضوع الصفحة.'),
],
[
'how' => I18n::tr('Run the audit after adding H2s.', 'شغّل فحصًا جديدًا بعد إضافة العناوين الفرعية.'),
'pass_condition' => I18n::tr('H2 present on long-content pages.', 'وجود H2 في الصفحات ذات المحتوى الطويل.'),
],
$this->id()
);
}
// ── Very thin content ──
if (!empty($very_thin)) {
$findings[] = $this->finding(
'onpage_very_thin_content',
I18n::tr('Very thin content (under 150 words) on checked pages', 'محتوى خفيف جداً (أقل من 150 كلمة) في صفحات العينة'),
'On-Page',
'med',
-4,
[
'source' => 'html',
'details' => I18n::tr('The main content of these pages has fewer than 150 words.', 'المحتوى الأساسي في هذه الصفحات أقل من 150 كلمة.'),
'samples' => array_slice($very_thin, 0, 10),
],
I18n::tr('Very thin content does not satisfy user intent and AI engines mostly ignore it.', 'المحتوى الخفيف جداً لا يجيب على نية المستخدم ومحركات AI تتجاهله في الغالب.'),
[
I18n::tr('Add useful content: a service/product description, benefits, FAQ, customer testimonials.', 'أضف محتوى مفيدًا: وصف الخدمة أو المنتج ومزاياه وأسئلة شائعة وآراء العملاء.'),
I18n::tr('These pages may need noindex if they have no specific purpose.', 'إذا لم يكن لهذه الصفحات هدف واضح فقد تحتاج إلى منع أرشفتها.'),
],
[
'how' => I18n::tr('Run the audit after adding content.', 'شغّل فحصًا جديدًا بعد إضافة المحتوى.'),
'pass_condition' => I18n::tr('Important pages have at least 150 words.', 'word_count >= 150 في الصفحات المهمة.'),
],
$this->id()
);
}
// ── Thin content ──
if (!empty($thin)) {
$findings[] = $this->finding(
'onpage_thin_content_heuristic',
I18n::tr('Thin content (150-300 words) on checked pages', 'محتوى خفيف (150-300 كلمة) في صفحات العينة'),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => I18n::tr('The main content of these pages has 150 to 300 words.', 'المحتوى الأساسي في هذه الصفحات بين 150 و300 كلمة.'),
'samples' => array_slice($thin, 0, 10),
],
I18n::tr('Very short pages often do not answer the visitor\'s question fully, which makes them harder to rank.', 'الصفحات القصيرة جدًا لا تجيب غالبًا عن سؤال الزائر كاملًا، فيصعب ظهورها في نتائج البحث.'),
[
I18n::tr('Add: FAQ, detailed steps, practical examples, or a comparison depending on the topic.', 'أضف أسئلة شائعة أو خطوات مفصلة أو أمثلة عملية أو مقارنة بحسب الموضوع.'),
I18n::tr('Target: 300+ words for ordinary pages, 600+ for important articles.', 'الهدف: 300+ كلمة للصفحات العادية، 600+ للمقالات المهمة.'),
],
[
'how' => I18n::tr('Run the audit after improving the content.', 'شغّل فحصًا جديدًا بعد تحسين المحتوى.'),
'pass_condition' => I18n::tr('Important pages have at least 300 words.', 'word_count >= 300 في الصفحات المهمة.'),
],
$this->id()
);
}
// ── Heading hierarchy skip (H1→H3 etc.) ──
if (!empty($heading_skip)) {
$sev = count($heading_skip) >= 3 ? 'med' : 'low';
$imp = count($heading_skip) >= 3 ? -4 : -2;
$findings[] = $this->finding(
'onpage_heading_hierarchy_skip',
I18n::tr('Headings skip a level (for example H1 straight to H3)', 'ترتيب العناوين غير صحيح (مثل الانتقال من H1 إلى H3 مباشرة)'),
'On-Page',
$sev,
$imp,
[
'source' => 'html',
'details' => I18n::tr('Heading hierarchy breaks (e.g. H1 → H3 with no H2)', 'انقطاع في تسلسل العناوين (مثل H1 → H3 دون H2)'),
'samples' => array_slice($heading_skip, 0, 10),
],
I18n::tr('The correct order (H1→H2→H3) helps search engines and AI understand the content structure. Skipping weakens Passage Indexing.', 'الترتيب الصحيح للعناوين (H1 ثم H2 ثم H3) يساعد محركات البحث والذكاء الاصطناعي على فهم بنية المحتوى واقتباس أجزائه.'),
[
I18n::tr('Make sure every H3 is preceded by an H2, and every H4 by an H3.', 'تأكد أن كل H3 مسبوق بـ H2، وكل H4 مسبوق بـ H3.'),
I18n::tr('Do not use headings for styling — use CSS instead.', 'لا تستخدم العناوين لتكبير الخط فقط، واستخدم التنسيق لذلك.'),
],
[
'how' => I18n::tr('Run the audit after fixing the order.', 'شغّل فحصًا جديدًا بعد تصحيح الترتيب.'),
'pass_condition' => I18n::tr('Every page has a valid heading order.', 'ترتيب العناوين صحيح في كل الصفحات.'),
],
$this->id()
);
}
// ── Low text-to-HTML ratio ──
if (!empty($low_text_ratio)) {
// v3.3.49 — round-4 review: surface total affected count
// separately from capped samples. Helps customers and AI
// consumers understand the full scope vs the displayed list.
$low_ratio_total = count($low_text_ratio);
$findings[] = $this->finding(
'onpage_low_text_html_ratio',
sprintf(I18n::tr('Little visible text compared with page code (%d pages)', 'نص ظاهر قليل مقارنة بكود الصفحة (%d)'), $low_ratio_total),
'On-Page',
'low',
-2,
[
'source' => 'html',
'details' => sprintf(I18n::tr('On %d pages with 50+ words the visible text is under 5%% of the page code.', 'عدد الصفحات (50 كلمة فأكثر) التي يقل فيها النص الظاهر عن 5%% من كود الصفحة: %d.'), $low_ratio_total),
'affected_count' => $low_ratio_total,
'pages_inspected' => count($pages),
'samples' => array_slice($low_text_ratio, 0, 10),
],
I18n::tr('A low text ratio means excess HTML against little content — a negative signal for search engines that can also slow the page.', 'ملاحظة للعلم: الكود كبير مقارنة بالنص الظاهر. جوجل لا يعتمد هذه النسبة في الترتيب، لكن الكود الزائد قد يبطئ الصفحة.'),
[
I18n::tr('Reduce excess HTML: remove unnecessary div wrappers and clean up inline styles.', 'خفّف الكود الزائد في الصفحة: احذف العناصر المغلِّفة غير الضرورية والتنسيقات المكتوبة داخل العناصر.'),
I18n::tr('Add useful text content to raise the ratio.', 'أضف محتوى نصي مفيد لرفع النسبة.'),
],
[
'how' => I18n::tr('Run the audit after improving.', 'شغّل فحصًا جديدًا بعد التحسين.'),
'pass_condition' => I18n::tr('Visible text is at least 5% of the page code on important pages.', 'text_to_html_ratio >= 0.05 في الصفحات المهمة.'),
],
$this->id()
);
}
return $findings;
}
}