Analizzatore SEO <head>
Incolla il tag <head> della tua pagina e ricevi un report SEO immediato.
🔒 Analisi locale nel browser · Nessun dato inviato
Incolla il codice del tuo <head>
Esempi:
⚠ Il codice incollato non sembra contenere tag HTML validi.
🔍

Incolla il codice del tuo <head>
e clicca Analizza

🖥️
L'anteprima apparirà dopo l'analisi
📱
L'anteprima apparirà dopo l'analisi
Lancia l'analisi per vedere i suggerimenti
', 'text/html' ); return doc.head; } function getMeta(head, name) { const el = head.querySelector(`meta[name="${name}"]`); return el ? el.getAttribute('content') || '' : null; } function getProp(head, prop) { const el = head.querySelector(`meta[property="${prop}"]`); return el ? el.getAttribute('content') || '' : null; } function getLink(head, rel) { const el = head.querySelector(`link[rel="${rel}"]`); return el ? (el.getAttribute('href') || '') : null; } function hasLink(head, rel) { return head.querySelector(`link[rel="${rel}"]`) !== null; } // ── CHECKS SEO ─────────────────────────────────────────────── function runChecks(head) { const checks = []; const raw = document.getElementById('head-input').value; // 1. TITLE — check doppio tag const titleEls = head.querySelectorAll('title'); if (titleEls.length > 1) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: `Title duplicato (${titleEls.length} tag trovati)`, detail: 'Sono presenti più tag . Google usa il primo, ma la duplicazione è un bug frequente nei CMS. Rimuovi i duplicati.' }); } const titleEl = titleEls[0]; const title = titleEl ? titleEl.textContent.trim() : null; if (!title) { checks.push({ group: 'Fondamentali', status: 'error', icon: '❌', title: 'Title mancante', detail: 'Il tag <title> è obbligatorio. È il testo che appare nella tab del browser e nella SERP.' }); } else if (title.length < 30) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: `Title troppo corto (${title.length} car.)`, detail: 'Ideale: 50–65 caratteri. Troppo corto spreca opportunità SEO.', value: title }); } else if (title.length > 65) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: `Title lungo (${title.length} car.)`, detail: 'Google misura in pixel, non in caratteri — soglia indicativa ~65 car. ASCII. Con emoji o caratteri larghi il limite può essere inferiore.', value: title }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: `Title nella norma (${title.length} car.)`, detail: 'Lunghezza ottimale (50–65 caratteri).', value: title }); } // 2. META DESCRIPTION const desc = getMeta(head, 'description'); if (!desc) { checks.push({ group: 'Fondamentali', status: 'error', icon: '❌', title: 'Meta description mancante', detail: 'Senza meta description Google genera automaticamente un estratto, spesso poco efficace.' }); } else if (desc.length < 70) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: `Meta description corta (${desc.length} car.)`, detail: 'Ideale: 150–160 caratteri. Includi keyword e un invito all\'azione.', value: desc }); } else if (desc.length > 160) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: `Meta description lunga (${desc.length} car.)`, detail: 'Google tronca oltre i 160 caratteri.', value: desc }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: `Meta description ottimale (${desc.length} car.)`, detail: 'Lunghezza nella norma (150–160 caratteri).', value: desc }); } // 3. CANONICAL const canonical = getLink(head, 'canonical'); if (!canonical) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: 'Canonical mancante', detail: 'Il tag canonical previene problemi di contenuto duplicato.' }); } else if (canonical.startsWith('./') || canonical.startsWith('../')) { checks.push({ group: 'Fondamentali', status: 'error', icon: '❌', title: 'Canonical usa path relativo', detail: 'Il canonical richiede un URL assoluto con https://. I path relativi con ./ o ../ non sono accettati.', value: canonical }); } else if (canonical.startsWith('/')) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: 'Canonical root-relative', detail: 'Google tollera i canonical root-relative (es. /pagina/) ma l\'URL assoluto con https:// è preferibile.', value: canonical }); } else if (!canonical.startsWith('https://')) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: 'Canonical non usa HTTPS', detail: 'Il canonical dovrebbe puntare alla versione HTTPS.', value: canonical }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: 'Canonical presente e HTTPS', detail: 'URL canonico correttamente dichiarato.', value: canonical }); } // 4. LANG — prima dal DOM parsato, poi regex sul raw come fallback const langFromDOM = (() => { try { const d = new DOMParser().parseFromString( '<!DOCTYPE html><html>' + cleanHeadInput(raw) + '<body></body></html>', 'text/html' ); return d.documentElement.getAttribute('lang'); } catch(e) { return null; } })(); const langVal = langFromDOM || (raw.match(/lang=["']([a-z]{2}(?:-[A-Z]{2})?)['"]/i) || [])[1] || null; if (!langVal) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: 'Attributo lang mancante', detail: 'Aggiungi lang="it" al tag <html> per dichiarare la lingua.' }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: `Lingua dichiarata: ${langVal}`, detail: 'L\'attributo lang è presente.' }); } // 5. ROBOTS const robots = getMeta(head, 'robots'); if (!robots) { checks.push({ group: 'Fondamentali', status: 'info', icon: 'ℹ️', title: 'Meta robots non dichiarato', detail: 'Google usa "index, follow" come default. Non è un errore, ma dichiararlo è buona pratica.' }); } else if (robots.includes('noindex')) { checks.push({ group: 'Fondamentali', status: 'error', icon: '🚫', title: 'Pagina esclusa (noindex)', detail: 'Google non indicizzerà questa pagina. Se intenzionale, ok.', value: robots }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: `Robots: ${robots}`, detail: 'La pagina è indicizzabile.' }); } // 6. VIEWPORT const viewport = getMeta(head, 'viewport'); if (!viewport) { checks.push({ group: 'Fondamentali', status: 'error', icon: '❌', title: 'Viewport mancante', detail: 'Senza viewport la pagina non è responsive. Google penalizza i siti non mobile-friendly.' }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: 'Viewport dichiarato', detail: 'Pagina configurata per dispositivi mobili.', value: viewport }); } // 7. FAVICON const favicon = head.querySelector('link[rel="icon"], link[rel="shortcut icon"]'); if (!favicon) { checks.push({ group: 'Fondamentali', status: 'warn', icon: '⚠️', title: 'Favicon mancante', detail: 'Migliora il riconoscimento del sito nelle tab e nei preferiti.' }); } else { checks.push({ group: 'Fondamentali', status: 'ok', icon: '✅', title: 'Favicon presente', detail: '' }); } // 8. OPEN GRAPH const ogTitle = getProp(head, 'og:title'); const ogDesc = getProp(head, 'og:description'); const ogImage = getProp(head, 'og:image'); const ogUrl = getProp(head, 'og:url'); const ogType = getProp(head, 'og:type'); if (!ogTitle && !ogDesc && !ogImage) { checks.push({ group: 'Open Graph', status: 'error', icon: '❌', title: 'Open Graph completamente mancante', detail: 'Senza OG tag le condivisioni social non mostreranno anteprime ottimizzate.' }); } else { if (!ogTitle) checks.push({ group: 'Open Graph', status: 'warn', icon: '⚠️', title: 'og:title mancante', detail: 'Necessario per controllare il titolo nelle condivisioni social.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: 'og:title presente', detail: '', value: ogTitle }); if (!ogDesc) checks.push({ group: 'Open Graph', status: 'warn', icon: '⚠️', title: 'og:description mancante', detail: 'Aggiungi og:description per la descrizione nelle condivisioni.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: 'og:description presente', detail: '', value: ogDesc }); if (!ogImage) checks.push({ group: 'Open Graph', status: 'warn', icon: '⚠️', title: 'og:image mancante', detail: 'Senza og:image le condivisioni non avranno anteprima visiva. Min 1200×630px.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: 'og:image presente', detail: 'Min 1200×630px.', value: ogImage }); if (!ogUrl) checks.push({ group: 'Open Graph', status: 'info', icon: 'ℹ️', title: 'og:url non dichiarato', detail: 'Consigliato per evitare ambiguità sull\'URL nelle condivisioni.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: 'og:url presente', detail: '', value: ogUrl }); if (!ogType) checks.push({ group: 'Open Graph', status: 'info', icon: 'ℹ️', title: 'og:type non dichiarato', detail: 'Usa og:type="website" per homepage, "article" per articoli.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: `og:type: ${ogType}`, detail: '' }); // og:locale e og:site_name (buona pratica) const ogLocale = getProp(head, 'og:locale'); const ogSiteName = getProp(head, 'og:site_name'); if (!ogLocale) checks.push({ group: 'Open Graph', status: 'info', icon: 'ℹ️', title: 'og:locale non dichiarato', detail: 'Consigliato per dichiarare la lingua del contenuto. Es: it_IT, en_US.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: `og:locale: ${ogLocale}`, detail: '' }); if (!ogSiteName) checks.push({ group: 'Open Graph', status: 'info', icon: 'ℹ️', title: 'og:site_name non dichiarato', detail: 'Permette a Facebook di mostrare il nome del sito nella card di condivisione.' }); else checks.push({ group: 'Open Graph', status: 'ok', icon: '✅', title: `og:site_name: ${ogSiteName}`, detail: '' }); // Check coerenza canonical vs og:url if (canonical && ogUrl && canonical !== ogUrl) { checks.push({ group: 'Open Graph', status: 'warn', icon: '⚠️', title: 'Canonical e og:url non coincidono', detail: 'Google potrebbe ricevere segnali contrastanti. I due URL dovrebbero essere identici.', value: `canonical: ${canonical} | og:url: ${ogUrl}` }); } } // 9. TWITTER const twCard = getMeta(head, 'twitter:card'); const twTitle = getMeta(head, 'twitter:title'); const twImage = getMeta(head, 'twitter:image'); if (!twCard) { checks.push({ group: 'Twitter/X', status: 'warn', icon: '⚠️', title: 'Twitter Card mancante', detail: 'Senza twitter:card le condivisioni su X non mostreranno anteprime. Usa "summary" o "summary_large_image".' }); } else { checks.push({ group: 'Twitter/X', status: 'ok', icon: '✅', title: `Twitter Card: ${twCard}`, detail: '' }); if (!twTitle) checks.push({ group: 'Twitter/X', status: 'info', icon: 'ℹ️', title: 'twitter:title non dichiarato', detail: 'Twitter usa og:title come fallback, ma dichiararlo esplicitamente è consigliato.' }); else checks.push({ group: 'Twitter/X', status: 'ok', icon: '✅', title: 'twitter:title presente', detail: '', value: twTitle }); // twitter:image obbligatoria per summary_large_image if (twCard === 'summary_large_image' && !twImage) { checks.push({ group: 'Twitter/X', status: 'error', icon: '❌', title: 'twitter:image mancante (obbligatoria)', detail: 'Con twitter:card="summary_large_image" l\'immagine è obbligatoria. Senza, X non mostrerà l\'anteprima.' }); } else if (!twImage) { checks.push({ group: 'Twitter/X', status: 'info', icon: 'ℹ️', title: 'twitter:image non dichiarata', detail: 'X usa og:image come fallback, ma dichiarare twitter:image esplicitamente garantisce il controllo dell\'anteprima.' }); } else { checks.push({ group: 'Twitter/X', status: 'ok', icon: '✅', title: 'twitter:image presente', detail: '', value: twImage }); } } // 10. SCHEMA.ORG const SCHEMA_WHITELIST = new Set([ 'WebPage','WebSite','Article','NewsArticle','BlogPosting','LearningResource', 'Course','FAQPage','HowTo','BreadcrumbList','Product','Offer','Review', 'Person','Organization','LocalBusiness','Event','VideoObject','ImageObject', 'SoftwareApplication','ItemList','CreativeWork','Book','Recipe' ]); const schemas = head.querySelectorAll('script[type="application/ld+json"]'); if (schemas.length === 0) { checks.push({ group: 'Schema.org', status: 'warn', icon: '⚠️', title: 'Dati strutturati mancanti', detail: 'Schema.org (JSON-LD) permette a Google di mostrare rich results. Aumenta il CTR.' }); } else { let validCount = 0; const types = []; const unknownTypes = []; schemas.forEach(s => { try { const parsed = JSON.parse(s.textContent); validCount++; const t = parsed['@type']; if (t) { if (SCHEMA_WHITELIST.has(t)) types.push(t); else unknownTypes.push(t); } } catch(e) {} }); if (validCount < schemas.length) { checks.push({ group: 'Schema.org', status: 'error', icon: '❌', title: 'JSON-LD con errori di sintassi', detail: 'Usa il Rich Results Test di Google per verificare.' }); } else { if (types.length > 0) checks.push({ group: 'Schema.org', status: 'ok', icon: '✅', title: `Dati strutturati validi (${schemas.length})`, detail: `Tipi riconosciuti: ${types.join(', ')}` }); if (unknownTypes.length > 0) checks.push({ group: 'Schema.org', status: 'warn', icon: '⚠️', title: `Tipo Schema.org non standard: ${unknownTypes.join(', ')}`, detail: 'Il tipo dichiarato non è nella lista dei tipi Schema.org comuni. Verifica che sia corretto su schema.org.' }); } } // 11. CHARSET const charset = head.querySelector('meta[charset]'); if (!charset) { checks.push({ group: 'Tecnici', status: 'warn', icon: '⚠️', title: 'Charset non dichiarato', detail: 'Dichiara <meta charset="UTF-8"> come prima riga del head.' }); } else { const cs = charset.getAttribute('charset'); checks.push({ group: 'Tecnici', status: cs.toUpperCase() === 'UTF-8' ? 'ok' : 'warn', icon: cs.toUpperCase() === 'UTF-8' ? '✅' : '⚠️', title: `Charset: ${cs}`, detail: cs.toUpperCase() === 'UTF-8' ? 'Encoding corretto.' : 'UTF-8 è lo standard consigliato.' }); } // 12. RELATIVE PATHS in og:image if (ogImage && (ogImage.startsWith('./') || ogImage.startsWith('../') || ogImage.startsWith('/'))) { checks.push({ group: 'Tecnici', status: 'warn', icon: '⚠️', title: 'og:image usa path relativo', detail: 'og:image richiede un URL assoluto (https://...) per funzionare sui social.', value: ogImage }); } return checks; } // ── CHECKS PERFORMANCE ─────────────────────────────────────── function runPerfChecks(head) { const checks = []; const raw = document.getElementById('head-input').value; // preconnect / dns-prefetch const hasPreconnect = head.querySelector('link[rel="preconnect"]') !== null; const hasDnsPrefetch = head.querySelector('link[rel="dns-prefetch"]') !== null; if (!hasPreconnect && !hasDnsPrefetch) { checks.push({ status: 'warn', icon: '⚠️', title: 'Nessun preconnect o dns-prefetch', detail: 'Aggiungi <link rel="preconnect"> per i domini esterni critici (font, CDN) per migliorare il LCP.' }); } else { if (hasPreconnect) checks.push({ status: 'ok', icon: '✅', title: 'preconnect presente', detail: 'Riduce il tempo di connessione ai domini esterni critici.' }); if (hasDnsPrefetch) checks.push({ status: 'ok', icon: '✅', title: 'dns-prefetch presente', detail: 'Risolve in anticipo i DNS per i domini esterni.' }); } // preload const hasPreload = head.querySelector('link[rel="preload"]') !== null; if (!hasPreload) { checks.push({ status: 'info', icon: 'ℹ️', title: 'Nessun preload dichiarato', detail: 'Usa <link rel="preload"> per risorse critiche (font, hero image) e migliorare il LCP.' }); } else { checks.push({ status: 'ok', icon: '✅', title: 'preload dichiarato', detail: 'Risorse critiche caricate in anticipo — ottimo per il LCP.' }); } // Google Fonts caricati via stylesheet (non solo preconnect) const googleFontsLinks = Array.from(head.querySelectorAll('link[rel="stylesheet"]')) .filter(el => (el.getAttribute('href') || '').includes('fonts.googleapis.com')); const hasGoogleFonts = googleFontsLinks.length > 0; const hasFontDisplay = googleFontsLinks.some(el => (el.getAttribute('href') || '').includes('display=swap')) || /font-display\s*:\s*swap/.test(raw); if (hasGoogleFonts && !hasFontDisplay) { checks.push({ status: 'warn', icon: '⚠️', title: 'Google Fonts senza font-display', detail: 'Aggiungi &display=swap all\'URL dei font Google per evitare il FOIT e migliorare il LCP.' }); } else if (hasGoogleFonts) { checks.push({ status: 'ok', icon: '✅', title: 'Google Fonts con font-display', detail: 'Font configurati correttamente per il rendering progressivo.' }); } // Script render-blocking const blockingScripts = Array.from(head.querySelectorAll('script:not([defer]):not([async])')).filter(s => { const t = (s.getAttribute('type') || '').toLowerCase(); return t !== 'application/ld+json' && t !== 'module'; }); if (blockingScripts.length > 0) { checks.push({ status: 'warn', icon: '⚠️', title: `${blockingScripts.length} script render-blocking`, detail: 'Script nel <head> senza defer o async bloccano il rendering e aumentano il TBT. Aggiungi defer o sposta in fondo al body.' }); } else { checks.push({ status: 'ok', icon: '✅', title: 'Nessun script render-blocking', detail: 'Tutti gli script nel head usano defer, async o type="module".' }); } // CSS bloccante const blockingCSS = head.querySelectorAll('link[rel="stylesheet"]:not([media])'); if (blockingCSS.length > 1) { checks.push({ status: 'warn', icon: '⚠️', title: `${blockingCSS.length} stylesheet bloccanti`, detail: 'Ogni <link rel="stylesheet"> senza media attribute blocca il rendering. Valuta di caricare CSS non critici in modo asincrono.' }); } else if (blockingCSS.length === 1) { checks.push({ status: 'ok', icon: '✅', title: '1 stylesheet bloccante', detail: 'Un solo CSS bloccante è accettabile per il foglio di stile principale.' }); } // Richieste esterne nel head const externalLinks = Array.from(head.querySelectorAll('link[href], script[src]')).filter(el => { const val = el.getAttribute('href') || el.getAttribute('src') || ''; return val.startsWith('http') || val.startsWith('//'); }); if (externalLinks.length > 0) { checks.push({ status: 'info', icon: 'ℹ️', title: `${externalLinks.length} richieste esterne nel head`, detail: `Ogni richiesta esterna aggiunge latenza. Valuta di ridurle o usare preconnect per i domini critici.` }); } // theme-color const themeColor = head.querySelector('meta[name="theme-color"]'); if (!themeColor) { checks.push({ status: 'info', icon: 'ℹ️', title: 'theme-color non dichiarato', detail: 'Il meta theme-color personalizza il colore dell\'interfaccia browser su mobile.' }); } else { checks.push({ status: 'ok', icon: '✅', title: `theme-color: ${themeColor.getAttribute('content')}`, detail: 'Colore interfaccia mobile dichiarato.' }); } return checks; } // ── SCORE ──────────────────────────────────────────────────── function calcScore(checks, perfChecks) { let total = 0, earned = 0; checks.forEach(c => { if (c.status === 'ok') { total += 10; earned += 10; } if (c.status === 'warn') { total += 10; earned += 5; } if (c.status === 'error') { total += 10; } }); (perfChecks || []).forEach(c => { if (c.status === 'ok') { total += 5; earned += 5; } if (c.status === 'warn') { total += 5; earned += 2; } if (c.status === 'error') { total += 5; } }); const score = total > 0 ? Math.round((earned / total) * 100) : 0; return { score, total }; } function scoreClass(s) { if (s >= 90) return 'excellent'; if (s >= 70) return 'good'; if (s >= 50) return 'warning'; return 'poor'; } function scoreLabel(s) { if (s >= 90) return '🎉 Eccellente'; if (s >= 70) return '👍 Buono'; if (s >= 50) return '⚠️ Migliorabile'; return '🚨 Critico'; } // ── RENDER REPORT ──────────────────────────────────────────── function renderReport(checks, perfChecks, scoreData) { const { score, total } = scoreData; const panel = document.getElementById('report-panel'); const cls = scoreClass(score); const allChecks = [...checks, ...(perfChecks || [])]; const nOk = checks.filter(c => c.status === 'ok').length; const nWarn = checks.filter(c => c.status === 'warn').length; const nError = checks.filter(c => c.status === 'error').length; const nPOk = (perfChecks||[]).filter(c => c.status === 'ok').length; const nPWarn = (perfChecks||[]).filter(c => c.status === 'warn').length; const nPError= (perfChecks||[]).filter(c => c.status === 'error').length; const ORDER = { error: 0, warn: 1, ok: 2, info: 3 }; const sorted = [...checks].sort((a, b) => ORDER[a.status] - ORDER[b.status]); const groups = {}; sorted.forEach(c => { if (!groups[c.group]) groups[c.group] = []; groups[c.group].push(c); }); let html = ` <div class="score-wrap"> <div class="score-circle ${cls}"> <span class="score-num">${score}</span> <span class="score-max">/100</span> </div> <div class="score-info"> <div class="score-label">${scoreLabel(score)}</div> <div class="score-sublabel">SEO: ✅ ${nOk} · ⚠️ ${nWarn} · ❌ ${nError}</div> <div class="score-sublabel">Perf: ✅ ${nPOk} · ⚠️ ${nPWarn} · ❌ ${nPError}</div> ${total < 120 ? '<div class="score-note">⚠ Head parziale — il punteggio è più affidabile su head completi</div>' : ''} </div> </div>`; Object.entries(groups).forEach(([group, items], gi) => { html += `<div class="report-section-label">${group}</div>`; items.forEach((c, i) => { html += `<div class="check-item ${c.status}" style="animation-delay:${(gi*5+i)*0.04}s"> <span class="check-icon">${c.icon}</span> <div class="check-content"> <div class="check-title">${c.title}</div> ${c.detail ? `<div class="check-detail">${c.detail}</div>` : ''} ${c.value ? `<div class="check-value">${escHtml(c.value.substring(0,120))}${c.value.length>120?'…':''}</div>` : ''} </div> </div>`; }); }); panel.innerHTML = html; } // ── RENDER PERF ────────────────────────────────────────────── function renderPerf(checks) { const panel = document.getElementById('perf-panel'); let html = ''; checks.forEach((c, i) => { html += `<div class="check-item ${c.status}" style="animation-delay:${i*0.05}s"> <span class="check-icon">${c.icon}</span> <div class="check-content"> <div class="check-title">${c.title}</div> ${c.detail ? `<div class="check-detail">${c.detail}</div>` : ''} </div> </div>`; }); panel.innerHTML = html; } // ── SERP PREVIEW ───────────────────────────────────────────── function updateSerpPreview(head) { const titleEl = head.querySelector('title'); const descEl = head.querySelector('meta[name="description"]'); const canonEl = head.querySelector('link[rel="canonical"]'); const title = titleEl ? titleEl.textContent.trim() : '—'; const desc = descEl ? descEl.getAttribute('content') || '—' : '—'; let url = canonEl ? canonEl.getAttribute('href') || '' : ''; if (url) { try { const u = new URL(url); url = u.hostname + u.pathname; } catch(e) {} } const titleEl2 = document.getElementById('serp-title'); titleEl2.textContent = title.length > 60 ? title.substring(0,57)+'…' : title; titleEl2.className = 'serp-title' + (title.length > 60 ? ' too-long' : title.length < 30 ? ' too-short' : ''); document.getElementById('serp-url').textContent = url || 'tuodominio.it › pagina'; document.getElementById('serp-desc').textContent = desc.length > 160 ? desc.substring(0,157)+'…' : desc; document.getElementById('serp-empty').style.display = 'none'; document.getElementById('serp-content').style.display = 'block'; } // ── SOCIAL PREVIEW ─────────────────────────────────────────── function updateSocialPreview(head) { const ogTitle = getProp(head, 'og:title') || head.querySelector('title')?.textContent || '—'; const ogDesc = getProp(head, 'og:description') || getMeta(head, 'description') || '—'; const ogImage = getProp(head, 'og:image') || ''; const ogUrl = getProp(head, 'og:url') || getLink(head, 'canonical') || ''; const twTitle = getMeta(head, 'twitter:title') || ogTitle; const twDesc = getMeta(head, 'twitter:description') || ogDesc; let domain = '—'; if (ogUrl) { try { domain = new URL(ogUrl).hostname.toUpperCase(); } catch(e) {} } document.getElementById('og-domain').textContent = domain; document.getElementById('og-title').textContent = ogTitle.substring(0, 80); document.getElementById('og-desc').textContent = ogDesc.substring(0, 120); document.getElementById('tw-title').textContent = twTitle.substring(0, 80); document.getElementById('tw-desc').textContent = twDesc.substring(0, 120); if (ogImage && ogImage.startsWith('https://')) { const img = document.getElementById('og-img'); img.src = ogImage; img.style.display = 'block'; document.getElementById('og-img-ph').style.display = 'none'; } document.getElementById('social-empty').style.display = 'none'; document.getElementById('social-content').style.display = 'flex'; } function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } // ── MAIN ───────────────────────────────────────────────────── function analyze() { const raw = document.getElementById('head-input').value.trim(); const inputEl = document.getElementById('head-input'); const errorMsg = document.getElementById('input-error-msg'); if (!raw) { inputEl.classList.add('input-error'); errorMsg.textContent = '⚠ Incolla il codice del tuo <head> prima di procedere.'; errorMsg.style.display = 'block'; return; } inputEl.classList.remove('input-error'); errorMsg.style.display = 'none'; const head = parseHead(raw); const checks = runChecks(head); const perfChecks = runPerfChecks(head); const scoreData = calcScore(checks, perfChecks); document.getElementById('tab-bar').style.display = 'flex'; renderReport(checks, perfChecks, scoreData); renderPerf(perfChecks); document.getElementById('perf-empty').style.display = 'none'; document.getElementById('perf-panel').style.display = 'flex'; updateSerpPreview(head); updateSocialPreview(head); } ); </script> </body> </html>