"""Depth + duplicate title/meta analysis for odoo-expertos.com (read-only).""" import json, re, sys, urllib.parse as up import requests from bs4 import BeautifulSoup BASE = "https://www.odoo-expertos.com" S = requests.Session() S.headers["User-Agent"] = "Mozilla/5.0 (SEO-audit; +read-only)" def get(u): try: r = S.get(u, timeout=30) return r.status_code, r.text except Exception as e: return None, str(e) def meta(html): soup = BeautifulSoup(html, "lxml") t = (soup.title.string.strip() if soup.title and soup.title.string else "") md = "" m = soup.find("meta", attrs={"name": "description"}) if m and m.get("content"): md = m["content"].strip() h1s = [h.get_text(strip=True) for h in soup.find_all("h1")] return t, md, h1s, soup def internal_links(soup): out = set() for a in soup.find_all("a", href=True): href = a["href"].split("#")[0].strip() if not href or href.startswith(("mailto:", "tel:", "javascript:")): continue full = up.urljoin(BASE + "/", href) if full.startswith(BASE): out.add(full.rstrip("/") + "/") return out # 1. CLICK DEPTH (Spanish tree): home -> categories -> articles depth = {} _, home = get(BASE + "/") soup_home = BeautifulSoup(home, "lxml") if home else None d1 = internal_links(soup_home) if soup_home else set() cats = [u for u in d1 if re.search(r"/(odoo|odoo-hosting|odoo-ia)/$", u)] depth["home_internal_links"] = len(d1) depth["categories_linked_from_home"] = sorted(cats) d2 = set() cat_link_counts = {} for c in ["/odoo/", "/odoo-hosting/", "/odoo-ia/"]: code, html = get(BASE + c) if html and code == 200: s = BeautifulSoup(html, "lxml") links = internal_links(s) arts = {u for u in links if re.search(r"/(odoo|odoo-hosting|odoo-ia)/[^/]+/$", u)} cat_link_counts[c] = len(arts) d2 |= arts depth["articles_linked_from_category_indexes(depth2)"] = len(d2) depth["per_category_article_links"] = cat_link_counts # Compare to total ES articles in sitemap try: es = open(r"C:\Users\eugen\.cursor\projects\Odoo Expertos\odoo-expertos\seo-audit-2026-05-27\raw\sitemap-es.xml", encoding="utf-8").read() es_locs = set(re.findall(r"(https[^<]+)", es)) es_articles = {u.rstrip("/") + "/" for u in es_locs if re.search(r"/(odoo|odoo-hosting|odoo-ia)/[^/]+/$", u)} depth["total_ES_articles_in_sitemap"] = len(es_articles) reachable = (d2 & es_articles) depth["ES_articles_reachable_within_2_clicks"] = len(reachable) depth["ES_articles_NOT_reachable_within_2_clicks"] = len(es_articles - reachable) except Exception as e: depth["sitemap_compare_error"] = str(e) # 2. DUPLICATE / IDENTICAL title + meta across a sample sample = [ "/", "/odoo/", "/odoo-hosting/", "/odoo-ia/", "/odoo/odoo-hosting-cloud-infraestructura-2025/", "/odoo/odoo-crm-gestion-clientes-2025/", "/odoo/odoo-inventario-gestion-almacen-2025/", "/odoo-hosting/aws-hosting-odoo-2026/", "/odoo-hosting/odoo-docker-hosting-2026/", "/odoo-hosting/servidor-dedicado-odoo-2026/", "/odoo-ia/que-es-odoo-ia-guia-basica-2025/", "/odoo-ia/odoo-openai-api-integracion-2025/", "/en/", "/de/", "/fr/", "/pt/", "/ar/", ] rows = [] for path in sample: code, html = get(BASE + path) if html and code == 200: t, md, h1s, _ = meta(html) rows.append({ "url": path, "status": code, "title": t, "title_len": len(t), "meta": md, "meta_len": len(md), "h1": h1s[0] if h1s else "", "h1_count": len(h1s), "title_eq_h1": (t.split("|")[0].strip().lower() == (h1s[0].lower() if h1s else "")), "title_eq_meta": (t.strip().lower() == md.strip().lower()), }) else: rows.append({"url": path, "status": code}) # duplicate detection within sample titles = {} metas = {} for r in rows: if r.get("title"): titles.setdefault(r["title"], []).append(r["url"]) if r.get("meta"): metas.setdefault(r["meta"], []).append(r["url"]) dup_titles = {k: v for k, v in titles.items() if len(v) > 1} dup_metas = {k: v for k, v in metas.items() if len(v) > 1} result = { "depth": depth, "pages": rows, "duplicate_titles_in_sample": dup_titles, "duplicate_metas_in_sample": dup_metas, } print(json.dumps(result, ensure_ascii=False, indent=2))