كود لاكتشاف الصفحات “اليتيمة” (Orphan Pages) التي لا يراها جوجل

دقائق القراءة: 6

كود لاكتشاف الصفحات “اليتيمة” (Orphan Pages) التي لا يراها جوجل

الصفحات اليتيمة هي الروابط الموجودة على موقعك ولكنها لا تستقبل روابط داخلية كافية، أو لا تظهر ضمن مسار الزحف المنطقي الذي يعتمد عليه محرك البحث. عملياً، قد تكون الصفحة منشورة ومفيدة، لكنها معزولة عن بنية الموقع، فيصعب على Googlebot اكتشافها أو منحها قيمة زحف مناسبة.

في بيئات النشر الكبيرة، لا يكفي الاعتماد على Sitemap وحدها، لأن وجود الرابط داخلها لا يعني أنه مدمج فعلاً داخل الشبكة الداخلية للموقع. هنا تأتي فائدة الأتمتة: مقارنة بيانات الزحف الداخلي مع خرائط الموقع وبيانات الأداء لاستخراج الصفحات التي يُحتمل أن تكون يتيمة بدقة عالية.

هذا النوع من الفحوصات امتداد طبيعي لما ناقشناه في مدخل إلى عالم أتمتة الـ SEO: لماذا الآن؟، كما أنه يستفيد مباشرة من مهارات الإعداد الواردة في تهيئة بيئة العمل: تثبيت Python والمكتبات الأساسية، ومن قوة التحليل التي شرحناها في استخدام مكتبة Pandas لتحليل بيانات الـ SEO الضخمة.

ما المقصود بالصفحة اليتيمة تقنياً؟

الصفحة اليتيمة ليست مجرد صفحة ضعيفة الأداء، بل صفحة لا يمكن الوصول إليها بسهولة عبر الروابط الداخلية أثناء الزحف. قد تكون موجودة في XML Sitemap أو حتى تم اكتشافها سابقاً من خلال روابط خارجية، لكن عدم وجود روابط داخلية فعالة إليها يجعلها خارج الهيكل الفعلي للموقع.

تظهر المشكلة غالباً بعد نقل محتوى قديم، تحديثات تصميم، تغيير تصنيفات، أو نشر صفحات هبوط جديدة دون ربطها من المقالات أو الأقسام أو صفحات الفئات. النتيجة: المحتوى موجود، لكن جوجل يتعامل معه ككيان هامشي.

لماذا تؤثر الصفحات اليتيمة على السيو وأدسنس؟

من منظور السيو، الصفحات اليتيمة تستهلك المحتوى دون أن تستفيد من Internal Link Equity. ومن منظور الجودة، كثرة الصفحات المعزولة قد تعطي انطباعاً بموقع غير منظم أو ضعيف الصيانة، خصوصاً إذا كانت هذه الصفحات قديمة أو رقيقة المحتوى.

بالنسبة لـ AdSense، فالمواقع النظيفة ذات الهيكل الواضح ومسارات التصفح السليمة تكون أكثر اتساقاً مع متطلبات التجربة الجيدة. لذلك، معالجة الصفحات اليتيمة ليست رفاهية تقنية، بل خطوة تدعم الثقة، الزحف، والأرشفة.

منهجية الاكتشاف الدقيقة

أفضل طريقة عملية هي بناء قائمة موحدة من جميع الروابط المعروفة، ثم استبعاد الصفحات التي تم العثور عليها أثناء الزحف الداخلي. أي رابط يوجد في Sitemap أو ملف تصدير أو قاعدة بيانات المحتوى، لكنه لا يظهر في الروابط المكتشفة أثناء الزحف، يُصنّف كمرشح قوي ليكون صفحة يتيمة.

مصادر البيانات التي سنقارن بينها

  • روابط الزحف الداخلي من الموقع نفسه.
  • روابط XML Sitemap.
  • قائمة إضافية اختيارية من CMS export أو ملف يدوي.

متى تكون النتيجة أكثر موثوقية؟

  1. عندما يكون الزحف قد بدأ من الصفحة الرئيسية والأقسام الأساسية.
  2. عندما تقوم بتنظيف الروابط من المعاملات مثل utm_source.
  3. عندما توحّد البروتوكول وشرطة النهاية trailing slash.

فكرة السكربت قبل تنفيذ الكود

السكربت التالي يقوم بأربع مهام رئيسية: يجلب روابط خرائط الموقع، يزحف داخل الموقع لاستخراج الروابط الداخلية، يطبّع جميع الروابط، ثم يخرج ملفاً نهائياً بالصفحات التي ظهرت في الخريطة لكنها لم تظهر ضمن مسار الربط الداخلي.

وإذا كنت تعمل لاحقاً على تطويره ليتكامل مع واجهات برمجية، فراجع مفهوم الـ API: كيف نطلب البيانات من Google وOpenAI والحماية والأمان: كيف تخفي مفاتيحك السرية في الكود؟ قبل توصيل أي مفاتيح إنتاجية.

كود بايثون لاكتشاف الصفحات اليتيمة

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse, urlunparse
from collections import deque
import pandas as pd
import xml.etree.ElementTree as ET

START_URL = "https://example.com/"
SITEMAP_URL = "https://example.com/sitemap.xml"
MAX_PAGES = 300

headers = {
    "User-Agent": "Mozilla/5.0 (compatible; SEOOrphanPageBot/1.0)"
}

def normalize_url(url):
    parsed = urlparse(url)
    scheme = parsed.scheme.lower() or "https"
    netloc = parsed.netloc.lower()
    path = parsed.path.rstrip("/")
    if not path:
        path = ""
    clean = urlunparse((scheme, netloc, path, "", "", ""))
    return clean

