Free SEO Tool

Meta Tag Analyzer & SEO Validator

Analyze any webpage's meta tags, get an instant SEO score, preview social cards, and receive actionable optimization recommendations.

No signup required 100% private Instant analysis Social previews

Fetching and analyzing meta tags...

0 SEO Score
-
SEO Analysis
Complete analysis of meta tags and SEO elements.
Actions
🔍 Google Search Preview
📘 Facebook / Open Graph Preview
🐦 Twitter Card Preview
Status Tag Attribute Value
📜 Analysis History 0

Everything You Need for Meta Tag Analysis

A comprehensive SEO toolkit to audit, validate, and optimize your website's meta tags for better search visibility and social sharing.

📊

SEO Score

Get a 0-100 SEO score with letter grade based on title, description, OG tags, Twitter cards, and 15+ other factors.

🔍

Deep Tag Extraction

Extract every meta tag including OG, Twitter Cards, viewport, robots, canonical, hreflang, theme-color, and more.

👁️

Social Previews

See exactly how your page appears in Google search, Facebook shares, and Twitter cards before you publish.

Color-Coded Validation

Instant visual feedback with green (good), amber (warning), and red (error) indicators for every checked element.

💡

Smart Recommendations

Prioritized, actionable suggestions to fix issues and improve your SEO with specific guidance for each problem.

📋

Export & Copy

Copy all tags as text or JSON. Export full reports as TXT or HTML files for documentation and team sharing.

🌐

URL or HTML Input

Analyze live URLs via CORS proxy or paste raw HTML directly. Works with any web page, even behind authentication.

📜

Analysis History

Auto-saves your last 5 analyses with scores. Revisit previous results instantly without re-analyzing.

🔒

100% Private

All processing happens in your browser. No data is sent to any server except the URL being fetched for analysis.

Frequently Asked Questions

A meta tag analyzer inspects a webpage's HTML head section to extract and evaluate all meta tags, including the title, description, Open Graph tags, Twitter Cards, viewport settings, robots directives, and more. It provides an SEO score and actionable recommendations to improve search engine visibility and social media sharing.
When you enter a URL, we fetch the page's HTML through a CORS proxy service (allorigins.win), then parse and analyze the meta tags entirely in your browser. No data is stored on any server. If the proxy fails, you can always paste the HTML source code directly using the HTML/Paste tab.
The SEO score ranges from 0 to 100 and evaluates 18 different factors including title optimization, description length, viewport presence, canonical URL, Open Graph completeness, Twitter Card tags, and more. Scores of 90+ receive an A grade, 80-89 a B, 70-79 a C, 60-69 a D, and below 60 an F.
Google typically displays 50-60 characters of a title tag in search results. Titles shorter than 30 characters may not fully utilize the available space, while titles longer than 70 characters risk being truncated. Aim for 50-60 characters for the best balance of visibility and message delivery.
Meta descriptions should be between 150-160 characters. Google may truncate descriptions longer than 170 characters. Descriptions shorter than 50 characters are considered too brief and may not effectively summarize the page content for searchers.
Open Graph tags control how your page appears when shared on Facebook, LinkedIn, and other platforms. Twitter Card tags do the same for Twitter/X. Without these tags, social platforms will try to guess the title, description, and image, often resulting in poor previews that reduce click-through rates.
Yes. All meta tag analysis and scoring happens entirely in your browser using JavaScript. When analyzing a URL, the HTML is fetched through a third-party CORS proxy, but we don't store or transmit any data. Your analysis history is saved only in your browser's localStorage and never leaves your device.
Copied!
to ensure proper character encoding.', 'critical'); } // CANONICAL (10 pts) if (data.canonical) { const canonicalConsistent = !data.url || data.url === data.canonical || data.canonical.includes(data.url.replace(/^https?:\/\//, '').replace(/\/$/, '')); if (canonicalConsistent) { score += 10; addCheck('Canonical URL', 'good', `Present and consistent`, 'good'); } else { score += 7; addCheck('Canonical URL', 'warn', `Present but may differ from page URL`, 'warn'); addRec('Check Canonical URL Consistency', `Your canonical URL (${data.canonical}) differs from the page URL. This may indicate duplicate content issues.`, 'important'); } } else { addCheck('Canonical URL', 'error', 'Missing. Helps prevent duplicate content issues.', 'error'); addRec('Add Canonical URL', 'Specify a canonical URL to tell search engines which version of the page is the primary one.', 'important'); } // ROBOTS (5 pts) if (data.robots) { score += 5; const isIndex = /index/i.test(data.robots); const isFollow = /follow/i.test(data.robots); addCheck('Robots Meta', isIndex && isFollow ? 'good' : 'warn', `Present: ${data.robots}${!isIndex ? ' (noindex detected!)' : ''}${!isFollow ? ' (nofollow detected!)' : ''}`, isIndex && isFollow ? 'good' : 'warn'); if (!isIndex) addRec('Review Robots Directives', 'Your page has "noindex" set. Search engines will not index this page. If this is intentional, ignore this recommendation.', 'critical'); if (!isFollow) addRec('Review Nofollow Directive', 'Your page has "nofollow" set. Search engines will not follow links on this page.', 'important'); } else { addCheck('Robots Meta', 'info', 'Not set. Default behavior is index, follow (which is usually fine).', 'info'); } // OG TITLE (5 pts) if (data.og['og:title']) { score += 5; addCheck('OG Title', 'good', `Present: "${truncate(data.og['og:title'], 60)}"`, 'good'); } else { addCheck('OG Title', 'error', 'Missing. Needed for social sharing on Facebook/LinkedIn.', 'error'); addRec('Add OG Title', 'Add for proper social sharing previews.', 'important'); } // OG DESCRIPTION (5 pts) if (data.og['og:description']) { score += 5; addCheck('OG Description', 'good', `Present: "${truncate(data.og['og:description'], 60)}..."`, 'good'); } else { addCheck('OG Description', 'warn', 'Missing. Description enhances social sharing previews.', 'warn'); addRec('Add OG Description', 'Add for better social shares.', 'important'); } // OG IMAGE (5 pts) if (data.og['og:image']) { score += 5; addCheck('OG Image', 'good', `Present: "${truncate(data.og['og:image'], 60)}"`, 'good'); } else { addCheck('OG Image', 'error', 'Missing. Essential for engaging social shares.', 'error'); addRec('Add OG Image', 'Add with a 1200x630px image for optimal social sharing.', 'critical'); } // OG URL (3 pts) if (data.og['og:url']) { score += 3; addCheck('OG URL', 'good', `Present: ${data.og['og:url']}`, 'good'); } else { addCheck('OG URL', 'warn', 'Missing. Helps Facebook identify the page URL.', 'warn'); } // OG TYPE (2 pts) if (data.og['og:type']) { score += 2; addCheck('OG Type', 'good', `Present: ${data.og['og:type']}`, 'good'); } else { addCheck('OG Type', 'info', 'Missing. Defaults to "website". Consider specifying for better social understanding.', 'info'); } // OG SITE_NAME if (data.og['og:site_name']) { addCheck('OG Site Name', 'good', `Present: ${data.og['og:site_name']}`, 'good'); } else { addCheck('OG Site Name', 'info', 'Missing. Identifies your website name in social shares.', 'info'); } // OG LOCALE if (data.og['og:locale']) { addCheck('OG Locale', 'good', `Present: ${data.og['og:locale']}`, 'good'); } else { addCheck('OG Locale', 'info', 'Missing. Defaults to en_US.', 'info'); } // TWITTER CARD (5 pts) if (data.twitter['twitter:card']) { score += 5; addCheck('Twitter Card', 'good', `Present: ${data.twitter['twitter:card']}`, 'good'); } else { addCheck('Twitter Card', 'error', 'Missing. Required for Twitter card display.', 'error'); addRec('Add Twitter Card Type', 'Add to enable rich cards on Twitter/X.', 'important'); } // TWITTER TITLE (3 pts) if (data.twitter['twitter:title']) { score += 3; addCheck('Twitter Title', 'good', `Present`, 'good'); } else { addCheck('Twitter Title', 'warn', 'Missing. Fallbacks to or OG title.', 'warn'); } // TWITTER DESCRIPTION (3 pts) if (data.twitter['twitter:description']) { score += 3; addCheck('Twitter Description', 'good', `Present`, 'good'); } else { addCheck('Twitter Description', 'warn', 'Missing. Fallbacks to meta description or OG description.', 'warn'); } // TWITTER IMAGE (2 pts) if (data.twitter['twitter:image']) { score += 2; addCheck('Twitter Image', 'good', `Present`, 'good'); } else { addCheck('Twitter Image', 'warn', 'Missing. Fallbacks to OG image.', 'warn'); } // THEME COLOR (2 pts) if (data.themeColor) { score += 2; addCheck('Theme Color', 'good', `Present: ${data.themeColor}`, 'good'); } else { addCheck('Theme Color', 'info', 'Missing. Improves mobile browser experience.', 'info'); } // FAVICON (5 pts) if (data.favicon) { score += 5; addCheck('Favicon', 'good', `Present: ${truncate(data.favicon, 60)}`, 'good'); } else { addCheck('Favicon', 'warn', 'Missing. Favicon helps with brand recognition in browser tabs and bookmarks.', 'warn'); addRec('Add a Favicon', 'Add <link rel="icon" href="/favicon.ico"> to improve brand recognition.', 'suggestion'); } // LANG ATTRIBUTE (2 pts) if (data.lang) { score += 2; addCheck('Lang Attribute', 'good', `Present: ${data.lang}`, 'good'); } else { addCheck('Lang Attribute', 'warn', 'Missing on <html> element. Helps search engines understand language.', 'warn'); addRec('Add Lang Attribute', 'Add lang="en" (or your language code) to the <html> element for better SEO.', 'suggestion'); } // HREFLANG if (data.hreflang.length > 0) { addCheck('Hreflang Tags', 'good', `${data.hreflang.length} alternate language(s) found`, 'good'); } else { addCheck('Hreflang Tags', 'info', 'Not present. Only needed for multi-language sites.', 'info'); } // KEYWORDS (informational) if (data.keywords) { addCheck('Keywords Meta', 'info', `Present (note: Google ignores this tag, but other engines may use it)`, 'info'); } else { addCheck('Keywords Meta', 'info', 'Not present (Google ignores this tag, so it is optional)', 'info'); } // GOOGLEBOT if (data.googlebot) { addCheck('Googlebot Meta', 'info', `Present: ${data.googlebot}`, 'info'); } // APPLE TOUCH ICON if (data.appleTouchIcon) { addCheck('Apple Touch Icon', 'good', 'Present for iOS home screen bookmarking.', 'good'); } else { addCheck('Apple Touch Icon', 'info', 'Not present. Recommended for iOS PWA support.', 'info'); } // CONTENT TYPE DETECTION const contentType = data.og['og:type'] || 'website'; addCheck('Content Type', 'info', `Detected: ${contentType.charAt(0).toUpperCase() + contentType.slice(1)}`, 'info'); return { score, checks, recs, contentType }; } function truncate(str, len) { return str.length > len ? str.slice(0, len) + '...' : str; } function getGrade(score) { if (score >= 95) return { grade: 'A+', color: 'var(--green)' }; if (score >= 90) return { grade: 'A', color: 'var(--green)' }; if (score >= 80) return { grade: 'B', color: 'var(--green)' }; if (score >= 70) return { grade: 'C', color: 'var(--amber)' }; if (score >= 60) return { grade: 'D', color: 'var(--amber)' }; return { grade: 'F', color: 'var(--red)' }; } function getScoreColor(score) { if (score >= 80) return 'var(--green)'; if (score >= 60) return 'var(--amber)'; return 'var(--red)'; } /* ------------------------------------------- RENDER RESULTS ------------------------------------------- */ function renderResults(data) { currentData = data; // Extract and score const { score, checks, recs } = calculateScore(data); const grade = getGrade(score); // Count statuses const goodCount = checks.filter(c => c.status === 'good').length; const warnCount = checks.filter(c => c.status === 'warn').length; const errorCount = checks.filter(c => c.status === 'error').length; const infoCount = checks.filter(c => c.status === 'info').length; // Score ring const offset = CIRCUMFERENCE - (score / 100) * CIRCUMFERENCE; els.scoreRingFill.style.stroke = getScoreColor(score); els.scoreRingFill.style.strokeDasharray = CIRCUMFERENCE; // Reset animation els.scoreRingFill.style.strokeDashoffset = CIRCUMFERENCE; requestAnimationFrame(() => { requestAnimationFrame(() => { els.scoreRingFill.style.strokeDashoffset = offset; }); }); // Score number (animate) animateNumber(els.scoreNumber, score); // Grade els.scoreGrade.textContent = grade.grade; els.scoreGrade.style.background = grade.color; // Summary els.scoreSummaryTitle.textContent = `${score}/100 SEO Score`; if (score >= 90) { els.scoreSummaryDesc.textContent = 'Excellent! Your page has well-optimized meta tags with strong SEO fundamentals.'; } else if (score >= 80) { els.scoreSummaryDesc.textContent = 'Good job! A few optimizations could push your score even higher.'; } else if (score >= 70) { els.scoreSummaryDesc.textContent = 'Decent foundation, but several important optimizations are recommended.'; } else if (score >= 60) { els.scoreSummaryDesc.textContent = 'Some critical SEO elements need attention. Review the recommendations below.'; } else { els.scoreSummaryDesc.textContent = 'Significant SEO improvements are needed. Start with the critical recommendations.'; } // Stats els.scoreStats.innerHTML = ''; if (goodCount > 0) els.scoreStats.innerHTML += `<span class="stat-chip"><span class="stat-dot good"></span>${goodCount} Good</span>`; if (warnCount > 0) els.scoreStats.innerHTML += `<span class="stat-chip"><span class="stat-dot warn"></span>${warnCount} Warnings</span>`; if (errorCount > 0) els.scoreStats.innerHTML += `<span class="stat-chip"><span class="stat-dot error"></span>${errorCount} Errors</span>`; if (infoCount > 0) els.scoreStats.innerHTML += `<span class="stat-chip"><span class="stat-dot info"></span>${infoCount} Info</span>`; // Check list renderCheckList(checks); // Social previews renderGooglePreview(data); renderOgPreview(data); renderTwitterPreview(data); // All tags table renderTagsTable(data); // Recommendations renderRecommendations(recs); // Show results showResults(); // Save to history saveToHistory(data, score); // Show history panel renderHistory(); } function animateNumber(el, target) { let current = 0; const duration = 1200; const start = performance.now(); function update(now) { const elapsed = now - start; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); // ease out cubic current = Math.round(eased * target); el.textContent = current; if (progress < 1) requestAnimationFrame(update); } requestAnimationFrame(update); } /* ------------------------------------------- RENDER: Check List ------------------------------------------- */ function renderCheckList(checks) { const frag = document.createDocumentFragment(); let currentGroup = ''; checks.forEach(check => { // Determine group let group = 'Essential SEO'; const name = check.name.toLowerCase(); if (name.includes('og') || name.includes('open graph')) group = 'Open Graph'; else if (name.includes('twitter')) group = 'Twitter Cards'; else if (name.includes('canonical') || name.includes('hreflang') || name.includes('robots') || name.includes('googlebot') || name.includes('content type') || name.includes('keywords')) group = 'Technical SEO'; else if (name.includes('theme color') || name.includes('favicon') || name.includes('apple') || name.includes('charset') || name.includes('lang') || name.includes('viewport')) group = 'Technical Settings'; if (group !== currentGroup) { currentGroup = group; const groupEl = document.createElement('div'); groupEl.className = 'check-group-title'; groupEl.textContent = group; frag.appendChild(groupEl); } const item = document.createElement('div'); item.className = 'check-item'; const iconMap = { good: '✅', warn: '⚠️', error: '❌', info: 'ℹ️' }; item.innerHTML = ` <div class="check-icon ${check.status}">${iconMap[check.status]}</div> <div class="check-body"> <div class="check-name">${escapeHtml(check.name)}</div> <div class="check-detail">${escapeHtml(check.detail)}</div> </div> `; frag.appendChild(item); }); els.checkList.innerHTML = ''; els.checkList.appendChild(frag); } /* ------------------------------------------- RENDER: Google Preview ------------------------------------------- */ function renderGooglePreview(data) { const url = data.url || data.canonical || 'https://example.com/page'; let domain = ''; try { domain = new URL(url).hostname; } catch { domain = url; } const path = tryUrl(url).pathname || '/'; const title = data.og['og:title'] || data.title || 'Page Title'; const desc = data.og['og:description'] || data.description || 'No description available for this page.'; els.googlePreview.innerHTML = ` <div class="google-url"> <div class="google-favicon">🌐</div> <span class="google-site-name">${escapeHtml(domain)}</span> <span>›</span> <span class="google-breadcrumb">${escapeHtml(path)}</span> </div> <div class="google-title">${escapeHtml(title)}</div> <div class="google-desc">${escapeHtml(desc)}</div> `; } function tryUrl(str) { try { return new URL(str); } catch { return { hostname: str, pathname: '' }; } } /* ------------------------------------------- RENDER: OG Preview ------------------------------------------- */ function renderOgPreview(data) { const siteName = data.og['og:site_name'] || tryUrl(data.url || data.canonical || '').hostname || 'Website'; const title = data.og['og:title'] || data.title || 'Page Title'; const desc = data.og['og:description'] || data.description || 'No description available.'; const image = data.og['og:image'] || ''; let imageHtml = ''; if (image) { imageHtml = `<div class="og-preview-image" style="background-image:url('${escapeHtml(image)}')"></div>`; } else { imageHtml = ` <div class="og-preview-image"> <div class="og-preview-no-img"> <span class="og-preview-no-img-icon">🖼️</span> <span>No OG image set</span> </div> </div>`; } els.ogPreview.innerHTML = ` ${imageHtml} <div class="og-preview-body"> <div class="og-preview-site">${escapeHtml(siteName.toLowerCase())}</div> <div class="og-preview-title">${escapeHtml(title)}</div> <div class="og-preview-desc">${escapeHtml(desc)}</div> </div> `; } /* ------------------------------------------- RENDER: Twitter Preview ------------------------------------------- */ function renderTwitterPreview(data) { const cardType = data.twitter['twitter:card'] || 'summary'; const title = data.twitter['twitter:title'] || data.og['og:title'] || data.title || 'Page Title'; const desc = data.twitter['twitter:description'] || data.og['og:description'] || data.description || 'No description available.'; const image = data.twitter['twitter:image'] || data.og['og:image'] || ''; const domain = tryUrl(data.url || data.canonical || '').hostname || 'example.com'; if (cardType === 'summary_large_image') { let imgHtml = ''; if (image) { imgHtml = `<div class="twitter-preview-image" style="background-image:url('${escapeHtml(image)}')"></div>`; } else { imgHtml = `<div class="twitter-preview-image"><div class="og-preview-no-img"><span class="og-preview-no-img-icon">🖼️</span><span>No image</span></div></div>`; } els.twitterPreview.innerHTML = ` <div class="twitter-preview"> ${imgHtml} <div class="twitter-preview-body"> <div class="twitter-preview-title">${escapeHtml(title)}</div> <div class="twitter-preview-desc">${escapeHtml(desc)}</div> <div class="twitter-preview-domain">${escapeHtml(domain)}</div> </div> </div> <div style="margin-top:8px;font-size:0.75rem;color:var(--muted);">Card type: summary_large_image</div> `; } else { let imgHtml = ''; if (image) { imgHtml = `<div class="twitter-summary-image" style="background-image:url('${escapeHtml(image)}')"></div>`; } else { imgHtml = `<div class="twitter-summary-image"><span>🖼️</span></div>`; } els.twitterPreview.innerHTML = ` <div class="twitter-preview"> <div class="twitter-summary"> ${imgHtml} <div class="twitter-summary-body"> <div class="twitter-summary-title">${escapeHtml(title)}</div> <div class="twitter-summary-desc">${escapeHtml(desc)}</div> <div class="twitter-summary-domain">${escapeHtml(domain)}</div> </div> </div> </div> <div style="margin-top:8px;font-size:0.75rem;color:var(--muted);">Card type: ${escapeHtml(cardType)}</div> `; } } /* ------------------------------------------- RENDER: Tags Table ------------------------------------------- */ function renderTagsTable(data) { const frag = document.createDocumentFragment(); const importantTags = ['title', 'charset', 'viewport', 'description', 'robots', 'canonical', 'keywords', 'author', 'theme-color', 'og:title', 'og:description', 'og:image', 'og:url', 'og:type', 'og:site_name', 'og:locale', 'twitter:card', 'twitter:title', 'twitter:description', 'twitter:image', 'icon', 'googlebot']; const importantSet = new Set(importantTags); data.allTags.forEach(tag => { const tr = document.createElement('tr'); const isImportant = importantSet.has(tag.name); let statusClass = 'info'; if (isImportant && tag.content) statusClass = 'good'; if (isImportant && !tag.content) statusClass = 'error'; tr.innerHTML = ` <td><span class="tag-status ${statusClass}">${tag.content ? '✓' : '✕'}</span></td> <td class="tag-name">${escapeHtml(tag.name)}</td> <td class="tag-attr">${escapeHtml(tag.attr)}</td> <td class="tag-value">${escapeHtml(tag.content || '<em style="color:var(--muted)">Not set</em>')}</td> `; frag.appendChild(tr); }); els.tagsTbody.innerHTML = ''; els.tagsTbody.appendChild(frag); } /* ------------------------------------------- RENDER: Recommendations ------------------------------------------- */ function renderRecommendations(recs) { const frag = document.createDocumentFragment(); // Sort by priority const order = { critical: 0, important: 1, suggestion: 2 }; recs.sort((a, b) => (order[a.priority] || 2) - (order[b.priority] || 2)); const iconMap = { critical: '🔴', important: '🟡', suggestion: '🔵' }; if (recs.length === 0) { const item = document.createElement('div'); item.className = 'rec-item'; item.innerHTML = ` <div class="rec-icon suggestion">🎉</div> <div class="rec-body"> <div class="rec-title">Excellent! No recommendations.</div> <div class="rec-desc">Your meta tags are well-optimized. Keep up the great work!</div> </div> `; frag.appendChild(item); } recs.forEach(rec => { const item = document.createElement('div'); item.className = 'rec-item'; item.innerHTML = ` <div class="rec-icon ${rec.priority}">${iconMap[rec.priority]}</div> <div class="rec-body"> <div class="rec-title">${escapeHtml(rec.title)}</div> <div class="rec-desc">${escapeHtml(rec.desc)}</div> </div> <span class="rec-priority ${rec.priority}">${rec.priority}</span> `; frag.appendChild(item); }); els.recList.innerHTML = ''; els.recList.appendChild(frag); } /* ------------------------------------------- UTILITY: Copy / Export ------------------------------------------- */ function getAllTagsText() { if (!currentData) return ''; const d = currentData; let text = '=== META TAG ANALYSIS REPORT ===\n'; text += `URL: ${d.url || 'N/A'}\n`; text += `Generated: ${new Date().toISOString()}\n\n`; text += '--- BASIC META TAGS ---\n'; text += `Title: ${d.title || '(not set)'}\n`; text += `Description: ${d.description || '(not set)'}\n`; text += `Keywords: ${d.keywords || '(not set)'}\n`; text += `Robots: ${d.robots || '(not set)'}\n`; text += `Viewport: ${d.viewport || '(not set)'}\n`; text += `Charset: ${d.charset || '(not set)'}\n`; text += `Canonical: ${d.canonical || '(not set)'}\n`; text += `Author: ${d.author || '(not set)'}\n`; text += `Theme Color: ${d.themeColor || '(not set)'}\n`; text += `Lang: ${d.lang || '(not set)'}\n`; text += `Favicon: ${d.favicon || '(not set)'}\n`; text += '\n--- OPEN GRAPH TAGS ---\n'; Object.keys(d.og).forEach(k => { text += `${k}: ${d.og[k]}\n`; }); text += '\n--- TWITTER CARD TAGS ---\n'; Object.keys(d.twitter).forEach(k => { text += `${k}: ${d.twitter[k]}\n`; }); text += '\n--- HREFLANG TAGS ---\n'; if (d.hreflang.length > 0) { d.hreflang.forEach(h => { text += `${h.lang}: ${h.href}\n`; }); } else { text += '(none)\n'; } return text; } function getAllTagsJson() { if (!currentData) return ''; const d = currentData; const obj = { url: d.url || '', title: d.title || '', description: d.description || '', keywords: d.keywords || '', robots: d.robots || '', viewport: d.viewport || '', charset: d.charset || '', canonical: d.canonical || '', author: d.author || '', lang: d.lang || '', themeColor: d.themeColor || '', favicon: d.favicon || '', og: d.og, twitter: d.twitter, hreflang: d.hreflang, analyzedAt: new Date().toISOString() }; return JSON.stringify(obj, null, 2); } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function exportTxtReport() { const text = getAllTagsText(); downloadFile(text, 'meta-tag-report.txt', 'text/plain'); showToast('Report exported as TXT', '📄'); } function exportHtmlReport() { if (!currentData) return; const d = currentData; const { score } = calculateScore(d); const grade = getGrade(score); let html = `<!DOCTYPE html><html lang="en"><head> <!-- Google tag (gtag.js) --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4JDC5DCBV"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-Y4JDC5DCBV'); </script><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Meta Tag Report - ${escapeHtml(d.url || 'Analysis')}`; html += `

🔍 Meta Tag Analysis Report

`; html += `

URL: ${escapeHtml(d.url || 'N/A')}

`; html += `

Date: ${new Date().toLocaleString()}

`; html += `

Score: ${score}/100 (${grade.grade})

`; html += `

Basic Meta Tags

`; const basics = [ ['Title', d.title], ['Description', d.description], ['Keywords', d.keywords], ['Robots', d.robots], ['Viewport', d.viewport], ['Charset', d.charset], ['Canonical', d.canonical], ['Author', d.author], ['Lang', d.lang], ['Theme Color', d.themeColor], ['Favicon', d.favicon] ]; basics.forEach(([label, val]) => { html += `
${label}:${val ? escapeHtml(val) : 'Not set'}
`; }); html += `

Open Graph Tags

`; const ogKeys = Object.keys(d.og); if (ogKeys.length > 0) { ogKeys.forEach(k => { html += `
${escapeHtml(k)}:${escapeHtml(d.og[k])}
`; }); } else { html += `

No Open Graph tags found.

`; } html += `

Twitter Card Tags

`; const twKeys = Object.keys(d.twitter); if (twKeys.length > 0) { twKeys.forEach(k => { html += `
${escapeHtml(k)}:${escapeHtml(d.twitter[k])}
`; }); } else { html += `

No Twitter Card tags found.

`; } html += `

Generated by Tool Xeno Meta Tag Analyzer

`; html += ``; downloadFile(html, 'meta-tag-report.html', 'text/html'); showToast('Report exported as HTML', '🌐'); } /* ------------------------------------------- ACTION BUTTONS ------------------------------------------- */ $('#btn-copy').addEventListener('click', () => { if (!currentData) return; const text = getAllTagsText(); copyToClipboard(text); showToast('All meta tags copied!', '📋'); }); $('#btn-copy-json').addEventListener('click', () => { if (!currentData) return; const json = getAllTagsJson(); copyToClipboard(json); showToast('Copied as JSON!', '{ }'); }); $('#btn-export-txt').addEventListener('click', exportTxtReport); $('#btn-export-html').addEventListener('click', exportHtmlReport); $('#btn-reset').addEventListener('click', () => { currentData = null; els.urlInput.value = ''; els.htmlInput.value = ''; els.resultsSection.classList.remove('active'); els.errorSection.classList.remove('active'); els.loadingSection.classList.remove('active'); showToast('Analysis cleared', '🔄'); }); $('#btn-clear-html').addEventListener('click', () => { els.htmlInput.value = ''; els.htmlInput.focus(); }); function copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); } else { fallbackCopy(text); } } function fallbackCopy(text) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } /* ------------------------------------------- HISTORY ------------------------------------------- */ function getHistory() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } catch { return []; } } function saveToHistory(data, score) { let history = getHistory(); const entry = { url: data.url || 'HTML Paste', score: score, title: data.title || '', timestamp: Date.now() }; history.unshift(entry); // Remove duplicate URLs history = history.filter((h, i) => history.findIndex(x => x.url === h.url) === i); history = history.slice(0, MAX_HISTORY); localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); } function renderHistory() { const history = getHistory(); if (history.length === 0) { els.historyPanel.classList.remove('active'); return; } els.historyPanel.classList.add('active'); els.historyCount.textContent = history.length; const frag = document.createDocumentFragment(); history.forEach((entry, idx) => { const item = document.createElement('div'); item.className = 'history-item'; const scoreClass = entry.score >= 80 ? 'good' : (entry.score >= 60 ? 'warn' : 'error'); const timeStr = formatTime(entry.timestamp); item.innerHTML = `
${entry.score}
${escapeHtml(entry.url)}
${timeStr}${entry.title ? ' · ' + escapeHtml(truncate(entry.title, 40)) : ''}
`; frag.appendChild(item); }); els.historyList.innerHTML = ''; els.historyList.appendChild(frag); // Delete buttons $$('.history-delete', els.historyList).forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const idx = parseInt(btn.dataset.idx); let history = getHistory(); history.splice(idx, 1); localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); renderHistory(); showToast('History item removed', '🗑️'); }); }); // Click to re-analyze URL entries $$('.history-item', els.historyList).forEach(item => { item.addEventListener('click', (e) => { if (e.target.closest('.history-delete')) return; const url = item.querySelector('.history-url').textContent; if (url !== 'HTML Paste' && isValidUrl(url)) { els.urlInput.value = url; // Switch to URL tab $$('.input-tab').forEach(t => t.classList.remove('active')); $('.input-tab[data-tab="url"]').classList.add('active'); $$('.input-mode').forEach(m => m.classList.remove('active')); $('#mode-url').classList.add('active'); analyzeUrl(); } }); }); } function formatTime(ts) { const d = new Date(ts); const now = new Date(); const diff = now - d; if (diff < 60000) return 'Just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return d.toLocaleDateString(); } // History toggle els.historyToggle.addEventListener('click', () => { els.historyToggle.classList.toggle('open'); els.historyList.classList.toggle('active'); }); /* ------------------------------------------- MAIN ANALYSIS FLOW ------------------------------------------- */ async function analyzeUrl() { const url = els.urlInput.value.trim(); if (!url) { showError('URL Required', 'Please enter a URL to analyze.', 'Example: https://example.com'); return; } if (!isValidUrl(url)) { showError('Invalid URL', 'The URL you entered is not valid.', 'Make sure it starts with http:// or https://'); return; } showLoading(); try { const { html, url: resolvedUrl } = await fetchHtml(url); const data = extractMetaTags(html, resolvedUrl); renderResults(data); } catch (err) { showError( 'Analysis Failed', err.message || 'Unable to fetch the page.', 'Try pasting the HTML directly using the "HTML / Paste" tab, or check if the URL is accessible.' ); } } function analyzeHtml() { const html = els.htmlInput.value.trim(); if (!html) { showError('HTML Required', 'Please paste some HTML code to analyze.', 'You can get the HTML by right-clicking a page and selecting "View Page Source".'); return; } showLoading(); // Small delay for UX setTimeout(() => { try { const data = extractMetaTags(html, ''); renderResults(data); } catch (err) { showError('Parse Error', err.message, 'Please check that the HTML is well-formed and try again.'); } }, 300); } /* ------------------------------------------- EVENT LISTENERS ------------------------------------------- */ els.btnAnalyzeUrl.addEventListener('click', analyzeUrl); els.btnAnalyzeHtml.addEventListener('click', analyzeHtml); // Enter key on URL input els.urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); analyzeUrl(); } }); // Escape HTML function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } /* ------------------------------------------- INIT ------------------------------------------- */ initTheme(); renderHistory(); // Auto-detect paste and switch tab els.urlInput.addEventListener('paste', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const val = els.urlInput.value.trim(); if (val.includes(' t.classList.remove('active')); $('.input-tab[data-tab="html"]').classList.add('active'); $$('.input-mode').forEach(m => m.classList.remove('active')); $('#mode-html').classList.add('active'); showToast('Detected HTML – switched to Paste mode', '📋'); } }, 300); }); })();