Costruito su richiesta di Vincenzo Rubino
2026

Analisi SEO Gratuita

50+ controlli SEO inclusi E-E-A-T e GEO (AI Search Readiness).
L'unico tool gratuito che verifica se sei pronto per Google AI Overviews.

Aggiornato 2026
Analisi istantanea
🔒 Gratuito al 100%
🌐 Qualsiasi URL
🔍
Analisi On-Page
Titolo, meta description, heading, keyword density
📷
Controllo Immagini
Alt text mancanti, immagini pesanti, ottimizzazione
🔗
Analisi Link
Link interni, esterni, broken link, struttura
📱
Social & Mobile
Open Graph, Twitter Card, viewport, performance
👤
E-E-A-T Signals
Autore, contatti, privacy, trust, schema Organization
🤖
GEO — AI Readiness
Ottimizzazione per Google AI Overviews, ChatGPT, Perplexity
1

Inserisci URL

Incolla l'indirizzo della pagina da analizzare

2

Analisi automatica

Scansione completa in pochi secondi

3

Report dettagliato

Problemi, suggerimenti e punteggio SEO

Analizza il tuo sito
Inserisci un URL completo con https://
Inserisci la keyword per cui vuoi posizionarti. Otterrai un'analisi di ottimizzazione dedicata.
Prova con:

Come funziona l'analisi SEO

L'analisi SEO (Search Engine Optimization) è il processo di valutazione di una pagina web per identificare problemi che impediscono un buon posizionamento sui motori di ricerca come Google. Un buon SEO analyzer esamina oltre 50 fattori critici: dal titolo della pagina alla struttura degli heading, dalla velocità di caricamento alla compatibilità mobile.

Perché l'analisi SEO è importante per le PMI

Per le piccole e medie imprese, apparire nelle prime posizioni di Google può fare la differenza tra crescere o restare invisibili. Il 75% degli utenti non va oltre la prima pagina dei risultati. Un'analisi SEO gratuita ti permette di scoprire esattamente cosa migliorare: meta description mancanti, immagini senza alt text, link rotti, problemi di accessibilità e molto altro.

Cosa analizza il nostro SEO Analyzer

Il nostro strumento gratuito controlla in tempo reale: tag title e meta description (lunghezza e contenuto), gerarchia degli heading (H1-H6), ottimizzazione delle immagini (alt text, dimensioni, lazy loading), struttura dei link interni ed esterni, leggibilità del testo (Flesch score), keyword density, tag Open Graph e Twitter Card per i social media, dati strutturati JSON-LD, aspetti tecnici (viewport, canonical, hreflang, HTTPS), header di sicurezza, accessibilità (ARIA, form labels, skip navigation) e crawlability (robots.txt, sitemap.xml). Il report include un punteggio da 0 a 100, un'anteprima Google, raccomandazioni prioritarie e una cronologia delle analisi per monitorare i miglioramenti nel tempo.

Free SEO analysis vs paid tools

Unlike paid SEO tools that charge $50-200/month, this free SEO analyzer gives you an instant, comprehensive audit with no registration required. You get the same core checks that professional tools offer: on-page SEO scoring, technical SEO validation, mobile-friendliness testing, security header verification, and accessibility auditing. Perfect for small businesses, freelancers, and anyone who wants to understand how search engines see their website.

I 35+ controlli SEO spiegati nel dettaglio

Il nostro SEO analyzer esegue un'analisi approfondita su oltre 50 aree critiche, organizzate in categorie chiave:

Tag Title e Meta Description — Il titolo della pagina è il primo elemento che gli utenti vedono nei risultati di Google. Deve essere lungo tra 50 e 60 caratteri, contenere la keyword principale e essere unico per ogni pagina. La meta description (150-160 caratteri) deve convincere l'utente a cliccare. Il nostro tool verifica lunghezza, presenza della keyword e unicità di entrambi.

Gerarchia Heading (H1-H6) — Una struttura heading corretta aiuta Google a comprendere la gerarchia dei contenuti. Deve esserci un solo H1 per pagina, coerente con il title tag. Gli heading successivi (H2, H3...) non devono saltare livelli. Il nostro analyzer rileva heading mancanti, duplicati e livelli saltati.

Ottimizzazione Immagini — Le immagini senza attributo alt text sono invisibili ai motori di ricerca e agli screen reader. Immagini senza dimensioni (width/height) causano CLS (Cumulative Layout Shift), un Core Web Vital di Google. Verifichiamo anche il lazy loading per le immagini below-the-fold e il peso complessivo.

Analisi Link — I link interni distribuiscono l'autorità tra le pagine del sito. I link esterni verso fonti autorevoli migliorano la credibilità. Link rotti (404) danneggiano sia l'esperienza utente che il ranking. Analizziamo quantità, qualità degli anchor text e presenza di link problematici.

Leggibilità e Contenuto — Google premia contenuti approfonditi e ben strutturati. Calcoliamo il Flesch Reading Ease score, il rapporto testo/HTML, la presenza di paragrafi, liste, grassetto e media. Pagine con meno di 300 parole sono considerate "thin content" e difficilmente si posizionano bene.

Aspetti Tecnici e Sicurezza — Viewport meta tag per il mobile, canonical URL per evitare contenuti duplicati, hreflang per siti multilingua, HTTPS per la sicurezza, header come HSTS e CSP. Controlliamo anche robots.txt, sitemap.xml e la crawlability complessiva della pagina.

Come migliorare il posizionamento SEO del tuo sito

Migliorare il SEO non è un'azione una tantum ma un processo continuo. Ecco i passi più efficaci, in ordine di priorità:

1. Correggi gli errori critici — Inizia dai problemi segnalati in rosso nel report: title mancante, nessun H1, immagini senza alt text, link rotti. Questi errori bloccano attivamente il tuo posizionamento.

2. Ottimizza title e meta description — Scrivi un titolo che includa la keyword principale nei primi 60 caratteri. La meta description deve essere un "mini-annuncio" che invoglia al click. Ogni pagina deve avere titolo e description unici.

3. Crea contenuti approfonditi — Google premia pagine che rispondono completamente alla domanda dell'utente. Punta a 1000+ parole per pagine informative, con heading strutturati, liste, e paragrafi brevi. Usa il nostro SEO Analyzer per misurare la qualità del contenuto.

4. Migliora la velocità — Comprimi le immagini (prova il nostro compressore immagini), abilita il lazy loading, minimizza CSS e JavaScript. Il tempo di caricamento è un fattore di ranking diretto.

5. Monitora e ripeti — Usa il nostro Site Monitor per ricevere un controllo automatico ogni settimana. Traccia i miglioramenti nel tempo con grafici e alert quando qualcosa peggiora.

SEO Analyzer per e-commerce e siti aziendali

L'analisi SEO è particolarmente critica per gli e-commerce e i siti aziendali. Le pagine prodotto devono avere descrizioni uniche (non copiate dal fornitore), immagini ottimizzate con alt text descrittivi, dati strutturati Product con prezzo e disponibilità, e URL puliti con la keyword. I siti aziendali devono curare le pagine servizi, il blog e le landing page per catturare traffico a diverse fasi del funnel. Il nostro analyzer controlla tutti questi aspetti in pochi secondi, aiutandoti a identificare le pagine che necessitano di intervento immediato.

Checklist SEO 2026: i 10 controlli essenziali

Prima di pubblicare qualsiasi pagina web, verifica questi 10 punti fondamentali del SEO on-page:

  1. Title tag presente, unico, 50-60 caratteri, con keyword principale
  2. Meta description presente, 150-160 caratteri, con call-to-action
  3. Un solo H1 per pagina, coerente con il title
  4. Gerarchia heading corretta: H1 → H2 → H3, senza salti
  5. Immagini ottimizzate: alt text descrittivo, dimensioni specificate, formato WebP/AVIF
  6. URL pulito e leggibile con keyword (no parametri o ID numerici)
  7. Link interni verso pagine correlate (min. 3-5 per pagina)
  8. HTTPS attivo con certificato valido e redirect da HTTP
  9. Mobile responsive: viewport tag, font leggibili, tap target ≥48px
  10. Core Web Vitals: LCP < 2.5s, CLS < 0.1, INP < 200ms

Come analizzare il SEO di un sito web

Analizzare il SEO di un sito web significa esaminare sistematicamente tutti gli elementi che influenzano il posizionamento sui motori di ricerca. Che tu sia un principiante o un professionista, il processo è lo stesso:

Passo 1: Inserisci l'URL — Copia l'indirizzo completo del sito che vuoi analizzare (es. https://example.com) e incollalo nel campo sopra. Il nostro analyzer supporta qualsiasi sito web pubblico, indipendentemente dalla piattaforma (WordPress, Shopify, Wix, custom).

Passo 2: Leggi il punteggio — Il report mostra un punteggio da 0 a 100 suddiviso in categorie: SEO on-page, performance, sicurezza, mobile e contenuto. I problemi critici sono segnalati in rosso, i warning in giallo, i check superati in verde.

Passo 3: Correggi per priorità — Inizia sempre dagli errori critici (rossi), poi passa ai warning. Ogni problema include una spiegazione e il codice da usare per risolverlo. Un miglioramento di 10-20 punti è possibile in meno di un'ora correggendo i problemi principali.

Passo 4: Rianalizza — Dopo le correzioni, lancia una nuova analisi per verificare i miglioramenti. Il confronto prima/dopo ti mostra esattamente quanto hai guadagnato in ogni area.

Errori SEO comuni che il 90% dei siti commette

Dopo aver analizzato migliaia di siti web, questi sono gli errori SEO più frequenti: title tag generico come "Home" o il nome dell'azienda (senza keyword); meta description assente, lasciando a Google la scelta del testo mostrato nei risultati; immagini senza alt text, rinunciando a una fonte importante di traffico da Google Images; nessun link interno, impedendo ai motori di ricerca di scoprire e valorizzare le altre pagine; contenuto troppo breve (sotto 300 parole) che Google classifica come "thin content"; e mancanza di dati strutturati (Schema.org), perdendo l'opportunità di apparire con rich snippet nei risultati di ricerca. Usa il nostro SEO analyzer per scoprire quali di questi errori riguardano il tuo sito e risolverli oggi stesso.

Strumenti correlati

Completa la tua strategia SEO con gli altri strumenti gratuiti di ANIMA: il generatore di codici a barre per i tuoi prodotti, il generatore QR code per il marketing offline-to-online, il generatore di firma email per comunicazioni professionali, e il calcolatore IVA per la gestione fiscale.

✎ Suggerisci una modifica

Domande Frequenti

Cos'è un SEO Analyzer?

È uno strumento che analizza una pagina web e identifica problemi di ottimizzazione per i motori di ricerca (SEO). Controlla titolo, meta description, heading, immagini, link e aspetti tecnici.

È davvero gratuito?

Sì, completamente gratuito e senza limiti. Non serve registrazione.

Quali aspetti SEO vengono analizzati?

40+ aree di analisi: titolo, meta description, coerenza titolo/H1, gerarchia heading (H1-H6 con rilevamento livelli saltati), immagini (alt text, width/height per CLS, lazy loading), link (interni/esterni/problematici con analisi qualità anchor text), contenuto e struttura (paragrafi, liste, grassetto, tabelle, media), leggibilità (Flesch score e rapporto testo/HTML), keyword density con analisi n-gram, mappa posizionamento keyword, tag social (Open Graph e Twitter Card), mobile e performance, Core Web Vitals via Google PageSpeed (LCP, CLS, TBT, FCP), rilevamento tecnologie (CMS, framework, analytics, CDN), dati strutturati (JSON-LD), aspetti tecnici (viewport, charset, canonical, hreflang, HTTPS, favicon), rilevamento noindex/nofollow, catena redirect (301/302), struttura URL, crawlability (robots.txt, sitemap.xml, peso pagina), header di sicurezza (HSTS, CSP, X-Frame-Options), accessibilità (ARIA labels, form labels, skip link, tabindex, qualità alt text) e tempo di caricamento. Include anteprima Google/social, cronologia analisi con grafico trend, stime impatto per ogni problema, copia raccomandazioni e report HTML scaricabile.

Come migliorare il punteggio SEO?

Segui i suggerimenti nel report: ottimizza titolo e meta description, usa un solo H1, aggiungi alt text alle immagini, scrivi contenuti approfonditi (300+ parole), configura correttamente gli aspetti tecnici. Controlla anche le anteprime Google e social per assicurarti che la tua pagina appaia bene nei risultati di ricerca e sui social media.

Funziona con qualsiasi sito?

Funziona con la maggior parte dei siti web pubblici. Alcuni siti con protezioni anti-bot potrebbero non essere analizzabili.

Cos'è il CLS e perché controllate le dimensioni delle immagini?

Il CLS (Cumulative Layout Shift) è un Core Web Vital di Google che misura la stabilità visiva della pagina. Le immagini senza attributi width e height causano spostamenti del layout durante il caricamento, peggiorando l'esperienza utente e il ranking. Il nostro analyzer verifica che ogni immagine abbia dimensioni definite e utilizzi il lazy loading per le immagini non visibili inizialmente.

Perché controllate robots.txt, sitemap.xml e header di sicurezza?

Il robots.txt guida i crawler dei motori di ricerca, la sitemap.xml li aiuta a scoprire tutte le pagine. Senza questi file, l'indicizzazione potrebbe essere incompleta. Gli header di sicurezza (HSTS, CSP, X-Frame-Options) proteggono il sito da attacchi e sono un segnale positivo per Google, che favorisce i siti sicuri nei risultati di ricerca.

Cos'è l'E-E-A-T e perché è importante per la SEO?

E-E-A-T sta per Experience, Expertise, Authoritativeness, Trustworthiness (Esperienza, Competenza, Autorevolezza, Affidabilità). È un insieme di criteri che Google usa per valutare la qualità di un sito. Il nostro analyzer verifica la presenza di informazioni autore, pagina About, contatti, privacy policy, profili social e schema Organization/Person. Siti con forti segnali E-E-A-T si posizionano meglio, specialmente per argomenti YMYL (salute, finanza, legale).

Cos'è il GEO (Generative Engine Optimization) e perché lo controllate?

Il GEO è l'ottimizzazione per i motori di ricerca basati su AI come Google AI Overviews, ChatGPT e Perplexity. Nel 2026, oltre l'86% delle ricerche Google mostra risultati generati dall'AI. Il nostro analyzer verifica se il contenuto ha risposte dirette all'inizio, schema FAQ/HowTo, pattern domanda-risposta, dati statistici, contenuto strutturato e definizioni esplicite — tutti fattori che aumentano la probabilità di essere citati nelle risposte AI.

Posso scaricare il report SEO come PDF?

Sì, offriamo due opzioni di esportazione completamente gratuite: 'Salva PDF' apre il report in una nuova scheda con la finestra di stampa del browser, dove puoi salvare come PDF con un click. 'Scarica HTML' scarica un file HTML completo con tutte le analisi, i grafici e le raccomandazioni. Puoi anche copiare le raccomandazioni come testo con il pulsante 'Copia Raccomandazioni'. Tutti i competitor fanno pagare per l'export PDF — noi lo offriamo gratis.

Posso confrontare il mio sito con un competitor?

Sì! Clicca il pulsante 'Confronta con un competitor' sotto il campo URL principale e inserisci l'URL del competitor. Dopo l'analisi vedrai un confronto fianco a fianco con punteggi, voti per categoria e indicazione chiara di chi vince in ogni area. Nessun altro tool gratuito offre questa funzione — di solito è riservata ai piani premium.

Prova i tool potenziati dall'AI

Genera testi professionali, email, bio e slogan in pochi secondi. 10 crediti gratis alla registrazione.

'}); } structItems.push({tip:L('tip_schema')}); renderSection('seo-detail-structured','🔬',L('sec_structured'),structStatus, structItems); // --- TECHNICAL --- const hasViewport = !!viewport; const hasCharset = !!doc.querySelector('meta[charset]') || html.toLowerCase().includes('charset=utf-8'); const hasCanonical = !!doc.querySelector('link[rel="canonical"]'); const hasLang = doc.documentElement && doc.documentElement.getAttribute('lang'); const hasRobots = !!doc.querySelector('meta[name="robots"]'); const hasOG = !!ogTitle; let techScore=0, techMax=6; if(hasViewport) techScore++; if(hasCharset) techScore++; if(hasCanonical) techScore++; if(hasLang) techScore++; if(!hasRobots || !(doc.querySelector('meta[name="robots"]')?.getAttribute('content')||'').includes('noindex')) techScore++; if(hasOG) techScore++; maxScore += 20; score += Math.round((techScore/techMax)*20); let techStatus = techScore>=5?'ok':techScore>=3?'warn':'fail'; checks.push({status:techStatus, text: L('sec_technical')+': '+techScore+'/'+techMax}); renderSection('seo-detail-technical','⚙',L('sec_technical'),techStatus,[ {label:L('tech_viewport'), value: hasViewport?'✓':'✗', raw:true}, {label:L('tech_charset'), value: hasCharset?'✓':'✗', raw:true}, {label:L('tech_canonical'), value: hasCanonical?'✓':'✗', raw:true}, {label:L('tech_lang'), value: hasLang||'✗', raw:true}, {label:L('tech_robots'), value: hasRobots?(doc.querySelector('meta[name="robots"]').getAttribute('content')):'—', raw:true}, {label:L('tech_og'), value: hasOG?'✓':'✗', raw:true}, ].concat((!hasCanonical||!hasLang||!hasCharset)?[{fix:(!hasCharset?'\n':'')+(!hasLang?'\n':'')+(!hasCanonical?'':'')}]:[]).concat([ {tip:L('tip_viewport')} ])); // --- URL STRUCTURE --- const urlPath = baseUrl.pathname; const urlFull = baseUrl.href; let urlScore=0, urlMax=4, urlIssues=[]; maxScore += 10; if(urlFull.length<=75) urlScore++; else urlIssues.push(L('url_long')+' ('+urlFull.length+')'); if(!baseUrl.search) urlScore++; else urlIssues.push(L('url_params')); if(urlPath===urlPath.toLowerCase()) urlScore++; else urlIssues.push(L('url_uppercase')); if(!urlPath.includes('_')) urlScore++; else urlIssues.push(L('url_underscores')); let urlStatus = urlScore>=4?'ok':urlScore>=2?'warn':'fail'; score += Math.round((urlScore/urlMax)*10); checks.push({status:urlStatus, text: L('sec_url')+': '+urlScore+'/'+urlMax}); renderSection('seo-detail-url','🔗',L('sec_url'),urlStatus,[ {label:'URL', value: urlFull}, {label:'Path', value: urlPath}, {label:'Length', value: urlFull.length+' chars'}, ...(urlIssues.length?urlIssues.map(m=>({status:'warn',msg:m})):[{status:'ok',msg:L('url_ok')}]), {tip:L('tip_url')} ]); // --- READABILITY & TEXT-TO-HTML RATIO --- const sentences = bodyText.split(/[.!?]+/).filter(s=>s.trim().length>5); const avgSentenceLen = sentences.length>0 ? bodyText.split(/\s+/).length / sentences.length : 0; const syllableCount = (word) => { word = word.toLowerCase().replace(/[^a-z]/g,''); if(word.length<=3) return 1; let count = word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/,'').replace(/^y/,'').match(/[aeiouy]{1,2}/g); return count ? count.length : 1; }; const words = bodyText.split(/\s+/).filter(w=>w.length>0); const totalSyllables = words.reduce((sum,w)=>sum+syllableCount(w),0); const fleschScore = words.length>0&&sentences.length>0 ? Math.round(206.835 - 1.015*(words.length/sentences.length) - 84.6*(totalSyllables/words.length)) : 0; const htmlLen = html.length; const textLen = bodyText.length; const textHtmlRatio = htmlLen>0 ? Math.round((textLen/htmlLen)*100) : 0; let readStatus, readMsg; if(fleschScore>=60){ readStatus='ok'; readMsg=L('readability_easy')+' (Flesch: '+fleschScore+')'; } else if(fleschScore>=30){ readStatus='warn'; readMsg=L('readability_moderate')+' (Flesch: '+fleschScore+')'; } else{ readStatus='fail'; readMsg=L('readability_hard')+' (Flesch: '+fleschScore+')'; } let ratioStatus = textHtmlRatio>=15?'ok':textHtmlRatio>=10?'warn':'fail'; let ratioMsg = textHtmlRatio>=15?L('text_html_ok'):L('text_html_low'); const overallReadStatus = readStatus==='ok'&&ratioStatus==='ok'?'ok':(readStatus==='fail'||ratioStatus==='fail')?'fail':'warn'; checks.push({status:overallReadStatus, text: L('sec_readability')+': Flesch '+fleschScore+', '+L('text_html_ratio')+' '+textHtmlRatio+'%'}); // Load time let loadStatus='ok', loadLabel=L('load_fast'); if(loadMs>3000){ loadStatus='fail'; loadLabel=L('load_slow'); } else if(loadMs>1500){ loadStatus='warn'; loadLabel=L('load_medium'); } renderSection('seo-detail-readability','📖',L('sec_readability'),overallReadStatus,[ {label:'Flesch Reading Ease', value: fleschScore+'/100'}, {label:L('text_html_ratio'), value: textHtmlRatio+'%'}, {label:L('load_time'), value: (loadMs/1000).toFixed(2)+'s — '+loadLabel}, {status:readStatus, msg:readMsg}, {status:ratioStatus, msg:ratioMsg}, {tip:L('tip_readability')} ]); // --- KEYWORD DENSITY (with n-gram analysis) --- const stopWords = new Set(['the','a','an','and','or','but','in','on','at','to','for','of','with','by','from','is','it','this','that','are','was','were','be','been','have','has','had','do','does','did','will','would','could','should','can','may','might','not','no','so','if','as','its','i','you','he','she','we','they','my','your','our','their','me','him','her','us','them','what','which','who','when','where','how','all','each','every','both','few','more','most','other','some','such','than','too','very','just','about','up','out','into','over','after','before','between','under','above','il','lo','la','le','gli','un','una','di','del','della','dei','delle','da','dal','dalla','in','nel','nella','con','su','sul','sulla','per','tra','fra','che','e','o','ma','non','si','ci','ne','come','anche','più','questo','questa','sono','è','ha','ho','al','alla','ai','alle']); // Clean words for keyword analysis const cleanWords = words.map(w=>w.toLowerCase().replace(/[^a-zàèéìòùäöüß0-9]/g,'')).filter(w=>w.length>2); const significantWords = cleanWords.filter(w=>!stopWords.has(w)); // Single keywords const kwMap = {}; significantWords.forEach(w=>{ kwMap[w]=(kwMap[w]||0)+1; }); const kwSorted = Object.entries(kwMap).sort((a,b)=>b[1]-a[1]).slice(0,8); const totalWords = words.length; // Bigrams (2-word phrases) const bigramMap = {}; for(let i=0;i2 && w2.length>2){ const bg=w1+' '+w2; bigramMap[bg]=(bigramMap[bg]||0)+1; } } } const biSorted = Object.entries(bigramMap).filter(([,c])=>c>=2).sort((a,b)=>b[1]-a[1]).slice(0,5); // Trigrams (3-word phrases) const trigramMap = {}; for(let i=0;i2 && w2.length>2 && w3.length>2){ const tg=w1+' '+w2+' '+w3; trigramMap[tg]=(trigramMap[tg]||0)+1; } } const triSorted = Object.entries(trigramMap).filter(([,c])=>c>=2).sort((a,b)=>b[1]-a[1]).slice(0,5); // Status let kwStatus='ok', kwMsg=L('kw_good'); if(kwSorted.length===0){ kwStatus='warn'; kwMsg=L('kw_none'); } else if(totalWords>0 && kwSorted[0][1]/totalWords > 0.05){ kwStatus='warn'; kwMsg=L('kw_high')+' ('+kwSorted[0][0]+': '+(kwSorted[0][1]/totalWords*100).toFixed(1)+'%)'; } checks.push({status:kwStatus, text: L('sec_keywords')+': '+(kwSorted.length?kwSorted[0][0]+' ('+kwSorted[0][1]+'x)':'—')+(biSorted.length?', "'+biSorted[0][0]+'" ('+biSorted[0][1]+'x)':'')}); // Build items with single + bigram + trigram sections const kwItems = []; kwItems.push({label:''+L('kw_top')+'', value:'', raw:true}); kwSorted.forEach(([kw,cnt])=>{ kwItems.push({label:kw, value:cnt+' '+L('kw_occurrences')+' — '+(totalWords>0?(cnt/totalWords*100).toFixed(1):'0')+'% '+L('kw_density')}); }); if(biSorted.length>0){ kwItems.push({label:''+L('kw_bigrams')+'', value:'', raw:true}); biSorted.forEach(([bg,cnt])=>{ kwItems.push({label:'"'+bg+'"', value:cnt+' '+L('kw_occurrences')}); }); } if(triSorted.length>0){ kwItems.push({label:''+L('kw_trigrams')+'', value:'', raw:true}); triSorted.forEach(([tg,cnt])=>{ kwItems.push({label:'"'+tg+'"', value:cnt+' '+L('kw_occurrences')}); }); } kwItems.push({status:kwStatus, msg:kwMsg}); kwItems.push({tip:L('tip_keywords')}); renderSection('seo-detail-keywords','🔑',L('sec_keywords'),kwStatus,kwItems); // --- KEYWORD PLACEMENT MAP --- if(kwSorted.length > 0){ const topKws = kwSorted.slice(0,5).map(([kw])=>kw); const titleLow = titleText.toLowerCase(); const h1Text = h1s.length>0 ? h1s[0].textContent.trim().toLowerCase() : ''; const metaLow = metaText.toLowerCase(); const urlLow = decodeURIComponent(baseUrl.pathname.toLowerCase().replace(/[-_/]/g,' ')); // Get first meaningful paragraph text const firstParas = Array.from(doc.querySelectorAll('p')).filter(p=>p.textContent.trim().length>30); const firstPText = firstParas.length>0 ? firstParas[0].textContent.trim().toLowerCase() : ''; const ck = (text,kw) => text.includes(kw); let gridHtml = '
'; gridHtml += '
'+L('kwmap_keyword_col')+'
'; gridHtml += '
'+L('kwmap_title_col')+'
'; gridHtml += '
'+L('kwmap_h1_col')+'
'; gridHtml += '
'+L('kwmap_meta_col')+'
'; gridHtml += '
'+L('kwmap_url_col')+'
'; gridHtml += '
'+L('kwmap_first_p')+'
'; let totalHits=0, totalSlots=0; topKws.forEach(kw=>{ const inTitle=ck(titleLow,kw), inH1=ck(h1Text,kw), inMeta=ck(metaLow,kw), inUrl=ck(urlLow,kw), inFirst=ck(firstPText,kw); const hits=[inTitle,inH1,inMeta,inUrl,inFirst].filter(Boolean).length; totalHits+=hits; totalSlots+=5; const y='', n=''; gridHtml += '
'+kw+'
'; gridHtml += '
'+(inTitle?y:n)+'
'; gridHtml += '
'+(inH1?y:n)+'
'; gridHtml += '
'+(inMeta?y:n)+'
'; gridHtml += '
'+(inUrl?y:n)+'
'; gridHtml += '
'+(inFirst?y:n)+'
'; }); gridHtml += '
'; const kwmapPct = totalSlots>0 ? Math.round((totalHits/totalSlots)*100) : 0; let kwmapStatus, kwmapMsg; if(kwmapPct>=60){ kwmapStatus='ok'; kwmapMsg=L('kwmap_good'); } else if(kwmapPct>=30){ kwmapStatus='warn'; kwmapMsg=L('kwmap_fair'); } else{ kwmapStatus='fail'; kwmapMsg=L('kwmap_poor'); priorityActions.push({icon:'🔑',msg:L('kwmap_poor'),severity:'high'}); } checks.push({status:kwmapStatus, text: L('sec_kwmap')+': '+kwmapPct+'% coverage'}); renderSection('seo-detail-kwmap','🎯',L('sec_kwmap'),kwmapStatus,[ {label:'', value:gridHtml, raw:true}, {label:'Coverage', value: kwmapPct+'% ('+totalHits+'/'+totalSlots+')'}, {status:kwmapStatus, msg:kwmapMsg}, {tip:L('tip_kwmap')} ]); } // --- TARGET KEYWORD OPTIMIZATION (v8) --- const targetKw = (meta.targetKeyword||'').toLowerCase().trim(); if(targetKw){ const tkItems = []; let tkScore = 0, tkMax = 10; const tkLow = targetKw; const titleLow2 = titleText.toLowerCase(); const h1Text2 = h1s.length>0 ? h1s[0].textContent.trim().toLowerCase() : ''; const metaLow2 = metaText.toLowerCase(); const urlSlug = decodeURIComponent(baseUrl.pathname.toLowerCase().replace(/[-_/]/g,' ')); const firstParas2 = Array.from(doc.querySelectorAll('p')).filter(p=>p.textContent.trim().length>30); const first100 = firstParas2.length>0 ? firstParas2.slice(0,3).map(p=>p.textContent.trim().toLowerCase()).join(' ') : ''; const altTexts = Array.from(imgs).map(i=>(i.getAttribute('alt')||'').toLowerCase()).join(' '); // Check title position (beginning is best) const inTitle2 = titleLow2.includes(tkLow); const titlePos = inTitle2 ? titleLow2.indexOf(tkLow) : -1; const titleAtStart = titlePos>=0 && titlePos<=5; if(inTitle2){ tkScore+=2; if(titleAtStart) tkScore+=1; } tkItems.push({label:L('tk_in_title'), value: inTitle2 ? (titleAtStart ? '✓ '+L('tk_at_start')+'' : '✓ '+L('tk_pos')+' '+(titlePos+1)+'') : '', raw:true}); // H1 const inH12 = h1Text2.includes(tkLow); if(inH12) tkScore+=2; tkItems.push({label:L('tk_in_h1'), value: inH12?'':'', raw:true}); // Meta const inMeta2 = metaLow2.includes(tkLow); if(inMeta2) tkScore+=1; tkItems.push({label:L('tk_in_meta'), value: inMeta2?'':'', raw:true}); // URL const inUrl2 = urlSlug.includes(tkLow.replace(/\s+/g,' ')); if(inUrl2) tkScore+=1; tkItems.push({label:L('tk_in_url'), value: inUrl2?'':'', raw:true}); // First 100 words const inFirst2 = first100.includes(tkLow); if(inFirst2) tkScore+=1; tkItems.push({label:L('tk_in_first'), value: inFirst2?'':'', raw:true}); // Alt text const inAlt = altTexts.includes(tkLow); if(inAlt) tkScore+=1; tkItems.push({label:L('tk_in_alt'), value: inAlt?'':'', raw:true}); // Keyword density for target keyword const bodyLow = bodyText.toLowerCase(); const tkRegex = new RegExp(tkLow.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'gi'); const tkMatches = bodyLow.match(tkRegex); const tkCount = tkMatches ? tkMatches.length : 0; const tkDensity = totalWords>0 ? (tkCount/totalWords*100).toFixed(2) : '0'; const densityOk = parseFloat(tkDensity)>=0.5 && parseFloat(tkDensity)<=3.0; if(densityOk) tkScore+=1; const densityColor = densityOk ? '#10b981' : parseFloat(tkDensity)<0.5 ? '#f59e0b' : '#ef4444'; tkItems.push({label:L('tk_density'), value: ''+tkDensity+'% ('+tkCount+'x / '+totalWords+' '+L('content_words')+')', raw:true}); // Ideal range bar const densityPct = Math.min(5, parseFloat(tkDensity)); const barHtml = '
' + '
' + '
' + '
0%0.5-2.5% '+L('tk_ideal')+'5%+
'; tkItems.push({label:'', value: barHtml, raw:true}); // Overall score tkPct = Math.round((tkScore/tkMax)*100); let tkStatus, tkMsg; if(tkPct>=80){ tkStatus='ok'; tkMsg=L('tk_excellent'); } else if(tkPct>=50){ tkStatus='warn'; tkMsg=L('tk_fair'); } else { tkStatus='fail'; tkMsg=L('tk_poor'); priorityActions.push({icon:'🎯',msg:L('tk_poor')+' ("'+targetKw+'")',severity:'high'}); } tkItems.push({status:tkStatus, msg:tkMsg+' ('+tkPct+'%)'}); // Suggestions if(!inTitle2) tkItems.push({fix:''+targetKw.charAt(0).toUpperCase()+targetKw.slice(1)+' — '+L('tk_fix_title')+''}); if(!inH12) tkItems.push({fix:'

'+targetKw.charAt(0).toUpperCase()+targetKw.slice(1)+' — '+L('tk_fix_h1')+'

'}); if(!inMeta2) tkItems.push({fix:''}); tkItems.push({tip:L('tip_targetkw')}); checks.push({status:tkStatus, text: L('sec_targetkw')+': "'+targetKw+'" — '+tkPct+'%'}); maxScore += 15; score += Math.round((tkScore/tkMax)*15); renderSection('seo-detail-targetkw','🎯',L('sec_targetkw')+': '+targetKw+'',tkStatus,tkItems); } else { document.getElementById('seo-detail-targetkw').style.display='none'; } // --- HREFLANG DEEP VALIDATION (v8) --- const hreflangTags = doc.querySelectorAll('link[hreflang]'); if(hreflangTags.length > 0){ const hlItems = []; let hlIssues = 0; const validLangs = new Set(['aa','ab','af','ak','am','an','ar','as','av','ay','az','ba','be','bg','bh','bi','bm','bn','bo','br','bs','ca','ce','ch','co','cr','cs','cu','cv','cy','da','de','dv','dz','ee','el','en','eo','es','et','eu','fa','ff','fi','fj','fo','fr','fy','ga','gd','gl','gn','gu','gv','ha','he','hi','ho','hr','ht','hu','hy','hz','ia','id','ie','ig','ii','ik','io','is','it','iu','ja','jv','ka','kg','ki','kj','kk','kl','km','kn','ko','kr','ks','ku','kv','kw','ky','la','lb','lg','li','ln','lo','lt','lu','lv','mg','mh','mi','mk','ml','mn','mr','ms','mt','my','na','nb','nd','ne','ng','nl','nn','no','nr','nv','ny','oc','oj','om','or','os','pa','pi','pl','ps','pt','qu','rm','rn','ro','ru','rw','sa','sc','sd','se','sg','si','sk','sl','sm','sn','so','sq','sr','ss','st','su','sv','sw','ta','te','tg','th','ti','tk','tl','tn','to','tr','ts','tt','tw','ty','ug','uk','ur','uz','ve','vi','vo','wa','wo','xh','yi','yo','za','zh','zu']); const hlMap = {}; let hasXDefault = false; let hasSelfRef = false; const pageUrl = new URL(url).href.replace(/\/$/,''); hreflangTags.forEach(tag=>{ const lang = (tag.getAttribute('hreflang')||'').toLowerCase().trim(); const href = (tag.getAttribute('href')||'').trim(); hlMap[lang] = href; if(lang==='x-default') hasXDefault=true; try{ if(new URL(href, url).href.replace(/\/$/,'')===pageUrl) hasSelfRef=true; }catch(e){} }); // List all tags Object.entries(hlMap).forEach(([lang,href])=>{ const shortHref = href.length>60 ? href.substring(0,57)+'...' : href; hlItems.push({label:''+lang+'', value: shortHref, raw:true}); }); // Validate language codes const invalidLangs = []; Object.keys(hlMap).forEach(lang=>{ if(lang==='x-default') return; const base = lang.split('-')[0]; if(!validLangs.has(base)) invalidLangs.push(lang); }); if(invalidLangs.length>0){ hlItems.push({status:'fail', msg: L('hl_invalid_lang')+': '+invalidLangs.join(', ')}); hlIssues++; } // Check self-referencing if(!hasSelfRef){ hlItems.push({status:'warn', msg: L('hl_no_self_ref')}); hlIssues++; } else { hlItems.push({status:'ok', msg: L('hl_self_ref_ok')}); } // Check x-default if(!hasXDefault){ hlItems.push({status:'warn', msg: L('hl_no_xdefault')}); hlIssues++; } else { hlItems.push({status:'ok', msg: L('hl_xdefault_ok')}); } // Check absolute URLs let relativeUrls = 0; Object.values(hlMap).forEach(href=>{ if(!href.startsWith('http://') && !href.startsWith('https://')) relativeUrls++; }); if(relativeUrls>0){ hlItems.push({status:'warn', msg: relativeUrls+' '+L('hl_relative_urls')}); hlIssues++; } hlItems.push({label:L('hl_total'), value: hreflangTags.length+' tags, '+Object.keys(hlMap).length+' '+L('hl_languages')}); if(hlIssues>0){ hlItems.push({fix:'\n\n'}); } hlItems.push({tip:L('tip_hreflang')}); const hlStatus = hlIssues===0?'ok':hlIssues<=2?'warn':'fail'; checks.push({status:hlStatus, text: L('sec_hreflang')+': '+hreflangTags.length+' tags'+(hlIssues>0?', '+hlIssues+' '+L('hl_issues'):'')}); renderSection('seo-detail-hreflang','🌐',L('sec_hreflang'),hlStatus,hlItems); } else { // No hreflang — just show a note, not a failure (not all sites need it) const hlItems = [{status:'warn', msg: L('hreflang_none')}, {tip:L('tip_hreflang')}]; checks.push({status:'warn', text: L('sec_hreflang')+': '+L('hreflang_none')}); renderSection('seo-detail-hreflang','🌐',L('sec_hreflang'),'warn',hlItems); } // --- FAVICON --- const hasFavicon = !!doc.querySelector('link[rel="icon"]') || !!doc.querySelector('link[rel="shortcut icon"]') || !!doc.querySelector('link[rel="apple-touch-icon"]'); checks.push({status:hasFavicon?'ok':'warn', text: hasFavicon?L('favicon_found'):L('favicon_missing')}); // --- CRAWLABILITY & INDEXING --- const pageSizeKB = meta.pageSizeBytes ? Math.round(meta.pageSizeBytes/1024) : Math.round(html.length/1024); maxScore += 10; let crawlScore = 0; if(meta.hasRobotsTxt) crawlScore += 3; if(meta.hasSitemapXml) crawlScore += 3; if(pageSizeKB < 500) crawlScore += 4; else if(pageSizeKB < 1500) crawlScore += 2; score += crawlScore; let crawlStatus = crawlScore >= 8 ? 'ok' : crawlScore >= 4 ? 'warn' : 'fail'; const crawlItems = [ {label:L('robots_txt'), value: meta.hasRobotsTxt ? '✓' : '✗'}, {label:L('sitemap_xml'), value: meta.hasSitemapXml ? '✓' : '✗'}, {label:L('page_size'), value: pageSizeKB + ' KB' + (pageSizeKB>500?' ⚠️':'')}, {status: meta.hasRobotsTxt?'ok':'warn', msg: meta.hasRobotsTxt?L('robots_found'):L('robots_missing')}, {status: meta.hasSitemapXml?'ok':'warn', msg: meta.hasSitemapXml?L('sitemap_found'):L('sitemap_missing')}, {status: pageSizeKB<500?'ok':(pageSizeKB<1500?'warn':'fail'), msg: pageSizeKB<500?L('page_size_ok'):L('page_size_heavy')+' ('+pageSizeKB+' KB)'}, {tip: L('tip_crawl')} ]; if(!meta.hasRobotsTxt) priorityActions.push({icon:'🤖',msg:L('robots_missing'),severity:'medium'}); if(!meta.hasSitemapXml) priorityActions.push({icon:'🗺',msg:L('sitemap_missing'),severity:'medium'}); if(!meta.hasRobotsTxt || !meta.hasSitemapXml){ let crawlFix = ''; if(!meta.hasRobotsTxt) crawlFix += '# robots.txt — upload to '+baseUrl.origin+'/robots.txt\nUser-agent: *\nAllow: /\n\nSitemap: '+baseUrl.origin+'/sitemap.xml\n'; if(!meta.hasSitemapXml) crawlFix += (crawlFix?'\n':'')+'\n\n\n \n '+url+'\n '+new Date().toISOString().split('T')[0]+'\n \n'; crawlItems.splice(-1, 0, {fix: crawlFix}); } checks.push({status:crawlStatus, text: L('sec_crawl')+': robots.txt '+(meta.hasRobotsTxt?'✓':'✗')+', sitemap '+(meta.hasSitemapXml?'✓':'✗')+', '+pageSizeKB+'KB'}); renderSection('seo-detail-crawl','🤖',L('sec_crawl'),crawlStatus,crawlItems); // --- SECURITY HEADERS --- const hdrs = meta.headers || {}; let secScore = 0, secMax = 5; maxScore += 10; const hasHSTS = !!hdrs['strict-transport-security']; const hasXFrame = !!hdrs['x-frame-options']; const hasXCT = !!hdrs['x-content-type-options']; const hasCSP = !!hdrs['content-security-policy']; const hasRefPol = !!hdrs['referrer-policy']; if(hasHSTS) secScore++; if(hasXFrame) secScore++; if(hasXCT) secScore++; if(hasCSP) secScore++; if(hasRefPol) secScore++; const secPts = Math.round((secScore/secMax)*10); score += secPts; let secStatus = secScore>=4?'ok':secScore>=2?'warn':'fail'; let secMsg = secScore>=4?L('security_good'):secScore>=2?L('security_partial'):L('security_poor'); const secFixParts = []; if(!hasHSTS) secFixParts.push('Strict-Transport-Security: max-age=31536000; includeSubDomains'); if(!hasXFrame) secFixParts.push('X-Frame-Options: SAMEORIGIN'); if(!hasXCT) secFixParts.push('X-Content-Type-Options: nosniff'); if(!hasRefPol) secFixParts.push('Referrer-Policy: strict-origin-when-cross-origin'); if(!hasCSP) secFixParts.push('Content-Security-Policy: default-src \'self\''); const secItems = [ {label:L('hsts'), value: hasHSTS?'✓':'✗'}, {label:L('x_frame'), value: hasXFrame?'✓ '+(hdrs['x-frame-options']||''):'✗'}, {label:L('x_content_type'), value: hasXCT?'✓':'✗'}, {label:L('csp'), value: hasCSP?'✓ (configured)':'✗'}, {label:L('referrer_policy'), value: hasRefPol?'✓ '+(hdrs['referrer-policy']||''):'✗'}, {label:L('sec_security'), value: secScore+'/'+secMax}, {status:secStatus, msg:secMsg}, ].concat(secFixParts.length>0?[{fix:'# Add these headers to your server config:\n'+secFixParts.join('\n')}]:[]).concat([ {tip:L('tip_security')} ]); checks.push({status:secStatus, text: L('sec_security')+': '+secScore+'/'+secMax}); renderSection('seo-detail-security','🔒',L('sec_security'),secStatus,secItems); // --- HTTP PERFORMANCE (compression, cache, protocol) --- let httpPerfScore = 0, httpPerfMax = 3; const contentEncoding = (hdrs['content-encoding']||'').toLowerCase(); const cacheControl = hdrs['cache-control']||''; const hasCompression = contentEncoding.includes('br') || contentEncoding.includes('gzip') || contentEncoding.includes('deflate'); const hasBrotli = contentEncoding.includes('br'); const hasCache = !!cacheControl && !cacheControl.includes('no-store'); const xPowered = hdrs['x-powered-by']||''; if(hasCompression) httpPerfScore++; if(hasBrotli) httpPerfScore++; else if(hasCompression) httpPerfScore += 0.5; if(hasCache) httpPerfScore++; maxScore += 6; const httpPerfPts = Math.round((httpPerfScore/httpPerfMax)*6); score += httpPerfPts; const httpPerfPct = Math.round((httpPerfScore/httpPerfMax)*100); let httpPerfStatus = httpPerfPct>=70?'ok':httpPerfPct>=40?'warn':'fail'; let httpPerfMsg = httpPerfPct>=70?L('httpperf_good'):httpPerfPct>=40?L('httpperf_fair'):L('httpperf_poor'); const httpPerfItems = [ {label:L('httpperf_compression'), value: hasCompression ? (hasBrotli?'Brotli ✓':'gzip ✓') : '✗'}, {status: hasCompression?(hasBrotli?'ok':'ok'):'fail', msg: hasCompression?(hasBrotli?L('httpperf_compression_br'):L('httpperf_compression_gzip')):L('httpperf_compression_none')}, {label:L('httpperf_cache'), value: hasCache ? '✓ '+cacheControl.substring(0,60) : '✗'}, {status: hasCache?'ok':'warn', msg: hasCache?L('httpperf_cache_ok'):L('httpperf_cache_missing')}, ]; if(xPowered) httpPerfItems.push({label:'X-Powered-By', value:xPowered+' ⚠️', raw:false}); if(xPowered) httpPerfItems.push({status:'warn', msg:'X-Powered-By header exposed — reveals server technology. Remove it for security.'}); const httpPerfFix = []; if(!hasCompression) httpPerfFix.push('# Enable gzip/brotli in your server:\n# Nginx: gzip on; gzip_types text/html text/css application/javascript;\n# Apache: AddOutputFilterByType DEFLATE text/html text/css application/javascript'); if(!hasCache) httpPerfFix.push('# Add Cache-Control headers:\n# Nginx: add_header Cache-Control "public, max-age=31536000, immutable";\n# Apache: Header set Cache-Control "public, max-age=31536000, immutable"'); if(httpPerfFix.length) httpPerfItems.push({fix:httpPerfFix.join('\n\n')}); httpPerfItems.push({status:httpPerfStatus, msg:httpPerfMsg}); httpPerfItems.push({tip:L('tip_httpperf')}); checks.push({status:httpPerfStatus, text: L('sec_httpperf')+': '+(hasCompression?(hasBrotli?'Brotli':'gzip'):'no compression')+', Cache: '+(hasCache?'✓':'✗')}); renderSection('seo-detail-httpperf','⚡',L('sec_httpperf'),httpPerfStatus,httpPerfItems); // --- ACCESSIBILITY QUICK AUDIT (v3) --- const ariaEls = doc.querySelectorAll('[aria-label],[aria-labelledby],[aria-describedby]'); const roleEls = doc.querySelectorAll('[role]'); const formInputs = doc.querySelectorAll('input:not([type="hidden"]),select,textarea'); let formNoLabel = 0; formInputs.forEach(inp=>{ const id = inp.getAttribute('id'); const hasLabel = id && doc.querySelector('label[for="'+id+'"]'); const hasAriaLabel = inp.getAttribute('aria-label') || inp.getAttribute('aria-labelledby'); const wrappedInLabel = inp.closest('label'); if(!hasLabel && !hasAriaLabel && !wrappedInLabel) formNoLabel++; }); const skipLink = doc.querySelector('a[href="#main"],a[href="#content"],a.skip-link,[class*="skip-nav"],[class*="skip-to"]'); const tabindexPos = doc.querySelectorAll('[tabindex]'); let posTabCount = 0; tabindexPos.forEach(el=>{ const v=parseInt(el.getAttribute('tabindex')); if(v>0) posTabCount++; }); // Alt text quality let altShort=0, altFilename=0; Array.from(imgs).forEach(img=>{ const alt=(img.getAttribute('alt')||'').trim(); if(alt && alt.length>0 && alt.length<5) altShort++; if(alt && /\.(jpg|jpeg|png|gif|svg|webp|bmp)$/i.test(alt)) altFilename++; }); // Score let a11yScore=0, a11yMax=7; if(hasLang) a11yScore++; if(ariaEls.length>0) a11yScore++; if(formNoLabel===0 && formInputs.length>=0) a11yScore++; if(noAlt.length===0 && imgs.length>0) a11yScore++; if(altShort===0 && altFilename===0) a11yScore++; if(posTabCount===0) a11yScore++; if(skipLink) a11yScore++; maxScore += 10; const a11yPts = Math.round((a11yScore/a11yMax)*10); score += a11yPts; let a11yStatus = a11yScore>=6?'ok':a11yScore>=3?'warn':'fail'; let a11yMsg = a11yScore>=6?L('a11y_good'):a11yScore>=3?L('a11y_fair'):L('a11y_poor'); const a11yItems = [ {label:L('a11y_lang_attr'), value: hasLang?'✓ '+hasLang:'✗'}, {label:L('a11y_aria_labels'), value: ariaEls.length+''}, {label:L('a11y_role_attrs'), value: roleEls.length+''}, {label:L('a11y_form_labels'), value: (formInputs.length-formNoLabel)+'/'+formInputs.length}, {label:L('a11y_skip_link'), value: skipLink?'✓':'✗'}, {label:L('a11y_tabindex'), value: posTabCount>0?posTabCount+' ⚠️':'0 ✓'}, ]; if(formNoLabel>0) a11yItems.push({status:'warn', msg:formNoLabel+' '+L('a11y_form_no_label')}); if(altShort>0) a11yItems.push({status:'warn', msg:altShort+' '+L('a11y_alt_short')}); if(altFilename>0) a11yItems.push({status:'warn', msg:altFilename+' '+L('a11y_alt_filename')}); a11yItems.push({status:a11yStatus, msg:a11yMsg}); a11yItems.push({tip:L('tip_a11y')}); checks.push({status:a11yStatus, text: L('sec_a11y')+': '+a11yScore+'/'+a11yMax}); renderSection('seo-detail-a11y','♿',L('sec_a11y'),a11yStatus,a11yItems); // --- REDIRECT CHAIN (v2) --- const redirChain = meta.redirectChain || []; let redirStatus = 'ok', redirItems = []; if(redirChain.length === 0){ redirItems.push({status:'ok', msg:L('redir_none')}); } else { redirChain.forEach((r,i)=>{ const is301 = r.status === 301; redirItems.push({label: (i+1)+'. '+r.status, value: r.from+' → '+r.to}); if(!is301) redirItems.push({status:'warn', msg:L('redir_302_warn')+' ('+r.status+')'}); }); if(redirChain.length >= 3){ redirStatus='fail'; priorityActions.push({icon:'🔄',msg:redirChain.length+' '+L('redir_chain')+' — '+L('redir_warn'),severity:'high'}); } else if(redirChain.some(r=>r.status!==301)){ redirStatus='warn'; } } redirItems.push({tip:L('tip_redirects')}); checks.push({status:redirStatus, text: L('sec_redirects')+': '+redirChain.length+' redirect'+(redirChain.length!==1?'s':'')}); renderSection('seo-detail-redirects','🔄',L('sec_redirects'),redirStatus,redirItems); // --- NOINDEX DETECTION (v2) --- const robotsContent = (doc.querySelector('meta[name="robots"]')?.getAttribute('content')||'').toLowerCase(); if(robotsContent.includes('noindex')){ priorityActions.unshift({icon:'🚫',msg:L('noindex_critical'),severity:'critical'}); checks.unshift({status:'fail', text: L('noindex_critical')}); score = Math.max(0, score - 30); // heavy penalty } if(robotsContent.includes('nofollow')){ priorityActions.push({icon:'🔗',msg:L('nofollow_warn'),severity:'medium'}); checks.push({status:'warn', text: L('nofollow_warn')}); } // --- CANONICAL VALIDATION (v2) --- const canonicalEl = doc.querySelector('link[rel="canonical"]'); const canonicalHref = canonicalEl ? (canonicalEl.getAttribute('href')||'').trim() : ''; if(!canonicalHref){ checks.push({status:'warn', text: L('canonical_missing')}); // Fix snippet handled in technical section } else { try { const canonUrl = new URL(canonicalHref, url).href; const pageUrl = new URL(url).href; if(canonUrl === pageUrl){ checks.push({status:'ok', text: L('canonical_self')}); } else { checks.push({status:'warn', text: L('canonical_other')+' → '+canonUrl.substring(0,60)}); priorityActions.push({icon:'🔗',msg:L('canonical_other'),severity:'high'}); } } catch(e){ checks.push({status:'warn', text: L('canonical_missing')}); } } // --- ANCHOR TEXT QUALITY (v2) --- const genericAnchors = ['click here','clicca qui','read more','leggi di più','leggi tutto','learn more','more','here','qui','link','scopri','vai','go','see more','vedi','continua','continue']; let genericCount = 0; const genericFound = new Set(); allLinksAll.forEach(a=>{ const text = (a.textContent||'').trim().toLowerCase(); if(text && genericAnchors.includes(text)){ genericCount++; genericFound.add(text); } }); if(genericCount > 0){ checks.push({status:'warn', text: genericCount+' '+L('anchor_generic')+': '+Array.from(genericFound).join(', ')}); } else if(allLinks.length > 0){ checks.push({status:'ok', text: L('anchor_ok')}); } // --- TITLE vs H1 CONSISTENCY (v2) --- if(titleText && h1s.length === 1){ const h1Text = h1s[0].textContent.trim(); const titleLower = titleText.toLowerCase().replace(/[^a-z0-9\s]/g,''); const h1Lower = h1Text.toLowerCase().replace(/[^a-z0-9\s]/g,''); if(titleLower === h1Lower){ checks.push({status:'warn', text: L('title_h1_identical')}); } else { // Check if they share significant words const titleWords = new Set(titleLower.split(/\s+/).filter(w=>w.length>3)); const h1Words = h1Lower.split(/\s+/).filter(w=>w.length>3); const overlap = h1Words.filter(w=>titleWords.has(w)).length; const similarity = h1Words.length > 0 ? overlap / h1Words.length : 0; if(similarity >= 0.3){ checks.push({status:'ok', text: L('title_h1_match')}); } else { checks.push({status:'warn', text: L('title_h1_different')}); } } } // --- TECH STACK DETECTION (v5) --- const techStackPatterns = [ // CMS {name:'WordPress', cat:'cms', test:()=>html.includes('wp-content') || html.includes('wp-includes') || !!doc.querySelector('meta[name="generator"][content*="WordPress"]')}, {name:'Shopify', cat:'cms', test:()=>html.includes('cdn.shopify.com') || html.includes('Shopify.theme')}, {name:'Wix', cat:'cms', test:()=>html.includes('static.wixstatic.com') || html.includes('wix-code')}, {name:'Squarespace', cat:'cms', test:()=>html.includes('squarespace.com') || html.includes('static1.squarespace.com')}, {name:'Webflow', cat:'cms', test:()=>html.includes('webflow.com') || !!doc.querySelector('html[data-wf-site]')}, {name:'Drupal', cat:'cms', test:()=>html.includes('drupal.js') || html.includes('/sites/default/files') || !!doc.querySelector('meta[name="generator"][content*="Drupal"]')}, {name:'Joomla', cat:'cms', test:()=>html.includes('/media/jui/') || !!doc.querySelector('meta[name="generator"][content*="Joomla"]')}, {name:'Ghost', cat:'cms', test:()=>!!doc.querySelector('meta[name="generator"][content*="Ghost"]')}, {name:'HubSpot', cat:'cms', test:()=>html.includes('js.hs-scripts.com') || html.includes('hs-banner.com')}, {name:'PrestaShop', cat:'cms', test:()=>html.includes('prestashop') || html.includes('/themes/') && html.includes('/modules/')}, // JS Frameworks {name:'React', cat:'js', test:()=>html.includes('__NEXT_DATA__') || html.includes('_reactRootContainer') || html.includes('react-root') || !!doc.querySelector('[data-reactroot]')}, {name:'Next.js', cat:'js', test:()=>html.includes('__NEXT_DATA__') || html.includes('/_next/')}, {name:'Vue.js', cat:'js', test:()=>html.includes('__vue__') || html.includes('vue.min.js') || html.includes('vue.js') || !!doc.querySelector('[data-v-]')}, {name:'Nuxt.js', cat:'js', test:()=>html.includes('__NUXT__') || html.includes('/_nuxt/')}, {name:'Angular', cat:'js', test:()=>!!doc.querySelector('[ng-version]') || html.includes('ng-app') || html.includes('angular.min.js')}, {name:'Svelte', cat:'js', test:()=>html.includes('__svelte') || !!doc.querySelector('[class*="svelte-"]')}, {name:'jQuery', cat:'js', test:()=>html.includes('jquery.min.js') || html.includes('jquery-') || html.includes('code.jquery.com')}, {name:'Bootstrap', cat:'js', test:()=>html.includes('bootstrap.min.css') || html.includes('bootstrap.min.js') || html.includes('cdn.jsdelivr.net/npm/bootstrap')}, {name:'Tailwind CSS', cat:'js', test:()=>html.includes('tailwindcss') || (doc.querySelector('[class*="flex"]') && doc.querySelector('[class*="bg-"]') && doc.querySelector('[class*="text-"]'))}, // Analytics {name:'Google Analytics 4', cat:'analytics', test:()=>html.includes('gtag') && html.includes('G-') || html.includes('googletagmanager.com/gtag')}, {name:'Google Tag Manager', cat:'analytics', test:()=>html.includes('googletagmanager.com/gtm.js') || html.includes('GTM-')}, {name:'Meta Pixel', cat:'analytics', test:()=>html.includes('connect.facebook.net') || html.includes('fbq(')}, {name:'Hotjar', cat:'analytics', test:()=>html.includes('static.hotjar.com') || html.includes('hj(')}, {name:'Clarity', cat:'analytics', test:()=>html.includes('clarity.ms') || html.includes('microsoft clarity')}, {name:'Matomo', cat:'analytics', test:()=>html.includes('matomo.js') || html.includes('piwik.js')}, {name:'Plausible', cat:'analytics', test:()=>html.includes('plausible.io')}, // CDN/Hosting {name:'Cloudflare', cat:'cdn', test:()=>(meta.headers||{}).server === 'cloudflare' || html.includes('cdnjs.cloudflare.com')}, {name:'Vercel', cat:'cdn', test:()=>(meta.headers||{})['x-vercel-id'] || html.includes('vercel.app')}, {name:'Netlify', cat:'cdn', test:()=>(meta.headers||{}).server === 'Netlify' || html.includes('netlify')}, {name:'AWS CloudFront', cat:'cdn', test:()=>!!(meta.headers||{})['x-amz-cf-id'] || html.includes('cloudfront.net')}, {name:'Fastly', cat:'cdn', test:()=>!!(meta.headers||{})['x-served-by'] && ((meta.headers||{})['x-served-by']||'').includes('cache')}, // Other {name:'Google Fonts', cat:'other', test:()=>html.includes('fonts.googleapis.com') || html.includes('fonts.gstatic.com')}, {name:'Font Awesome', cat:'other', test:()=>html.includes('font-awesome') || html.includes('fontawesome')}, {name:'reCAPTCHA', cat:'other', test:()=>html.includes('recaptcha') || html.includes('www.google.com/recaptcha')}, {name:'Stripe', cat:'other', test:()=>html.includes('js.stripe.com')}, {name:'Intercom', cat:'other', test:()=>html.includes('widget.intercom.io') || html.includes('intercomSettings')}, {name:'Crisp', cat:'other', test:()=>html.includes('client.crisp.chat')}, {name:'Tawk.to', cat:'other', test:()=>html.includes('embed.tawk.to')}, {name:'AMP', cat:'other', test:()=>!!doc.querySelector('html[amp]') || !!doc.querySelector('html[⚡]')}, {name:'PWA', cat:'other', test:()=>!!doc.querySelector('link[rel="manifest"]')}, ]; const detectedTech = []; techStackPatterns.forEach(p=>{ try{ if(p.test()) detectedTech.push({name:p.name, cat:p.cat}); }catch(e){} }); const catLabels = {cms:L('ts_cms'), js:L('ts_js'), analytics:L('ts_analytics'), cdn:L('ts_cdn'), other:L('ts_other')}; const tsItems = []; if(detectedTech.length > 0){ const byCat = {}; detectedTech.forEach(t=>{ if(!byCat[t.cat]) byCat[t.cat]=[]; byCat[t.cat].push(t.name); }); Object.keys(byCat).forEach(cat=>{ tsItems.push({label: catLabels[cat]||cat, value: byCat[cat].join(', ')}); }); tsItems.push({status:'ok', msg: detectedTech.length+' technologies detected'}); } else { tsItems.push({status:'warn', msg: L('ts_none')}); } tsItems.push({tip:L('tip_techstack')}); const tsStatus = detectedTech.length>0?'ok':'warn'; checks.push({status:tsStatus, text: L('sec_techstack')+': '+detectedTech.length+' found'}); renderSection('seo-detail-techstack','⚙️',L('sec_techstack'),tsStatus,tsItems); // --- IMAGE FORMAT CHECK (v7) --- const imgSrcs = Array.from(imgs).map(i=>(i.getAttribute('src')||'').toLowerCase()).filter(Boolean); const bgImages = html.match(/url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi)||[]; const allImgUrls = [...imgSrcs, ...bgImages.map(b=>(b.match(/url\s*\(\s*['"]?([^'")]+)/i)||[])[1]||'').filter(Boolean)]; const legacyFormats = allImgUrls.filter(u=>/\.(jpg|jpeg|png|gif|bmp|tiff?)(\?|$)/i.test(u)); const modernFormats = allImgUrls.filter(u=>/\.(webp|avif|svg)(\?|$)/i.test(u)); let imgfmtStatus, imgfmtMsg; if(allImgUrls.length===0){ imgfmtStatus='ok'; imgfmtMsg=L('img_none'); } else if(legacyFormats.length===0){ imgfmtStatus='ok'; imgfmtMsg=L('imgformat_ok'); } else { imgfmtStatus='warn'; imgfmtMsg=legacyFormats.length+' '+L('imgformat_legacy'); } const imgfmtItems = [ {label:L('imgformat_legacy').split('(')[0].trim(), value: legacyFormats.length+''}, {label:L('imgformat_modern').split('(')[0].trim(), value: modernFormats.length+''}, {status:imgfmtStatus, msg:imgfmtMsg}, ]; if(legacyFormats.length>0){ const examples = legacyFormats.slice(0,3).map(u=>{ const parts=u.split('/'); return parts[parts.length-1].substring(0,40); }); imgfmtItems.push({label:'Examples', value: examples.join(', ')}); imgfmtItems.push({fix:'\n\n \n \n Description\n'}); } imgfmtItems.push({tip:L('tip_imgformat')}); checks.push({status:imgfmtStatus, text: L('sec_imgformat')+': '+legacyFormats.length+' legacy, '+modernFormats.length+' modern'}); renderSection('seo-detail-imgformat','📸',L('sec_imgformat'),imgfmtStatus,imgfmtItems); // --- MIXED CONTENT DETECTION (v7) --- const isHttps = baseUrl.protocol === 'https:'; let mixedResources = []; if(isHttps){ const srcAttrs = doc.querySelectorAll('[src],[href],[action]'); srcAttrs.forEach(el=>{ const val = (el.getAttribute('src')||el.getAttribute('href')||el.getAttribute('action')||'').trim(); if(val.startsWith('http://') && !val.startsWith('http://localhost')){ const tag = el.tagName.toLowerCase(); if(['img','script','link','iframe','video','audio','source','embed','object','form'].includes(tag)){ mixedResources.push({tag, url: val.substring(0,80)}); } } }); } let mixedStatus = 'ok', mixedMsg = L('mixed_none'); if(mixedResources.length>0){ mixedStatus='fail'; mixedMsg=mixedResources.length+' '+L('mixed_found'); priorityActions.push({icon:'🔒',msg:mixedResources.length+' '+L('mixed_found'),severity:'high'}); } else if(!isHttps){ mixedStatus='warn'; mixedMsg=L('https_no'); } const mixedItems = [{status:mixedStatus, msg:mixedMsg}]; if(mixedResources.length>0){ mixedResources.slice(0,5).forEach(r=>{ mixedItems.push({label:'<'+r.tag+'>', value: r.url}); }); mixedItems.push({fix:'\n\n'}); } mixedItems.push({tip:L('tip_mixed')}); checks.push({status:mixedStatus, text: L('sec_mixedcontent')+': '+(mixedResources.length>0?mixedResources.length+' issues':'OK')}); renderSection('seo-detail-mixedcontent','🔒',L('sec_mixedcontent'),mixedStatus,mixedItems); // --- DUPLICATE META TAG DETECTION (v7) --- const titleTags = doc.querySelectorAll('title'); const descTags = doc.querySelectorAll('meta[name="description"]'); let dupStatus = 'ok', dupItems = []; if(titleTags.length>1){ dupStatus='warn'; dupItems.push({status:'warn',msg:titleTags.length+' '+L('dupmeta_titles')}); } if(descTags.length>1){ dupStatus='warn'; dupItems.push({status:'warn',msg:descTags.length+' '+L('dupmeta_descs')}); } if(dupStatus==='ok'){ dupItems.push({status:'ok',msg:L('dupmeta_none')}); } else { dupItems.push({fix:'\n\n Your Single Page Title\n \n'}); priorityActions.push({icon:'📋',msg:L('dupmeta_warn'),severity:'medium'}); } dupItems.push({tip:L('tip_dupmeta')}); checks.push({status:dupStatus, text: L('sec_duplicatemeta')+': '+(dupStatus==='ok'?'OK':(titleTags.length>1?titleTags.length+' titles ':'')+(descTags.length>1?descTags.length+' descs':''))}); renderSection('seo-detail-duplicatemeta','📋',L('sec_duplicatemeta'),dupStatus,dupItems); // --- RENDER-BLOCKING RESOURCES (v7) --- const headEl = doc.querySelector('head'); let blockScripts = 0, blockStyles = 0; if(headEl){ headEl.querySelectorAll('script[src]').forEach(s=>{ if(!s.getAttribute('async') && !s.getAttribute('defer') && !s.getAttribute('type')?.includes('module')){ blockScripts++; } }); headEl.querySelectorAll('link[rel="stylesheet"]').forEach(l=>{ const media = (l.getAttribute('media')||'').toLowerCase(); if(!media || media==='all' || media==='screen'){ blockStyles++; } }); } const blockTotal = blockScripts + blockStyles; let rbStatus, rbMsg; if(blockTotal===0){ rbStatus='ok'; rbMsg=L('renderblock_ok'); } else if(blockTotal<=3){ rbStatus='warn'; rbMsg=L('renderblock_warn'); } else { rbStatus='fail'; rbMsg=L('renderblock_warn'); priorityActions.push({icon:'⚡',msg:blockTotal+' render-blocking resources',severity:'medium'}); } const rbItems = [ {label:L('renderblock_scripts').split('senza')[0].split('without')[0].trim(), value: blockScripts+''}, {label:L('renderblock_styles'), value: blockStyles+''}, {status:rbStatus, msg:rbMsg}, ]; if(blockScripts>0) rbItems.push({fix:'\n\n\n'}); rbItems.push({tip:L('tip_renderblock')}); checks.push({status:rbStatus, text: L('sec_renderblock')+': '+blockScripts+' scripts, '+blockStyles+' styles'}); renderSection('seo-detail-renderblock','⚡',L('sec_renderblock'),rbStatus,rbItems); // --- CONTENT FRESHNESS (v7) --- let freshnessDate = null; // Check JSON-LD for dateModified const jsonLds = doc.querySelectorAll('script[type="application/ld+json"]'); jsonLds.forEach(s=>{ try{ const d=JSON.parse(s.textContent); if(d.dateModified) freshnessDate=d.dateModified; else if(d.datePublished && !freshnessDate) freshnessDate=d.datePublished; }catch(e){} }); // Check meta tags if(!freshnessDate){ const modMeta = doc.querySelector('meta[property="article:modified_time"]'); if(modMeta) freshnessDate=modMeta.getAttribute('content'); } if(!freshnessDate){ const pubMeta = doc.querySelector('meta[property="article:published_time"]'); if(pubMeta) freshnessDate=pubMeta.getAttribute('content'); } // Check last-modified header if(!freshnessDate && meta.headers && meta.headers['last-modified']){ freshnessDate=meta.headers['last-modified']; } let freshStatus, freshMsg; if(freshnessDate){ freshStatus='ok'; freshMsg=L('freshness_ok'); } else { freshStatus='warn'; freshMsg=L('freshness_missing'); } const freshItems = [ {label:L('freshness_date'), value: freshnessDate||'—'}, {status:freshStatus, msg:freshMsg}, ]; if(!freshnessDate) freshItems.push({fix:'\n"dateModified": "'+new Date().toISOString().split('T')[0]+'"\n\n\n'}); freshItems.push({tip:L('tip_freshness')}); checks.push({status:freshStatus, text: L('sec_freshness')+': '+(freshnessDate?freshnessDate.substring(0,10):'—')}); renderSection('seo-detail-freshness','📅',L('sec_freshness'),freshStatus,freshItems); // --- E-E-A-T SIGNALS --- let eeatScore = 0, eeatMax = 12; const eeatItems = []; // 1. Author info (meta author, rel=author, schema Person, byline patterns) const authorMeta = doc.querySelector('meta[name="author"]'); const relAuthor = doc.querySelector('a[rel="author"]'); const authorClass = doc.querySelector('[class*="author"],[itemprop="author"],[data-author]'); const hasAuthor = !!(authorMeta || relAuthor || authorClass); if(hasAuthor){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_author_found')}); } else { eeatItems.push({status:'warn',msg:L('eeat_author_missing')}); } eeatItems.push({label:L('eeat_author'), value:hasAuthor?(authorMeta?authorMeta.getAttribute('content'):'✓'):'—'}); // 2. About page link const allLinks = doc.querySelectorAll('a[href]'); let hasAbout = false; allLinks.forEach(a=>{ const h=a.getAttribute('href').toLowerCase(); if(h.includes('/about')||h.includes('/chi-siamo')||h.includes('/who-we-are')||h.includes('/ueber-uns')||h.includes('/a-propos')) hasAbout=true; }); if(hasAbout){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_about_found')}); } else { eeatItems.push({status:'warn',msg:L('eeat_about_missing')}); } // 3. Contact info let hasContact = false; allLinks.forEach(a=>{ const h=a.getAttribute('href').toLowerCase(); if(h.includes('/contact')||h.includes('/contatti')||h.includes('/kontakt')||h.startsWith('mailto:')||h.startsWith('tel:')) hasContact=true; }); if(hasContact){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_contact_found')}); } else { eeatItems.push({status:'warn',msg:L('eeat_contact_missing')}); } // 4. Privacy/Terms links let hasPrivacy = false, hasTerms = false; allLinks.forEach(a=>{ const h=a.getAttribute('href').toLowerCase(); const t=a.textContent.toLowerCase(); if(h.includes('privacy')||t.includes('privacy')) hasPrivacy=true; if(h.includes('terms')||h.includes('tos')||t.includes('terms')||t.includes('condizioni')) hasTerms=true; }); if(hasPrivacy||hasTerms){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_privacy_found')}); } else { eeatItems.push({status:'warn',msg:L('eeat_privacy_missing')}); priorityActions.push({icon:'📜',msg:L('eeat_privacy_missing'),severity:'medium'}); } // 5. Social profile links const socialDomains = ['facebook.com','twitter.com','x.com','linkedin.com','instagram.com','youtube.com','tiktok.com','github.com','pinterest.com']; let socialLinksFound = 0; allLinks.forEach(a=>{ const h=a.getAttribute('href').toLowerCase(); socialDomains.forEach(d=>{ if(h.includes(d)) socialLinksFound++; }); }); if(socialLinksFound>=2){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_social_found')+' ('+socialLinksFound+')'}); } else if(socialLinksFound===1){ eeatScore+=1; eeatItems.push({status:'warn',msg:L('eeat_social_found')+' (1)'}); } else { eeatItems.push({status:'warn',msg:L('eeat_social_missing')}); } // 6. Organization/Person schema let hasOrgSchema = false; jsonLds.forEach(s=>{ try{ const d=JSON.parse(s.textContent); const t=d['@type']||''; if(t==='Organization'||t==='Person'||t==='LocalBusiness'||(Array.isArray(t)&&(t.includes('Organization')||t.includes('Person')))) hasOrgSchema=true; if(d['@graph']) d['@graph'].forEach(g=>{ const gt=g['@type']||''; if(gt==='Organization'||gt==='Person'||gt==='LocalBusiness') hasOrgSchema=true; }); }catch(e){} }); if(hasOrgSchema){ eeatScore+=2; eeatItems.push({status:'ok',msg:L('eeat_org_found')}); } else { eeatItems.push({status:'warn',msg:L('eeat_org_missing')}); eeatItems.push({fix:''}); } // E-E-A-T overall const eeatPct = Math.round((eeatScore/eeatMax)*100); let eeatStatus; if(eeatPct>=75){ eeatStatus='ok'; eeatItems.unshift({status:'ok',msg:L('eeat_good')}); } else if(eeatPct>=40){ eeatStatus='warn'; eeatItems.unshift({status:'warn',msg:L('eeat_fair')}); } else { eeatStatus='fail'; eeatItems.unshift({status:'fail',msg:L('eeat_poor')}); priorityActions.push({icon:'👤',msg:L('eeat_poor'),severity:'high'}); } eeatItems.push({tip:L('tip_eeat')}); score += eeatScore; maxScore += eeatMax; checks.push({status:eeatStatus, text: L('sec_eeat')+': '+eeatScore+'/'+eeatMax}); renderSection('seo-detail-eeat','👤',L('sec_eeat'),eeatStatus,eeatItems); // --- GEO — AI SEARCH READINESS --- let geoScore = 0, geoMax = 12; const geoItems = []; // 1. Direct answer in first ~200 words const firstParas = doc.querySelectorAll('p'); let first200 = ''; for(let i=0;i100 && answerPatterns.test(first200words); if(hasDirectAnswer){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_direct_found')}); } else { geoItems.push({status:'warn',msg:L('geo_direct_missing')}); } // 2. FAQ/HowTo/Q&A schema let hasFaqSchema = false; jsonLds.forEach(s=>{ try{ const d=JSON.parse(s.textContent); const t=d['@type']||''; if(t==='FAQPage'||t==='HowTo'||t==='QAPage'||(Array.isArray(t)&&(t.includes('FAQPage')||t.includes('HowTo')||t.includes('QAPage')))) hasFaqSchema=true; if(d['@graph']) d['@graph'].forEach(g=>{ const gt=g['@type']||''; if(gt==='FAQPage'||gt==='HowTo'||gt==='QAPage') hasFaqSchema=true; }); }catch(e){} }); if(hasFaqSchema){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_faq_found')}); } else { geoItems.push({status:'warn',msg:L('geo_faq_missing')}); geoItems.push({fix:''}); } // 3. Question-answer patterns in content (headings with ? or question words) const headings = doc.querySelectorAll('h2,h3,h4'); let qaHeadings = 0; const questionWords = /^(what|how|why|when|where|who|which|can|does|is|are|should|will|cosa|come|perché|quando|dove|chi|quale|si può)/i; headings.forEach(h=>{ const t=h.textContent.trim(); if(t.includes('?')||questionWords.test(t)) qaHeadings++; }); if(qaHeadings>=3){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_qa_found')+' ('+qaHeadings+' Q&A headings)'}); } else if(qaHeadings>=1){ geoScore+=1; geoItems.push({status:'warn',msg:L('geo_qa_found')+' ('+qaHeadings+')'}); } else { geoItems.push({status:'warn',msg:L('geo_qa_missing')}); } // 4. Statistics and numbers const statsRegex = /\b\d+[\.,]?\d*\s*(%|percent|percento|million|miliard|billion|thousand|mila|euro|EUR|USD|\$|£|×|x faster|volte)\b/gi; const statsMatches = (bodyText.match(statsRegex)||[]).length; if(statsMatches>=3){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_stats_found')+' ('+statsMatches+' data points)'}); } else if(statsMatches>=1){ geoScore+=1; geoItems.push({status:'warn',msg:L('geo_stats_found')+' ('+statsMatches+')'}); } else { geoItems.push({status:'warn',msg:L('geo_stats_missing')}); } // 5. Structured content (lists + tables) const listsCount = doc.querySelectorAll('ul,ol').length; const tablesCount = doc.querySelectorAll('table').length; if(listsCount>=2||tablesCount>=1){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_lists_found')+' ('+listsCount+' lists, '+tablesCount+' tables)'}); } else if(listsCount>=1){ geoScore+=1; geoItems.push({status:'warn',msg:L('geo_lists_found')+' ('+listsCount+')'}); } else { geoItems.push({status:'warn',msg:L('geo_lists_missing')}); } // 6. Definitions ("X is...", "X refers to...", "X means...") const defRegex = /[A-Z][a-zA-ZÀ-ÿ\s]{2,30}\b(is|are|means|refers to|è|sono|significa|si riferisce a|defined as|viene definit[oa])\b/g; const defMatches = (bodyText.match(defRegex)||[]).length; if(defMatches>=2){ geoScore+=2; geoItems.push({status:'ok',msg:L('geo_def_found')+' ('+defMatches+')'}); } else if(defMatches>=1){ geoScore+=1; geoItems.push({status:'warn',msg:L('geo_def_found')+' ('+defMatches+')'}); } else { geoItems.push({status:'warn',msg:L('geo_def_missing')}); } // GEO overall const geoPct = Math.round((geoScore/geoMax)*100); let geoStatus; if(geoPct>=75){ geoStatus='ok'; geoItems.unshift({status:'ok',msg:L('geo_good')}); } else if(geoPct>=40){ geoStatus='warn'; geoItems.unshift({status:'warn',msg:L('geo_fair')}); } else { geoStatus='fail'; geoItems.unshift({status:'fail',msg:L('geo_poor')}); priorityActions.push({icon:'🤖',msg:L('geo_poor'),severity:'high'}); } geoItems.push({tip:L('tip_geo')}); score += geoScore; maxScore += geoMax; checks.push({status:geoStatus, text: L('sec_geo')+': '+geoScore+'/'+geoMax}); renderSection('seo-detail-geo','🤖',L('sec_geo'),geoStatus,geoItems); // --- TTFB (v7) --- if(meta.ttfbMs){ const ttfb = meta.ttfbMs; let ttfbStatus, ttfbMsg; if(ttfb<200){ ttfbStatus='ok'; ttfbMsg=L('ttfb_good')+' ('+ttfb+'ms)'; } else if(ttfb<600){ ttfbStatus='warn'; ttfbMsg=L('ttfb_ok')+' ('+ttfb+'ms)'; } else { ttfbStatus='fail'; ttfbMsg=L('ttfb_slow')+' ('+ttfb+'ms)'; priorityActions.push({icon:'⏱',msg:L('ttfb_slow')+' ('+ttfb+'ms)',severity:'high'}); } checks.push({status:ttfbStatus, text: L('ttfb_label')+': '+ttfb+'ms'}); } // --- SCORE --- const pct = Math.round((score/maxScore)*100); const circle = document.getElementById('seo-score-circle'); const label = document.getElementById('seo-score-label'); circle.querySelector('.score-num').textContent = pct; let scoreColor, scoreLabel; if(pct>=80){ scoreColor='#10b981'; scoreLabel=L('seo_score_excellent'); } else if(pct>=60){ scoreColor='#3b82f6'; scoreLabel=L('seo_score_good'); } else if(pct>=40){ scoreColor='#f59e0b'; scoreLabel=L('seo_score_fair'); } else{ scoreColor='#ef4444'; scoreLabel=L('seo_score_poor'); } // Animate SVG ring const ring = document.getElementById('seo-score-ring'); const circumference = 389.56; // 2 * PI * 62 const offset = circumference - (pct/100)*circumference; ring.style.stroke = scoreColor; requestAnimationFrame(()=>{ ring.style.strokeDashoffset = offset; }); circle.querySelector('.score-num').style.color = scoreColor; // Animate number counting up let current = 0; const step = Math.max(1, Math.floor(pct/40)); const counter = setInterval(()=>{ current = Math.min(current+step, pct); circle.querySelector('.score-num').textContent = current; if(current>=pct) clearInterval(counter); }, 25); label.textContent=scoreLabel; // --- SCORE DELTA (comparison with previous scan of same URL) --- const deltaEl = document.getElementById('seo-score-delta'); const prevHistory = getSEOHistory(); const prevScan = prevHistory.find(h => h.url === url); if(prevScan){ const delta = pct - prevScan.score; if(delta > 0){ deltaEl.innerHTML = '▲ +'+delta+' '+L('score_delta_up')+''; deltaEl.style.display = 'block'; } else if(delta < 0){ deltaEl.innerHTML = '▼ '+delta+' '+L('score_delta_down')+''; deltaEl.style.display = 'block'; } else { deltaEl.innerHTML = '▬ '+L('score_delta_same')+''; deltaEl.style.display = 'block'; } } else { deltaEl.style.display = 'none'; } // --- CATEGORY GRADES --- function pctToGrade(p){ return p>=90?'A':p>=75?'B':p>=55?'C':p>=35?'D':'F'; } function gradeColor(g){ return g==='A'?'#10b981':g==='B'?'#3b82f6':g==='C'?'#f59e0b':g==='D'?'#f97316':'#ef4444'; } const catScores = [ {cat:L('grade_title'), pct: titleStatus==='ok'?100:titleStatus==='warn'?60:0}, {cat:L('grade_meta'), pct: metaStatus==='ok'?100:metaStatus==='warn'?60:0}, {cat:L('grade_content'), pct: contentStatus==='ok'?100:contentStatus==='warn'?50:0}, {cat:L('grade_technical'), pct: Math.round((techScore/techMax)*100)}, {cat:L('grade_social'), pct: socialStatus==='ok'?100:socialStatus==='warn'?50:0}, {cat:L('grade_mobile'), pct: mobileStatus==='ok'?100:mobileStatus==='warn'?60:20}, {cat:L('grade_crawl'), pct: Math.round((crawlScore/10)*100)}, {cat:L('grade_security'), pct: Math.round((secScore/secMax)*100)}, {cat:L('grade_httpperf'), pct: httpPerfPct}, {cat:L('grade_a11y'), pct: Math.round((a11yScore/a11yMax)*100)}, {cat:L('grade_eeat'), pct: eeatPct}, {cat:L('grade_geo'), pct: geoPct}, ...(targetKw ? [{cat:L('grade_targetkw'), pct: typeof tkPct!=='undefined'?tkPct:0}] : []) ]; const gradesEl = document.getElementById('seo-grades'); gradesEl.innerHTML = catScores.map(c=>{ const g=pctToGrade(c.pct); const col=gradeColor(g); return '
'+g+''+c.cat+'
'; }).join(''); // --- RADAR CHART --- _lastCatScores = catScores.slice(); drawRadarChart(catScores); // --- EXECUTIVE SUMMARY --- renderExecutiveSummary(pct, checks, priorityActions, { titleStatus, titleText, metaStatus, metaText, h1Status, imgStatus, socialStatus, mobileStatus, secScore, secMax, a11yScore, a11yMax, contentStatus, bodyText, baseUrl, urlPath: baseUrl.pathname }); // --- SERP PREVIEW (pixel-accurate truncation) --- const serpEl = document.getElementById('seo-serp-preview'); if(titleText || metaText){ const displayUrl = baseUrl.origin + (urlPath.length>50?urlPath.substring(0,50)+'…':urlPath); // Find pixel-accurate truncation point for title let serpTitle = titleText || url; if(titleOverPx){ // Find the character where it would be truncated let truncIdx = serpTitle.length; for(let i=serpTitle.length;i>0;i--){ if(titlePixelWidth(serpTitle.substring(0,i)+'…') <= TITLE_PX_LIMIT){ truncIdx=i; break; } } serpTitle = ''+serpTitle.substring(0,truncIdx)+''+serpTitle.substring(truncIdx)+'…'; } // Find pixel-accurate truncation for meta let serpDesc = metaText || ''; if(metaOverPx){ let truncIdx = serpDesc.length; for(let i=serpDesc.length;i>0;i--){ if(metaPixelWidth(serpDesc.substring(0,i)+'…') <= META_PX_LIMIT){ truncIdx=i; break; } } serpDesc = serpDesc.substring(0,truncIdx)+''+serpDesc.substring(truncIdx)+'…'; } else { serpDesc = serpDesc.substring(0,160)+(metaText.length>160?'…':''); } serpEl.innerHTML = '
' + ''+serpTitle+'' + '
🌐'+displayUrl+'
' + '
'+serpDesc+'
' + '
'; } else { serpEl.innerHTML = '
'+L('serp_no_preview')+'
'; } // --- MOBILE SERP PREVIEW --- const serpMobileEl = document.getElementById('seo-serp-preview-mobile'); if(titleText || metaText){ // Mobile title truncation: ~78% of desktop width (~450px) const MOBILE_TITLE_PX = 450; let mobileTitle = titleText || url; const mobileTitlePx = titleText ? titlePixelWidth(titleText) : 0; if(mobileTitlePx > MOBILE_TITLE_PX){ let truncIdx = mobileTitle.length; for(let i=mobileTitle.length;i>0;i--){ if(titlePixelWidth(mobileTitle.substring(0,i)+'…') <= MOBILE_TITLE_PX){ truncIdx=i; break; } } mobileTitle = ''+mobileTitle.substring(0,truncIdx)+''+mobileTitle.substring(truncIdx)+'…'; } // Mobile description: ~120 chars max let mobileDesc = metaText || ''; mobileDesc = mobileDesc.substring(0,120)+(metaText.length>120?'…':''); // Mobile breadcrumb-style URL const pathParts = urlPath.split('/').filter(Boolean); const breadcrumb = baseUrl.hostname + (pathParts.length ? ' › ' + pathParts.join(' › ') : ''); serpMobileEl.innerHTML = '
' + '
🌐
'+baseUrl.hostname+''+breadcrumb+'
' + '
'+mobileTitle+'
' + '
'+mobileDesc+'
' + '
📱 '+L('serp_mobile_note')+'
' + '
'; } else { serpMobileEl.innerHTML = '
'+L('serp_no_preview')+'
'; } // --- SOCIAL SHARE PREVIEWS --- const ogTitleVal = ogTitle?(ogTitle.getAttribute('content')||''):(titleText||''); const ogDescVal = ogDesc?(ogDesc.getAttribute('content')||''):(metaText||''); const ogImageVal = ogImage?(ogImage.getAttribute('content')||''):''; const domain = baseUrl.hostname.toUpperCase(); // Facebook const fbEl = document.getElementById('seo-og-preview'); fbEl.innerHTML = '
'+(ogImageVal?'':L('og_no_image'))+'
' + '
'+domain+'
'+ogTitleVal.substring(0,70)+'
'+ogDescVal.substring(0,100)+'
'; // Twitter const twTitleVal = twTitle?(twTitle.getAttribute('content')||''):ogTitleVal; const twImageVal = twImage?(twImage.getAttribute('content')||''):ogImageVal; const twEl = document.getElementById('seo-tw-preview'); twEl.innerHTML = '
'+(twImageVal?'':L('tw_no_image'))+'
' + '
'+twTitleVal.substring(0,70)+'
'+baseUrl.hostname+'
'; // --- PRIORITY ACTIONS (sorted by severity: critical > high > medium, with impact estimates) --- const impactMap = { 'title_missing': 'impact_title_missing', 'meta_missing': 'impact_meta_missing', 'h1_missing': 'impact_h1_missing', 'noindex_critical': 'impact_noindex', 'robots_missing': 'impact_robots_missing', 'sitemap_missing': 'impact_sitemap_missing', 'img_no_dimensions': 'impact_images_no_dim', 'links_problematic': 'impact_links_problematic', }; // Try to match priority actions to impact keys priorityActions.forEach(a=>{ if(!a.impact){ Object.keys(impactMap).forEach(k=>{ if(a.msg && (a.msg.includes(L(k)) || a.msg===L(k))){ a.impact = L(impactMap[k]); } }); } }); const prioEl = document.getElementById('seo-priority-actions'); const prioList = document.getElementById('seo-priority-list'); if(priorityActions.length > 0){ const sevOrder = {critical:0, high:1, medium:2}; priorityActions.sort((a,b)=>(sevOrder[a.severity||'medium']||2)-(sevOrder[b.severity||'medium']||2)); prioEl.style.display='block'; prioList.innerHTML = priorityActions.slice(0,8).map(a=>{ const sev = a.severity||'medium'; const s = prioritySeverity(sev); const sevLabel = L('severity_'+sev); const impactHtml = a.impact ? '
⚡ '+a.impact+'
' : ''; return '
' + '
' + ''+a.icon+'' + ''+a.msg+'' + ''+sevLabel+'
' + impactHtml + '
'; }).join(''); } else { prioEl.style.display='none'; } // --- CHECKLIST --- const cl = document.getElementById('seo-checklist'); cl.innerHTML = checks.map(c=>{ const icon = c.status==='ok'?'✅':c.status==='warn'?'⚠️':'❌'; return '
'+icon+''+c.text+'
'; }).join(''); // Quick summary stats const okCount = checks.filter(c=>c.status==='ok').length; const warnCnt = checks.filter(c=>c.status==='warn').length; const failCnt = checks.filter(c=>c.status==='fail').length; const qsEl = document.getElementById('seo-quick-stats'); qsEl.innerHTML = '
'+okCount+' '+L('quick_passed')+'
' +'
⚠️ '+warnCnt+' '+L('quick_warnings')+'
' +'
'+failCnt+' '+L('quick_issues')+'
'; qsEl.style.display = 'flex'; document.getElementById('seo-results').style.display='block'; // Build section nav bar renderSectionNav(); document.getElementById('seo-results').scrollIntoView({behavior:'smooth',block:'start'}); // Save to history const issueCount = checks.filter(c=>c.status==='fail'||c.status==='warn').length; saveSEOScan(url, pct, issueCount); renderSEOHistory(); // --- CORE WEB VITALS (async, non-blocking) --- const cwvEl = document.getElementById('seo-detail-cwv'); cwvEl.innerHTML = '

'+L('sec_cwv')+'

...
'+L('cwv_loading')+'
'; fetchCoreWebVitals(url); } function fetchCoreWebVitals(url){ fetch('/api/pagespeed?url='+encodeURIComponent(url)) .then(r=>r.json()) .then(data=>{ if(data.error){ renderCWVError(); return; } renderCWV(data); }) .catch(()=>renderCWVError()); } function renderCWVError(){ const el = document.getElementById('seo-detail-cwv'); renderSection('seo-detail-cwv','⚡',L('sec_cwv'),'warn',[ {status:'warn', msg: L('cwv_error')}, {tip: L('tip_cwv')} ]); } function cwvMetricBar(value, goodThreshold, poorThreshold, unit, isLower){ // isLower=true means lower values are better (default for most metrics) if(isLower===undefined) isLower=true; let status, pct; if(isLower){ status = value<=goodThreshold?'ok':value<=poorThreshold?'warn':'fail'; pct = Math.min(100, (value/poorThreshold)*80); } else { status = value>=goodThreshold?'ok':value>=poorThreshold?'warn':'fail'; pct = Math.min(100, (value/goodThreshold)*80); } const colors = {ok:'#10b981', warn:'#f59e0b', fail:'#ef4444'}; const col = colors[status]; let displayVal; if(unit==='s') displayVal = (value/1000).toFixed(1)+'s'; else if(unit==='cls') displayVal = value.toFixed(3); else displayVal = Math.round(value)+'ms'; return '
' + '
' + ''+displayVal+'
'; } function cwvScoreCircle(score){ const col = score>=90?'#10b981':score>=50?'#f59e0b':'#ef4444'; return ''+score+''; } function renderCWV(data){ const items = []; // Performance score if(data.perf_score !== null && data.perf_score !== undefined){ items.push({label:L('cwv_perf_score'), value: cwvScoreCircle(data.perf_score), raw:true}); } items.push({label:'', value:'
'+L('cwv_lab')+'
', raw:true}); // LCP if(data.lcp_ms !== null && data.lcp_ms !== undefined){ items.push({label:L('cwv_lcp'), value: cwvMetricBar(data.lcp_ms, 2500, 4000, 's'), raw:true}); } // CLS if(data.cls !== null && data.cls !== undefined){ items.push({label:L('cwv_cls'), value: cwvMetricBar(data.cls, 0.1, 0.25, 'cls'), raw:true}); } // TBT if(data.tbt_ms !== null && data.tbt_ms !== undefined){ items.push({label:L('cwv_tbt'), value: cwvMetricBar(data.tbt_ms, 200, 600, 'ms'), raw:true}); } // FCP if(data.fcp_ms !== null && data.fcp_ms !== undefined){ items.push({label:L('cwv_fcp'), value: cwvMetricBar(data.fcp_ms, 1800, 3000, 's'), raw:true}); } // Speed Index if(data.si_ms !== null && data.si_ms !== undefined){ items.push({label:L('cwv_si'), value: cwvMetricBar(data.si_ms, 3400, 5800, 's'), raw:true}); } // Field data if(data.field && Object.keys(data.field).length > 0){ items.push({label:'', value:'
'+L('cwv_field')+' (p75)
', raw:true}); if(data.field.lcp_p75) items.push({label:'LCP', value: cwvMetricBar(data.field.lcp_p75, 2500, 4000, 's'), raw:true}); if(data.field.cls_p75 !== undefined) items.push({label:'CLS', value: cwvMetricBar(data.field.cls_p75/100, 0.1, 0.25, 'cls'), raw:true}); if(data.field.inp_p75) items.push({label:'INP', value: cwvMetricBar(data.field.inp_p75, 200, 500, 'ms'), raw:true}); if(data.field.fcp_p75) items.push({label:'FCP', value: cwvMetricBar(data.field.fcp_p75, 1800, 3000, 's'), raw:true}); } else { items.push({status:'warn', msg: L('cwv_no_field')}); } // Opportunities if(data.opportunities && data.opportunities.length > 0){ items.push({label:'', value:'
'+L('cwv_opportunities')+'
', raw:true}); data.opportunities.forEach(op=>{ const savingsText = op.savings_ms ? ' — '+L('cwv_savings')+': '+(op.savings_ms/1000).toFixed(1)+'s' : ''; items.push({status:'warn', msg: op.title + savingsText}); }); } // Overall status let cwvStatus = 'ok'; const perfScore = data.perf_score; if(perfScore !== null && perfScore !== undefined){ cwvStatus = perfScore>=90?'ok':perfScore>=50?'warn':'fail'; } const cwvMsg = cwvStatus==='ok'?L('cwv_good'):cwvStatus==='warn'?L('cwv_needs_work'):L('cwv_poor'); items.push({status:cwvStatus, msg: cwvMsg}); items.push({tip:L('tip_cwv')}); renderSection('seo-detail-cwv','⚡',L('sec_cwv'),cwvStatus,items); // Update grade badges — add CWV grade and redraw radar const gradesEl = document.getElementById('seo-grades'); if(gradesEl && perfScore !== null){ const g = perfScore>=90?'A':perfScore>=75?'B':perfScore>=55?'C':perfScore>=35?'D':'F'; const col = g==='A'?'#10b981':g==='B'?'#3b82f6':g==='C'?'#f59e0b':g==='D'?'#f97316':'#ef4444'; const badge = '
'+g+''+L('grade_cwv')+'
'; gradesEl.innerHTML += badge; // Redraw radar with CWV category if(_lastCatScores){ _lastCatScores.push({cat:L('grade_cwv'), pct: perfScore}); drawRadarChart(_lastCatScores); } } } /* ============ EXECUTIVE SUMMARY ============ */ function renderExecutiveSummary(pct, checks, priorityActions, ctx){ const summaryEl = document.getElementById('seo-executive-summary'); summaryEl.style.display = 'block'; // Count issues by type const criticalCount = checks.filter(c=>c.status==='fail').length; const warnCount = checks.filter(c=>c.status==='warn').length; const passedCount = checks.filter(c=>c.status==='ok').length; // Overall letter grade (A+ through F) function scoreToLetterGrade(s){ if(s>=95) return 'A+'; if(s>=90) return 'A'; if(s>=85) return 'A-'; if(s>=80) return 'B+'; if(s>=75) return 'B'; if(s>=70) return 'B-'; if(s>=65) return 'C+'; if(s>=60) return 'C'; if(s>=55) return 'C-'; if(s>=45) return 'D+'; if(s>=35) return 'D'; return 'F'; } const overallGrade = scoreToLetterGrade(pct); const gradeCol = pct>=80?'#10b981':pct>=60?'#3b82f6':pct>=40?'#f59e0b':'#ef4444'; // Overview stats const overviewEl = document.getElementById('seo-summary-overview'); overviewEl.innerHTML = '
' +'
'+overallGrade+'
' +'
'+L('summary_overall_grade')+'
' +'
' +'
'+criticalCount+'
' +'
'+L('summary_critical')+'
' +'
' +'
'+warnCount+'
' +'
'+L('summary_warnings')+'
' +'
' +'
'+passedCount+'
' +'
'+L('summary_passed')+'
'; // Quick wins — top 3 prioritized actions with smart suggestions const quickWinsEl = document.getElementById('seo-summary-quickwins'); const quickWins = buildQuickWins(priorityActions, ctx); if(quickWins.length > 0){ let qwHtml = '
'+L('summary_quickwins_title')+'
'; quickWins.slice(0,3).forEach((qw,i)=>{ const impactCol = qw.impact==='high'?'#ef4444':qw.impact==='medium'?'#f59e0b':'#3b82f6'; const impactBg = qw.impact==='high'?'rgba(239,68,68,0.1)':qw.impact==='medium'?'rgba(245,158,11,0.1)':'rgba(59,130,246,0.1)'; const impactLabel = qw.impact==='high'?L('summary_impact_high'):qw.impact==='medium'?L('summary_impact_medium'):L('summary_impact_low'); qwHtml += '
' +'
'+(i+1)+'
' +'
' +'
'+qw.title+'
' +'
'+qw.desc+'
' +(qw.suggestion ? '
'+L('summary_suggested_title')+': '+qw.suggestion+'
' : '') +''+impactLabel+'' +'
'; }); quickWinsEl.innerHTML = qwHtml; } else { quickWinsEl.innerHTML = '
✔ '+L('summary_no_issues')+'
'; } // Strengths const strengthsEl = document.getElementById('seo-summary-strengths'); const strengths = []; if(ctx.titleStatus==='ok') strengths.push(L('summary_strength_title')); if(ctx.metaStatus==='ok') strengths.push(L('summary_strength_meta')); if(ctx.h1Status==='ok') strengths.push(L('summary_strength_h1')); if(ctx.imgStatus==='ok') strengths.push(L('summary_strength_images')); if(ctx.socialStatus==='ok') strengths.push(L('summary_strength_social')); if(ctx.mobileStatus==='ok') strengths.push(L('summary_strength_mobile')); if(ctx.secScore/ctx.secMax>=0.7) strengths.push(L('summary_strength_security')); if(ctx.a11yScore/ctx.a11yMax>=0.7) strengths.push(L('summary_strength_a11y')); if(strengths.length > 0){ strengthsEl.innerHTML = '
'+L('summary_strengths_title')+'
' +'
'+strengths.map(s=>'✔ '+s+'').join('')+'
'; } else { strengthsEl.innerHTML = ''; } } function buildQuickWins(priorityActions, ctx){ const wins = []; // Smart title suggestion if(ctx.titleStatus==='fail'){ const suggestedTitle = generateTitleSuggestion(ctx); wins.push({title: L('sec_title'), desc: L('title_missing'), impact:'high', suggestion: suggestedTitle}); } else if(ctx.titleStatus==='warn' && ctx.titleText.length > 60){ const trimmed = smartTrimTitle(ctx.titleText); wins.push({title: L('sec_title'), desc: L('title_long')+' ('+ctx.titleText.length+' chars)', impact:'high', suggestion: trimmed}); } // Smart meta description suggestion if(ctx.metaStatus==='fail'){ const suggestedMeta = generateMetaSuggestion(ctx); wins.push({title: L('sec_meta'), desc: L('meta_missing'), impact:'high', suggestion: suggestedMeta}); } else if(ctx.metaStatus==='warn' && ctx.metaText.length > 160){ const trimmed = smartTrimMeta(ctx.metaText); wins.push({title: L('sec_meta'), desc: L('meta_long')+' ('+ctx.metaText.length+' chars)', impact:'medium', suggestion: trimmed}); } // H1 missing if(ctx.h1Status==='fail'){ wins.push({title: L('sec_headings'), desc: L('h1_missing'), impact:'high'}); } // Add remaining priority actions not already covered priorityActions.forEach(pa=>{ if(wins.length >= 3) return; const already = wins.some(w=>w.desc === pa.msg); if(!already){ wins.push({title: pa.msg, desc: L('impact_'+pa.severity==='critical'?'title_missing':'robots_missing')||'', impact: pa.severity==='critical'?'high':'medium'}); } }); return wins; } function smartTrimTitle(title){ // Trim to ~57 chars at a word boundary, keep the most important part if(title.length <= 60) return title; // Try to find a natural break (pipe, dash, colon) const separators = [' | ', ' — ', ' - ', ' : ']; for(const sep of separators){ const idx = title.indexOf(sep); if(idx > 15 && idx <= 57) return title.substring(0, idx + sep.length).trim() + '...'; } // Word boundary trim const trimmed = title.substring(0, 57); const lastSpace = trimmed.lastIndexOf(' '); return (lastSpace > 20 ? trimmed.substring(0, lastSpace) : trimmed) + '...'; } function smartTrimMeta(meta){ if(meta.length <= 155) return meta; const trimmed = meta.substring(0, 152); const lastPeriod = trimmed.lastIndexOf('.'); if(lastPeriod > 100) return trimmed.substring(0, lastPeriod + 1); const lastSpace = trimmed.lastIndexOf(' '); return (lastSpace > 100 ? trimmed.substring(0, lastSpace) : trimmed) + '...'; } function generateTitleSuggestion(ctx){ // Extract a title from page content: use H1 if available, or first meaningful text const bodyText = ctx.bodyText || ''; const hostname = ctx.baseUrl.hostname.replace(/^www\./, ''); const brandName = hostname.split('.')[0]; const capitalBrand = brandName.charAt(0).toUpperCase() + brandName.slice(1); // Try to extract meaningful keywords from first 200 chars of body const firstWords = bodyText.substring(0, 200).split(/\s+/).filter(w=>w.length>3).slice(0,6).join(' '); if(firstWords.length > 10){ const trimmed = firstWords.length > 45 ? firstWords.substring(0, 45).replace(/\s\S*$/, '') : firstWords; return trimmed + ' | ' + capitalBrand; } return capitalBrand + ' — ' + (ctx.urlPath || '').replace(/[\/\-_]/g, ' ').trim().substring(0, 35); } function generateMetaSuggestion(ctx){ // Extract first meaningful sentences from body text const bodyText = ctx.bodyText || ''; // Find sentences const sentences = bodyText.match(/[A-Z][^.!?]*[.!?]/g) || []; let suggestion = ''; for(const s of sentences){ const clean = s.trim(); if(clean.length < 20 || clean.length > 160) continue; if(suggestion.length + clean.length + 1 <= 155){ suggestion += (suggestion ? ' ' : '') + clean; } else break; } if(suggestion.length >= 80) return suggestion; // Fallback: first 150 chars of body const fallback = bodyText.substring(0, 150).trim(); const lastSpace = fallback.lastIndexOf(' '); return (lastSpace > 100 ? fallback.substring(0, lastSpace) : fallback) + '...'; } function printReport(){ const url = document.getElementById('seo-url').value.trim(); const score = document.getElementById('seo-score-circle').querySelector('.score-num').textContent; const scoreLabel = document.getElementById('seo-score-label').textContent; const grades = document.getElementById('seo-grades').innerHTML; const checklist = document.getElementById('seo-checklist').innerHTML; const priority = document.getElementById('seo-priority-list').innerHTML; const date = new Date().toLocaleDateString(); // Executive summary for report const summaryHtml = document.getElementById('seo-executive-summary').innerHTML; // Collect all detail sections for comprehensive report const detailIds = ['seo-detail-title','seo-detail-meta','seo-detail-headings','seo-detail-images', 'seo-detail-links','seo-detail-content','seo-detail-readability','seo-detail-keywords','seo-detail-kwmap', 'seo-detail-targetkw','seo-detail-hreflang', 'seo-detail-social','seo-detail-mobile','seo-detail-structured','seo-detail-technical', 'seo-detail-url','seo-detail-crawl','seo-detail-security','seo-detail-httpperf','seo-detail-a11y','seo-detail-redirects', 'seo-detail-cwv','seo-detail-techstack']; let detailsHtml = ''; detailIds.forEach(id=>{ const el = document.getElementById(id); if(el && el.innerHTML.trim()){ detailsHtml += '
'+el.innerHTML+'
'; } }); // Capture radar chart as image const radarCanvas = document.getElementById('seo-radar'); let radarImg = ''; if(radarCanvas && radarCanvas.style.display !== 'none'){ try{ radarImg = '
SEO Radar Chart
'; }catch(e){} } const html = 'SEO Report — '+url+'' +'

SEO Report

'+url+'
'+date+'
freeaidomain.com
' +'
'+score+'/100
'+scoreLabel+'
'+radarImg+'
'+grades+'
' +'
'+summaryHtml+'
' +(priority?'

⚠️ Priority Actions

'+priority:'') +'

Detailed Analysis

'+detailsHtml +'

Full Checklist

'+checklist +'' +''; if(arguments[0] === 'pdf'){ // Open in new tab for browser PDF print const w = window.open('', '_blank'); if(w){ w.document.write(html); w.document.close(); setTimeout(()=>{ w.print(); }, 400); } else { // Fallback if popup blocked const blob = new Blob([html], {type:'text/html'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'seo-report-'+new URL(url.startsWith('http')?url:'https://'+url).hostname.replace(/[^a-z0-9]/g,'-')+'.html'; a.click(); URL.revokeObjectURL(a.href); } } else { const blob = new Blob([html], {type:'text/html'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'seo-report-'+new URL(url.startsWith('http')?url:'https://'+url).hostname.replace(/[^a-z0-9]/g,'-')+'.html'; a.click(); URL.revokeObjectURL(a.href); } } // Track rendered sections for nav bar const _renderedSections = []; function renderSection(elId, icon, title, status, items){ const el = document.getElementById(elId); const badgeClass = status==='ok'?'seo-badge-ok':status==='warn'?'seo-badge-warn':'seo-badge-fail'; const badgeText = status==='ok'?'OK':status==='warn'?'Warning':'Issue'; const bodyId = elId+'-body'; // Auto-collapse OK sections to reduce overwhelm const autoCollapse = status==='ok'; let html = '
'+icon+'

'+title+'

'+badgeText+'
'; html += '
'; items.forEach(it=>{ if(it.fix){ const fid='fix-'+elId+'-'+Math.random().toString(36).substr(2,6); html+='
🛠️ '+L('fix_show_code')+'
'+it.fix.replace(//g,'>')+'
'; } else if(it.tip){ html+='
\uD83D\uDCA1 '+it.tip+'
'; } else if(it.status){ html+='
'+it.msg+'
'; } else{ html+='
'+it.label+'
'+it.value+'
'; } }); html += '
'; el.innerHTML=html; // Track for nav bar const cleanTitle = title.replace(/<[^>]*>/g,'').trim(); _renderedSections.push({id:elId, icon, title:cleanTitle, status}); } function toggleSection(bodyId, headEl){ const body = document.getElementById(bodyId); const arrow = headEl.querySelector('.toggle-arrow'); body.classList.toggle('collapsed'); arrow.classList.toggle('collapsed'); } function toggleAllSections(expand){ document.querySelectorAll('.seo-section-body').forEach(b=>{ if(expand) b.classList.remove('collapsed'); else b.classList.add('collapsed'); }); document.querySelectorAll('.toggle-arrow').forEach(a=>{ if(expand) a.classList.remove('collapsed'); else a.classList.add('collapsed'); }); const btn = document.getElementById('seo-toggle-all'); if(btn){ btn.dataset.expanded = expand?'1':'0'; btn.innerHTML = expand ? '▼ '+L('collapse_all') : '▶ '+L('expand_all'); } } function renderSectionNav(){ const nav = document.getElementById('seo-section-nav'); if(!nav || _renderedSections.length===0) return; // Build pills: toggle-all button + one pill per section let html = ''; // Count issues const failCount = _renderedSections.filter(s=>s.status==='fail').length; const warnCount = _renderedSections.filter(s=>s.status==='warn').length; if(failCount>0) html+=''+failCount+' ❌'; if(warnCount>0) html+=''+warnCount+' ⚠️'; // Show fail/warn sections first, then ok const sorted = [..._renderedSections].sort((a,b)=>{ const order={fail:0,warn:1,ok:2}; return (order[a.status]||2)-(order[b.status]||2); }); sorted.forEach(s=>{ const shortTitle = s.title.length>18 ? s.title.substring(0,16)+'…' : s.title; html+=''; }); nav.innerHTML = html; nav.style.display = 'flex'; } /* ============ SCAN HISTORY (localStorage) ============ */ const SEO_HISTORY_KEY = 'anima_seo_history'; const SEO_HISTORY_MAX = 10; function getSEOHistory(){ try{ return JSON.parse(localStorage.getItem(SEO_HISTORY_KEY)||'[]'); }catch(e){ return []; } } function saveSEOScan(url, score, issues){ const history = getSEOHistory(); history.unshift({url, score, issues, date: new Date().toISOString()}); if(history.length > SEO_HISTORY_MAX) history.length = SEO_HISTORY_MAX; localStorage.setItem(SEO_HISTORY_KEY, JSON.stringify(history)); } function clearSEOHistory(){ localStorage.removeItem(SEO_HISTORY_KEY); renderSEOHistory(); } function renderSEOHistory(){ const container = document.getElementById('seo-history'); const list = document.getElementById('seo-history-list'); const history = getSEOHistory(); if(history.length === 0){ container.style.display='none'; return; } container.style.display='block'; list.innerHTML = history.map((h,i)=>{ const col = h.score>=80?'#10b981':h.score>=60?'#3b82f6':h.score>=40?'#f59e0b':'#ef4444'; const domain = h.url.replace(/^https?:\/\//,'').split('/')[0]; const dateStr = new Date(h.date).toLocaleDateString(); const trend = ihistory[i+1].score?'▲':h.scorehistory[i+1].score?'#10b981':h.score' +'
'+h.score+'
' +'
'+domain+'
' +'
'+dateStr+(h.issues?' — '+h.issues+' issues':'')+'
' +(trend?'
'+trend+'
':'') +''; }).join(''); // Draw mini chart if 2+ entries const canvas = document.getElementById('seo-history-chart'); if(history.length >= 2){ canvas.style.display='block'; drawHistoryChart(canvas, history); } else { canvas.style.display='none'; } } function drawHistoryChart(canvas, history){ const ctx = canvas.getContext('2d'); const w = canvas.width, h = canvas.height; const pad = {t:15,b:25,l:35,r:15}; const pw = w-pad.l-pad.r, ph = h-pad.t-pad.b; ctx.clearRect(0,0,w,h); const data = history.slice().reverse(); // oldest first const n = data.length; // Grid ctx.strokeStyle='rgba(255,255,255,0.06)'; ctx.lineWidth=1; for(let y=0;y<=100;y+=25){ const py=pad.t+ph-(y/100)*ph; ctx.beginPath();ctx.moveTo(pad.l,py);ctx.lineTo(w-pad.r,py);ctx.stroke(); ctx.fillStyle='rgba(255,255,255,0.3)';ctx.font='10px sans-serif';ctx.textAlign='right'; ctx.fillText(y,pad.l-5,py+3); } // Line + fill const points = data.map((d,i)=>({x:pad.l+(i/(n-1))*pw, y:pad.t+ph-(d.score/100)*ph})); ctx.beginPath(); points.forEach((p,i)=>i===0?ctx.moveTo(p.x,p.y):ctx.lineTo(p.x,p.y)); ctx.strokeStyle='#6366f1';ctx.lineWidth=2.5;ctx.stroke(); // Fill under ctx.lineTo(points[n-1].x,pad.t+ph);ctx.lineTo(points[0].x,pad.t+ph);ctx.closePath(); ctx.fillStyle='rgba(99,102,241,0.1)';ctx.fill(); // Dots points.forEach((p,i)=>{ const col = data[i].score>=80?'#10b981':data[i].score>=60?'#3b82f6':data[i].score>=40?'#f59e0b':'#ef4444'; ctx.beginPath();ctx.arc(p.x,p.y,4,0,Math.PI*2);ctx.fillStyle=col;ctx.fill(); ctx.fillStyle='rgba(255,255,255,0.5)';ctx.font='9px sans-serif';ctx.textAlign='center'; ctx.fillText(data[i].score,p.x,p.y-8); }); // X labels ctx.fillStyle='rgba(255,255,255,0.3)';ctx.font='9px sans-serif';ctx.textAlign='center'; const step = Math.max(1,Math.floor(n/5)); for(let i=0;i div'); const checks = document.querySelectorAll('#seo-checklist .seo-check'); let text = L('seo_copy_title') + '\n'; text += '═'.repeat(40) + '\n\n'; // Priority actions if(priorityEls.length > 0){ text += '🔴 ' + L('seo_priority_title').replace(/&#\d+;/g,'').trim() + '\n'; priorityEls.forEach(el=>{ text += ' • ' + el.textContent.replace(/\s+/g,' ').trim() + '\n'; }); text += '\n'; } // Warnings and failures from checklist const warnings = [], failures = []; checks.forEach(ch=>{ const icon = ch.querySelector('.seo-check-icon'); const txt = ch.textContent.replace(/\s+/g,' ').trim(); if(icon && icon.textContent.includes('⚠')) warnings.push(txt); else if(icon && icon.textContent.includes('❌')) failures.push(txt); }); if(failures.length){ text += '❌ Issues\n'; failures.forEach(f=>{ text += ' • ' + f + '\n'; }); text += '\n'; } if(warnings.length){ text += '⚠️ Warnings\n'; warnings.forEach(w=>{ text += ' • ' + w + '\n'; }); text += '\n'; } text += '\n— Generated by ANIMA SEO Analyzer (freeaidomain.com)'; navigator.clipboard.writeText(text).then(()=>{ const btn = document.getElementById('seo-copy-btn'); const orig = btn.innerHTML; btn.innerHTML = '✓ ' + L('seo_copy_done'); btn.style.borderColor='#10b981';btn.style.color='#10b981'; setTimeout(()=>{ btn.innerHTML=orig; btn.style.borderColor=''; btn.style.color=''; },2000); }); } /* ============ SERP DEVICE TOGGLE ============ */ function switchSerpDevice(device){ const desktop = document.getElementById('seo-serp-preview'); const mobile = document.getElementById('seo-serp-preview-mobile'); const btnD = document.getElementById('serp-btn-desktop'); const btnM = document.getElementById('serp-btn-mobile'); if(device === 'mobile'){ desktop.style.display = 'none'; mobile.style.display = 'block'; btnD.classList.remove('serp-device-active'); btnM.classList.add('serp-device-active'); } else { desktop.style.display = 'block'; mobile.style.display = 'none'; btnD.classList.add('serp-device-active'); btnM.classList.remove('serp-device-active'); } } /* ============ COMPETITOR COMPARISON ============ */ let _primaryScores = null; // stored after primary analysis function toggleCompareInput(){ const wrap = document.getElementById('seo-compare-wrap'); const btn = document.getElementById('seo-compare-toggle'); if(wrap.style.display==='none'){ wrap.style.display='block'; btn.style.background='var(--primary)'; btn.style.color='#fff'; } else { wrap.style.display='none'; btn.style.background='rgba(99,102,241,0.08)'; btn.style.color='var(--primary,#6366f1)'; } } function quickAnalyze(html, url, meta){ /* Lightweight analysis — returns category scores without rendering */ const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const baseUrl = new URL(url); // Title const title = doc.querySelector('title'); const titleText = title ? title.textContent.trim() : ''; const titleOk = titleText.length>=30 && titleText.length<=60; // Meta const metaDesc = doc.querySelector('meta[name="description"]'); const metaText = metaDesc ? (metaDesc.getAttribute('content')||'').trim() : ''; const metaOk = metaText.length>=120 && metaText.length<=160; // H1 const h1s = doc.querySelectorAll('h1'); const h1Ok = h1s.length===1; // Images const imgs = doc.querySelectorAll('img'); const noAlt = Array.from(imgs).filter(i=>!i.getAttribute('alt')||!i.getAttribute('alt').trim()).length; const imgPct = imgs.length>0 ? Math.round(((imgs.length-noAlt)/imgs.length)*100) : 100; // Content const bodyText = (doc.body?doc.body.textContent:'').replace(/\s+/g,' ').trim(); const wordCount = bodyText.split(/\s+/).filter(w=>w.length>0).length; // Links const allLinks = doc.querySelectorAll('a[href]'); let internal=0, external=0; allLinks.forEach(a=>{ try{ const u=new URL(a.getAttribute('href')||'', url); if(u.hostname===baseUrl.hostname) internal++; else external++; }catch(e){ internal++; } }); // Social const ogTitle = doc.querySelector('meta[property="og:title"]'); const ogDesc = doc.querySelector('meta[property="og:description"]'); const ogImage = doc.querySelector('meta[property="og:image"]'); const twCard = doc.querySelector('meta[name="twitter:card"]'); let socialCount = 0; if(ogTitle) socialCount++; if(ogDesc) socialCount++; if(ogImage) socialCount++; if(twCard) socialCount++; // Mobile const viewport = doc.querySelector('meta[name="viewport"]'); const hasDeviceWidth = viewport && (viewport.getAttribute('content')||'').includes('width=device-width'); // Technical const hasCharset = !!doc.querySelector('meta[charset]') || html.toLowerCase().includes('charset=utf-8'); const hasCanonical = !!doc.querySelector('link[rel="canonical"]'); const hasLang = doc.documentElement && doc.documentElement.getAttribute('lang'); const isHttps = url.startsWith('https://'); let techScore=0; if(viewport) techScore++; if(hasCharset) techScore++; if(hasCanonical) techScore++; if(hasLang) techScore++; if(isHttps) techScore++; // Security headers const hdrs = meta.headers || {}; let secScore = 0; if(hdrs['strict-transport-security']) secScore++; if(hdrs['x-frame-options']) secScore++; if(hdrs['x-content-type-options']) secScore++; if(hdrs['content-security-policy']) secScore++; if(hdrs['referrer-policy']) secScore++; // Structured data const jsonLdScripts = doc.querySelectorAll('script[type="application/ld+json"]'); let schemaTypes = []; jsonLdScripts.forEach(s=>{ try{ const d=JSON.parse(s.textContent); if(d['@type']) schemaTypes.push(d['@type']); if(d['@graph']) d['@graph'].forEach(g=>{ if(g['@type']) schemaTypes.push(g['@type']); }); }catch(e){} }); // E-E-A-T signals let eeat = 0; if(doc.querySelector('meta[name="author"]')||doc.querySelector('[class*="author"]')) eeat++; let hasAbout=false, hasContact=false, hasPrivacy=false; doc.querySelectorAll('a[href]').forEach(a=>{ const h=(a.getAttribute('href')||'').toLowerCase(); if(h.includes('/about')||h.includes('/chi-siamo')) hasAbout=true; if(h.includes('/contact')||h.includes('/contatti')||h.startsWith('mailto:')) hasContact=true; if(h.includes('privacy')||h.includes('terms')) hasPrivacy=true; }); if(hasAbout) eeat++; if(hasContact) eeat++; if(hasPrivacy) eeat++; // Compute category percentages const cats = { title: titleOk ? 100 : (titleText ? 60 : 0), meta: metaOk ? 100 : (metaText ? 60 : 0), headings: h1Ok ? 100 : (h1s.length>0 ? 50 : 0), images: imgPct, content: wordCount>=300 ? 100 : (wordCount>=150 ? 60 : 20), links: Math.min(100, (internal+external)*5), social: Math.round((socialCount/4)*100), mobile: hasDeviceWidth ? 100 : (viewport ? 60 : 0), technical: Math.round((techScore/5)*100), security: Math.round((secScore/5)*100), schema: schemaTypes.length>0 ? 100 : 0, eeat: Math.round((eeat/4)*100), }; // Overall const vals = Object.values(cats); const overall = Math.round(vals.reduce((a,b)=>a+b,0)/vals.length); return { url, overall, cats, hostname: baseUrl.hostname, titleText: titleText.substring(0,50), wordCount, internal, external, }; } function renderComparison(primary, competitor){ const card = document.getElementById('seo-comparison-card'); const body = document.getElementById('seo-comparison-body'); card.style.display = 'block'; function scoreRing(pct, size){ const col = pct>=80?'#10b981':pct>=60?'#3b82f6':pct>=40?'#f59e0b':'#ef4444'; const r = (size/2)-6, circ = 2*Math.PI*r; const off = circ - (pct/100)*circ; return '
' +'' +'' +''+pct+'
'; } function grade(p){ return p>=90?'A':p>=75?'B':p>=55?'C':p>=35?'D':'F'; } function gradeCol(g){ return g==='A'?'#10b981':g==='B'?'#3b82f6':g==='C'?'#f59e0b':g==='D'?'#f97316':'#ef4444'; } const catNames = { title: L('grade_title'), meta: L('grade_meta'), headings: L('sec_headings'), images: L('sec_images'), content: L('grade_content'), links: L('sec_links'), social: L('grade_social'), mobile: L('grade_mobile'), technical: L('grade_technical'), security: L('sec_security'), schema: L('sec_structured'), eeat: L('sec_eeat'), }; // Score difference const diff = primary.overall - competitor.overall; let summaryMsg, summaryBg; if(diff > 5){ summaryMsg = '🏆 ' + L('cmp_you_win') + ' (+' + diff + ')'; summaryBg = 'rgba(16,185,129,0.08)'; } else if(diff < -5){ summaryMsg = '⚠️ ' + L('cmp_they_win') + ' (' + diff + ')'; summaryBg = 'rgba(239,68,68,0.08)'; } else { summaryMsg = '🤝 ' + L('cmp_tie'); summaryBg = 'rgba(59,130,246,0.08)'; } // Build HTML let html = '
'; // Left column (your site) html += '
'; html += '
' + L('cmp_your_site') + '
'; html += scoreRing(primary.overall, 100); html += '
' + primary.hostname + '
'; html += '
'; // VS html += '
' + L('cmp_vs') + '
'; // Right column (competitor) html += '
'; html += '
' + L('cmp_competitor') + '
'; html += scoreRing(competitor.overall, 100); html += '
' + competitor.hostname + '
'; html += '
'; html += '
'; // Category comparison grid html += '
'; let yourWins = 0, theirWins = 0; Object.keys(catNames).forEach(key => { const pVal = primary.cats[key] || 0; const cVal = competitor.cats[key] || 0; const pGrade = grade(pVal); const cGrade = grade(cVal); const pCol = gradeCol(pGrade); const cCol = gradeCol(cGrade); const pWins = pVal > cVal; const cWins = cVal > pVal; if(pWins) yourWins++; if(cWins) theirWins++; html += '
'; html += '
' + pGrade + '
'; html += '
' + catNames[key] + '
'; html += '
' + cGrade + '
'; html += '
'; }); html += '
'; // Summary html += '
' + summaryMsg; html += '
'; html += primary.hostname + ': ' + yourWins + ' ' + L('cmp_wins') + ' · ' + competitor.hostname + ': ' + theirWins + ' ' + L('cmp_wins'); html += '
'; body.innerHTML = html; } function runComparisonAnalysis(compareUrl){ /* Fetch and analyze competitor URL, then render comparison */ const card = document.getElementById('seo-comparison-card'); const body = document.getElementById('seo-comparison-body'); card.style.display = 'block'; body.innerHTML = '
' + L('cmp_loading') + '
'; fetch('/api/seo-proxy?url='+encodeURIComponent(compareUrl)) .then(r=>{ if(!r.ok) throw new Error(r.status); return r.json(); }) .then(data=>{ if(data.error) throw new Error(data.error); const competitor = quickAnalyze(data.html, compareUrl, { headers: data.headers||{}, hasRobotsTxt: data.has_robots_txt||false, hasSitemapXml: data.has_sitemap_xml||false, }); if(_primaryScores){ renderComparison(_primaryScores, competitor); } }) .catch(e=>{ body.innerHTML = '
' + L('cmp_error') + ' (' + e.message + ')
'; }); }