feat: scripts de projectos vindos do Hub (podcast, alojadamaria, clip, ocr, etc.)

Movidos do vault Hub para centralizar scripts. Hub mantem symlinks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 20:53:29 +01:00
parent e810bbb114
commit 6035542b67
27 changed files with 4246 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
#!/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))