Illustration for the seo analyzer tool
Built on request by Vincenzo Rubino
2026

Free SEO Analyzer

50+ SEO checks including E-E-A-T and GEO (AI Search Readiness).
The only free tool that checks if you're ready for Google AI Overviews.

Updated 2026
Instant analysis
🔒 100% Free
🌐 Any URL
🔍
Analisi On-Page
Title, meta description, headings, keyword density
📷
Controllo Immagini
Missing alt text, heavy images, optimization
🔗
Analisi Link
Internal, external links, broken links, structure
📱
Social & Mobile
Open Graph, Twitter Card, viewport, performance
👤
E-E-A-T Signals
Author, contact, privacy, trust, Organization schema
🤖
GEO — AI Readiness
Optimized for Google AI Overviews, ChatGPT, Perplexity
1

Enter URL

Paste the page address to analyze

2

Auto analysis

Full scan in seconds

3

Detailed report

Issues, suggestions and SEO score

Analyze your site
Enter a full URL with https://
Inserisci la keyword per cui vuoi posizionarti. Otterrai un'analisi di ottimizzazione dedicata.
Prova con:

How SEO analysis works

SEO (Search Engine Optimization) analysis is the process of evaluating a web page to identify issues that prevent good ranking on search engines like Google. A good SEO analyzer examines over 50 critical factors: from page title to heading structure, from E-E-A-T signals to GEO (Generative Engine Optimization) readiness for AI search.

Why SEO analysis matters for SMBs

For small and medium businesses, appearing in the top Google positions can mean the difference between growing or staying invisible. 75% of users never go past the first page of results. A free SEO analysis lets you discover exactly what to improve: missing meta descriptions, images without alt text, broken links, accessibility issues, and much more.

What our SEO Analyzer checks

Our free tool checks in real time: title tags and meta descriptions (length and content), heading hierarchy (H1-H6), image optimization (alt text, dimensions, lazy loading), internal and external link structure, text readability (Flesch score), keyword density, Open Graph and Twitter Card tags for social media, JSON-LD structured data, technical aspects (viewport, canonical, hreflang, HTTPS), security headers, accessibility (ARIA, form labels, skip navigation), and crawlability (robots.txt, sitemap.xml). The report includes a score from 0 to 100, a Google preview, priority recommendations, and a scan history to track improvements over time.

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.

The 50+ SEO checks explained in detail

Our SEO analyzer performs an in-depth analysis across over 50 critical areas, organized into key categories:

Title Tag & Meta Description — The page title is the first element users see in Google results. It should be 50-60 characters long, contain the main keyword, and be unique for each page. The meta description (150-160 characters) should convince the user to click. Our tool checks length, keyword presence, and uniqueness of both.

Heading Hierarchy (H1-H6) — A correct heading structure helps Google understand content hierarchy. There should be exactly one H1 per page, consistent with the title tag. Subsequent headings (H2, H3...) should not skip levels. Our analyzer detects missing, duplicate, and skipped-level headings.

Image Optimization — Images without alt text are invisible to search engines and screen readers. Images without dimensions (width/height) cause CLS (Cumulative Layout Shift), a Google Core Web Vital. We also verify lazy loading for below-the-fold images and overall page weight.

Link Analysis — Internal links distribute authority across your site's pages. External links to authoritative sources improve credibility. Broken links (404) damage both user experience and rankings. We analyze quantity, anchor text quality, and problematic links.

Readability & Content — Google rewards thorough, well-structured content. We calculate the Flesch Reading Ease score, text-to-HTML ratio, presence of paragraphs, lists, bold text, and media. Pages with fewer than 300 words are considered ‘thin content’ and rarely rank well.

Technical & Security — Viewport meta tag for mobile, canonical URL to avoid duplicate content, hreflang for multilingual sites, HTTPS for security, headers like HSTS and CSP. We also check robots.txt, sitemap.xml, and overall page crawlability.

How to improve your website’s SEO ranking

Improving SEO is not a one-time action but an ongoing process. Here are the most effective steps, in order of priority:

1. Fix critical errors — Start with issues flagged in red in the report: missing title, no H1, images without alt text, broken links. These errors actively block your ranking.

2. Optimize title & meta description — Write a title that includes the main keyword within the first 60 characters. The meta description should be a ‘mini-ad’ that encourages clicks. Each page must have unique title and description.

3. Create in-depth content — Google rewards pages that fully answer the user’s question. Aim for 1000+ words for informational pages, with structured headings, lists, and short paragraphs. Use our SEO Analyzer to measure content quality.

4. Improve speed — Compress your images (try our image compressor), enable lazy loading, minimize CSS and JavaScript. Load time is a direct ranking factor.