def is_internal(url, domain):
    return urlparse(url).netloc.lower() == domain.lower()

def get_sitemap_urls(sitemap_url):
    urls = set()
    response = requests.get(sitemap_url, headers=headers, timeout=20)
    response.raise_for_status()
    root = ET.fromstring(response.content)

    namespace = {"ns": "http://www.sitemaps.org/schemas/sitemap/0.9"}

    for loc in root.findall(".//ns:loc", namespace):
        if loc.text:
            urls.add(normalize_url(loc.text.strip()))

    return urls

def crawl_site(start_url, max_pages=300):
    visited = set()
    discovered_links = set()
    queue = deque([start_url])

    domain = urlparse(start_url).netloc

    while queue and len(visited) < max_pages:
        current_url = normalize_url(queue.popleft())

        if current_url in visited:
            continue

        try:
            response = requests.get(current_url, headers=headers, timeout=15)
            if "text/html" not in response.headers.get("Content-Type", ""):
                visited.add(current_url)
                continue

            visited.add(current_url)
            soup = BeautifulSoup(response.text, "html.parser")

            for a_tag in soup.find_all("a", href=True):
                href = a_tag["href"].strip()
                absolute_url = normalize_url(urljoin(current_url, href))

                if is_internal(absolute_url, domain):
                    discovered_links.add(absolute_url)
                    if absolute_url not in visited:
                        queue.append(absolute_url)

        except requests.RequestException:
            visited.add(current_url)
            continue

    return visited, discovered_links

def find_orphan_pages(sitemap_urls, crawled_urls):
    return sorted(list(sitemap_urls - crawled_urls))

def main():
    sitemap_urls = get_sitemap_urls(SITEMAP_URL)
    visited_pages, discovered_links = crawl_site(START_URL, MAX_PAGES)

    crawled_set = visited_pages.union(discovered_links)
    orphan_pages = find_orphan_pages(sitemap_urls, crawled_set)

    df = pd.DataFrame({"orphan_url": orphan_pages})
    df.to_csv("orphan_pages_report.csv", index=False, encoding="utf-8-sig")

    print(f"Sitemap URLs: {len(sitemap_urls)}")
    print(f"Crawled/Discovered URLs: {len(crawled_set)}")
    print(f"Potential Orphan Pages: {len(orphan_pages)}")
    print("Report saved to orphan_pages_report.csv")

if __name__ == "__main__":
    main()

كيف يعمل الكود خطوة بخطوة؟

1) توحيد شكل الروابط

الدالة normalize_url() تعالج مشكلة شائعة جداً: الرابط الواحد قد يظهر بأكثر من شكل. بدون هذا التوحيد، ستظهر نتائج مضللة لأن السكربت قد يعتبر نفس الصفحة صفحتين مختلفتين.

2) جلب روابط الخريطة

الدالة get_sitemap_urls() تقرأ ملف XML وتستخرج كل قيمة داخل loc. إن كنت تحتاج أساساً أقوى لفهم بنية البيانات، فمقال أساسيات التعامل مع ملفات JSON (لغة التفاهم بين الأنظمة) مفيد أيضاً في بناء عقلية التعامل مع تنسيقات التبادل المختلفة.

3) الزحف الداخلي

الدالة crawl_site() تستخدم أسلوب Breadth-First Search بشكل مبسط. هذا الأسلوب ممتاز لاكتشاف شبكة الروابط الداخلية تدريجياً من الصفحات الأكثر قرباً للصفحة الرئيسية.

4) استخراج الصفحات اليتيمة

المعادلة بسيطة: أي رابط موجود في sitemap_urls وغير موجود داخل مجموعة الروابط التي تم الزحف إليها، يخرج في التقرير النهائي كصفحة يتيمة محتملة.

كيف تطوّر السكربت لمستوى احترافي؟

نصيحة عملية: لا تتعامل مع كل صفحة يتيمة باعتبارها خطأ يجب إصلاحه فوراً. بعض الصفحات يجب حذفها، وبعضها يجب إعادة ربطه، وبعضها يحتاج فقط إلى noindex إذا كانت منخفضة القيمة أو أنشئت لغرض تقني مؤقت.

ماذا تفعل بعد استخراج التقرير؟

  1. راجع الصفحات ذات القيمة التجارية أو المعلوماتية العالية أولاً.
  2. أضف روابط داخلية سياقية من صفحات ذات صلة قوية.
  3. حدّث القوائم، صفحات الفئات، أو مقالات الركائز لاحتواء هذه الروابط.
  4. إذا كانت الصفحة غير مهمة، فاحذفها أو أعد توجيهها أو استبعدها من الخريطة.

وفي المواقع الكبيرة، من المفيد ربط هذا الإخراج بلوحة متابعة مثل بناء "Dashboard" تفاعلي لبيانات الموقع باستخدام Google Looker Studio حتى تراقب عدد الصفحات اليتيمة بمرور الوقت بدل إجراء فحص يدوي متباعد.

الخلاصة

اكتشاف الصفحات اليتيمة ليس مجرد تمرين تقني، بل عملية استراتيجية تكشف أين يضيع المحتوى الجيد خارج هيكل الموقع. باستخدام Python وPandas وبيانات Sitemap، يمكنك بناء تدقيق دوري يرفع كفاءة الزحف، ويحسن فرص الأرشفة، ويدعم جودة الموقع من منظور المستخدم ومحرك البحث معاً.

كلما كان الموقع أكثر ترابطاً واتساقاً، كانت إشارات الجودة أوضح. وهذا بالضبط ما تحتاجه أي استراتيجية سيو حديثة تريد نتائج مستقرة وقابلة للتوسع.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *