#!/usr/bin/env python3 """ Script de captura e análise visual SEO para descomplicar.pt Analisa: capturas desktop/mobile, above-the-fold, imagens, CTAs """ import json import re from playwright.sync_api import sync_playwright URL = "https://descomplicar.pt" SCREENSHOTS_DIR = "/media/ealmeida/Dados/Hub/03-Propostas/ALojaDaMaria/screenshots" VIEWPORTS = { "desktop": {"width": 1920, "height": 1080}, "laptop": {"width": 1366, "height": 768}, "tablet": {"width": 768, "height": 1024}, "mobile": {"width": 375, "height": 812}, } def capture(url, output_path, viewport_width=1920, viewport_height=1080): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page(viewport={"width": viewport_width, "height": viewport_height}) page.goto(url, wait_until="networkidle", timeout=30000) page.screenshot(path=output_path, full_page=False) browser.close() def analyse_page(url): results = {} with sync_playwright() as p: browser = p.chromium.launch() # --- Desktop 1920x1080 --- page = browser.new_page(viewport=VIEWPORTS["desktop"]) page.goto(url, wait_until="networkidle", timeout=30000) page.screenshot( path=f"{SCREENSHOTS_DIR}/desktop_1920.png", full_page=False ) page.screenshot( path=f"{SCREENSHOTS_DIR}/desktop_1920_full.png", full_page=True ) # Dados above-the-fold (desktop) atf = page.evaluate("""() => { const vw = window.innerWidth; const vh = window.innerHeight; // H1 const h1s = Array.from(document.querySelectorAll('h1')); const h1Visible = h1s.filter(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && r.width > 0; }); // CTAs (botões e links com texto de acção) const ctaKeywords = /contacto|falar|orçamento|começar|saber mais|ver mais|agendar|demo|serviços|get started|contact/i; const allBtns = Array.from(document.querySelectorAll('a, button')); const ctasAtf = allBtns.filter(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && r.width > 0 && ctaKeywords.test(el.textContent); }).map(el => ({text: el.textContent.trim().substring(0,60), tag: el.tagName, top: Math.round(el.getBoundingClientRect().top)})); // Value proposition (primeiro parágrafo/subtítulo visível) const textEls = Array.from(document.querySelectorAll('h2, h3, p, .subtitle, .hero-text, [class*="hero"] p, [class*="tagline"]')); const vpEl = textEls.find(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && el.textContent.trim().length > 30; }); // Sinais de confiança (logos, testimonials, reviews) const trustSelectors = '[class*="client"], [class*="partner"], [class*="logo"], [class*="review"], [class*="testim"], [class*="trust"], .stars, [class*="rating"]'; const trustEls = Array.from(document.querySelectorAll(trustSelectors)); const trustAtf = trustEls.filter(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && r.width > 0; }).length; return { viewport: {width: vw, height: vh}, h1Count: h1s.length, h1Texts: h1s.map(el => ({text: el.textContent.trim().substring(0,100), visible: h1Visible.includes(el)})), h1AboveFold: h1Visible.length, ctasAboveFold: ctasAtf, valueProposition: vpEl ? vpEl.textContent.trim().substring(0,200) : null, trustSignalsAboveFold: trustAtf, }; }""") # Análise de imagens images = page.evaluate("""() => { return Array.from(document.querySelectorAll('img')).map(img => ({ src: img.src.substring(0, 120), alt: img.alt, hasAlt: img.alt.trim().length > 0, loading: img.loading, width: img.width, height: img.height, hasWidthAttr: img.hasAttribute('width'), hasHeightAttr: img.hasAttribute('height'), isWebP: img.src.includes('.webp'), isAvif: img.src.includes('.avif'), naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight, rect: (() => { const r = img.getBoundingClientRect(); return {top: Math.round(r.top), visible: r.width > 0}; })() })); }""") # Dados de meta SEO meta_seo = page.evaluate("""() => { const getMeta = (name) => { const el = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`); return el ? el.getAttribute('content') : null; }; return { title: document.title, metaDescription: getMeta('description'), ogTitle: getMeta('og:title'), ogDescription: getMeta('og:description'), ogImage: getMeta('og:image'), canonical: (() => { const l = document.querySelector('link[rel="canonical"]'); return l ? l.href : null; })(), lang: document.documentElement.lang, h2Count: document.querySelectorAll('h2').length, h3Count: document.querySelectorAll('h3').length, }; }""") # Desempenho básico (recursos) perf = page.evaluate("""() => { const entries = performance.getEntriesByType('resource'); const imgs = entries.filter(e => e.initiatorType === 'img'); const scripts = entries.filter(e => e.initiatorType === 'script'); const styles = entries.filter(e => e.initiatorType === 'link' || e.initiatorType === 'css'); return { totalResources: entries.length, imgCount: imgs.length, scriptCount: scripts.length, styleCount: styles.length, }; }""") results["desktop_atf"] = atf results["images"] = images results["meta_seo"] = meta_seo results["perf"] = perf # --- Mobile 375x812 --- mobile_page = browser.new_page( viewport=VIEWPORTS["mobile"], user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1" ) mobile_page.goto(url, wait_until="networkidle", timeout=30000) mobile_page.screenshot( path=f"{SCREENSHOTS_DIR}/mobile_375.png", full_page=False ) mobile_page.screenshot( path=f"{SCREENSHOTS_DIR}/mobile_375_full.png", full_page=True ) mobile_checks = mobile_page.evaluate("""() => { const vw = window.innerWidth; const vh = window.innerHeight; const docWidth = document.documentElement.scrollWidth; // Verificar overflow horizontal const hasHorizontalScroll = docWidth > vw; // Navegação móvel const nav = document.querySelector('nav, [class*="nav"], [class*="menu"], header'); const navVisible = nav ? nav.getBoundingClientRect().width > 0 : false; const hamburger = document.querySelector('[class*="hamburger"], [class*="toggle"], [class*="burger"], .menu-icon, [aria-label*="menu"], [aria-label*="Menu"]'); // Tamanho dos tap targets (mínimo 48x48px) const allTapTargets = Array.from(document.querySelectorAll('a, button, input, select, textarea')); const smallTargets = allTapTargets.filter(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && (r.width < 44 || r.height < 44); }).slice(0, 10).map(el => ({ tag: el.tagName, text: el.textContent.trim().substring(0, 40), w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height) })); // Tamanho de fonte base const bodyFontSize = parseFloat(window.getComputedStyle(document.body).fontSize); // H1 visível no mobile const h1s = Array.from(document.querySelectorAll('h1')); const h1MobileVisible = h1s.filter(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && r.width > 0; }); // CTAs mobile const ctaKeywords = /contacto|falar|orçamento|começar|saber mais|ver mais|agendar|demo|serviços/i; const ctasMobile = Array.from(document.querySelectorAll('a, button')).filter(el => { const r = el.getBoundingClientRect(); return r.top >= 0 && r.bottom <= vh && r.width > 0 && ctaKeywords.test(el.textContent); }).map(el => ({text: el.textContent.trim().substring(0,50), w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height)})); return { viewport: {width: vw, height: vh}, documentWidth: docWidth, hasHorizontalScroll, navVisible, hasHamburger: !!hamburger, hamburgerClass: hamburger ? hamburger.className.substring(0,60) : null, smallTapTargets: smallTargets, smallTapTargetCount: smallTargets.length, bodyFontSize, h1AboveFoldMobile: h1MobileVisible.length, h1TextMobile: h1MobileVisible[0] ? h1MobileVisible[0].textContent.trim().substring(0,100) : null, ctasMobileAtf: ctasMobile, }; }""") results["mobile"] = mobile_checks # --- Laptop 1366x768 --- laptop_page = browser.new_page(viewport=VIEWPORTS["laptop"]) laptop_page.goto(url, wait_until="networkidle", timeout=30000) laptop_page.screenshot( path=f"{SCREENSHOTS_DIR}/laptop_1366.png", full_page=False ) browser.close() return results if __name__ == "__main__": print("A capturar screenshots e analisar descomplicar.pt...") data = analyse_page(URL) output_file = f"{SCREENSHOTS_DIR}/analysis_data.json" with open(output_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"Análise concluída. Dados guardados em: {output_file}") print(f"Screenshots em: {SCREENSHOTS_DIR}/") print("\n--- RESUMO ---") print(json.dumps(data, ensure_ascii=False, indent=2))