5. Monitor and repeat — Use our Site Monitor to get automatic weekly checks. Track improvements over time with charts and alerts when something gets worse.

SEO Analyzer for e-commerce and business websites

SEO analysis is particularly critical for e-commerce and business websites. Product pages need unique descriptions (not copied from suppliers), optimized images with descriptive alt text, Product structured data with price and availability, and clean URLs with keywords. Business websites should optimize service pages, blogs, and landing pages to capture traffic at different funnel stages. Our analyzer checks all these aspects in seconds, helping you identify pages that need immediate attention.

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.

Related tools

Complete your SEO strategy with ANIMA’s other free tools: the barcode generator for your products, the QR code generator for offline-to-online marketing, the email signature generator for professional communications, and the VAT calculator for tax management.

✎ Suggest a change

Frequently Asked Questions

What is an SEO Analyzer?

It's a tool that scans a web page and identifies search engine optimization issues. It checks title, meta description, headings, images, links, and technical aspects.

Is it really free?

Yes, completely free with no limits. No registration required.

What SEO aspects are analyzed?

40+ analysis areas: title, meta description, title/H1 consistency, heading hierarchy (H1-H6 with skipped level detection), images (alt text, width/height for CLS, lazy loading), links (internal/external/problematic with anchor text quality), content & structure (paragraphs, lists, bold, tables, media), readability (Flesch score and text-to-HTML ratio), keyword density with n-gram analysis, keyword placement map, social tags (Open Graph & Twitter Card), mobile & performance, Core Web Vitals via Google PageSpeed (LCP, CLS, TBT, FCP), technology detection (CMS, frameworks, analytics, CDN), structured data (JSON-LD), technical aspects (viewport, charset, canonical, hreflang, HTTPS, favicon), noindex/nofollow detection, redirect chain tracking (301/302), URL structure, crawlability (robots.txt, sitemap.xml, page weight), security headers (HSTS, CSP, X-Frame-Options), accessibility (ARIA labels, form labels, skip link, tabindex, alt text quality), and page load time. Includes Google/social previews, scan history with trend chart, impact estimates per issue, copy recommendations, and downloadable HTML report.

How to improve the SEO score?

Follow the report suggestions: optimize title and meta description, use a single H1, add alt text to images, write thorough content (300+ words), and properly configure technical aspects. Also check the Google and social previews to ensure your page looks good in search results and social media.

Does it work with any website?

It works with most public websites. Some sites with anti-bot protections may not be analyzable.

What is CLS and why do you check image dimensions?

CLS (Cumulative Layout Shift) is a Google Core Web Vital that measures visual stability. Images without width and height attributes cause layout shifts during loading, hurting user experience and rankings. Our analyzer checks that every image has defined dimensions and uses lazy loading for below-the-fold images.

Why do you check robots.txt, sitemap.xml and security headers?

The robots.txt file guides search engine crawlers, and sitemap.xml helps them discover all pages. Without these files, indexing may be incomplete. Security headers (HSTS, CSP, X-Frame-Options) protect your site from attacks and are a positive signal for Google, which favors secure sites in search results.

What is E-E-A-T and why does it matter for SEO?

E-E-A-T stands for Experience, Expertise, Authoritativeness, Trustworthiness. It's a set of criteria Google uses to evaluate site quality. Our analyzer checks for author information, About page, contact details, privacy policy, social profiles, and Organization/Person schema. Sites with strong E-E-A-T signals rank better, especially for YMYL (Your Money Your Life) topics like health, finance, and legal content.

What is GEO (Generative Engine Optimization) and why do you check it?

GEO is optimization for AI-powered search engines like Google AI Overviews, ChatGPT, and Perplexity. In 2026, over 86% of Google searches show AI-generated results. Our analyzer checks if your content has direct answers upfront, FAQ/HowTo schema, question-answer patterns, statistical data, structured content, and explicit definitions — all factors that increase the chance of being cited in AI-generated answers.

Can I download the SEO report as PDF?

Yes, we offer two completely free export options: 'Save PDF' opens the report in a new tab with your browser's print dialog, where you can save as PDF with one click. 'Download HTML' downloads a complete HTML file with all analyses, charts, and recommendations. You can also copy recommendations as text with the 'Copy Recommendations' button. Most competitors charge for PDF export — we offer it free.

Can I compare my site with a competitor?

Yes! Click the 'Compare with a competitor' button below the main URL field and enter the competitor URL. After analysis you'll see a side-by-side comparison with scores, category grades, and clear indication of who wins each area. No other free tool offers this feature — it's usually reserved for premium plans.

Try our AI-powered tools

Generate professional texts, emails, bios and slogans in seconds. 10 free credits at sign up — no card needed.

'}); } 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 + ')
'; }); }