Comprehensive SEO analysis tool with floating window and persistent filters
// ==UserScript==
// @name SEO Analyzer Pro
// @namespace http://tampermonkey.net/
// @version 1.7.0
// @description Comprehensive SEO analysis tool with floating window and persistent filters
// @author jotsaru0
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
/*
* ============================================================================
* SEO ANALYZER PRO v1.7.0
* ============================================================================
*
* FEATURES:
* --------
* ✓ Meta Tag Analysis (Title, Description, OG Tags, Twitter Cards)
* ✓ Header Hierarchy Analysis (H1-H6)
* ✓ Image Analysis (Alt tags, Types, Statistics)
* ✓ Link Analysis (Internal, External, NoFollow, Anchors)
* ✓ Schema Markup Detection (JSON-LD, Microdata, RDFa)
* ✓ Interactive Filtering (Max 3 selections per tab)
* ✓ Persistent Filter Memory (NEW - Saves across sessions)
* ✓ Clickable Links (Open in new tab)
* ✓ SERP Preview
* ✓ Word Count
* ✓ Keyboard Shortcut (Ctrl+M)
*
* USAGE:
* ------
* - Press Ctrl+M to toggle the SEO Analyzer window
* - Click on stat cards/table rows to filter content (max 3 selections)
* - All links are clickable and open in new tabs
* - Filter selections are automatically saved and restored
*
* CHANGELOG:
* ----------
* v1.7.0 (2025-01-15)
* - NEW: Persistent filter memory - selections saved across browser sessions
* - NEW: Filters automatically restore when reopening the tool
* - Enhanced: Improved stability and error handling for storage
* - Fixed: Filter restoration now works correctly on first load
*
* v1.6.0 (2025-01-14)
* - Added Tampermonkey memory preferences (GM_setValue/GM_getValue)
* - Filter selections persist across sessions
* - Improved filtering logic for images and links
* - Added clickable links with hover effects
*
* v1.5.0 (2025-01-13)
* - Added interactive filtering for all tabs
* - Maximum 3 selections per tab
* - Selection counter with visual feedback
* - Schema tab with expandable code blocks
*
* ============================================================================
*/
(function() {
'use strict';
// Toggle with Ctrl+M
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
toggleWindow();
}
});
let isVisible = false;
let floatingWindow = null;
let selectedFilters = {
headers: [],
images: [],
links: []
};
// Load saved preferences
function loadPreferences() {
try {
const saved = GM_getValue('seoAnalyzerFilters', null);
if (saved) {
selectedFilters = JSON.parse(saved);
console.log('SEO Analyzer: Loaded saved filters', selectedFilters);
}
} catch (e) {
console.error('SEO Analyzer: Failed to load preferences', e);
}
}
// Save preferences
function savePreferences() {
try {
GM_setValue('seoAnalyzerFilters', JSON.stringify(selectedFilters));
console.log('SEO Analyzer: Saved filters', selectedFilters);
} catch (e) {
console.error('SEO Analyzer: Failed to save preferences', e);
}
}
function toggleWindow() {
if (!floatingWindow) {
createFloatingWindow();
}
isVisible = !isVisible;
floatingWindow.style.display = isVisible ? 'flex' : 'none';
if (isVisible) {
loadPreferences();
analyzePageSEO();
}
}
function createFloatingWindow() {
floatingWindow = document.createElement('div');
floatingWindow.id = 'seo-analyzer-window';
floatingWindow.innerHTML = `
<div class="seo-header">
<h2>SEO Analyzer Pro v1.7</h2>
<button class="close-btn" id="close-seo">×</button>
</div>
<div class="seo-tabs">
<button class="tab-btn active" data-tab="meta">Meta</button>
<button class="tab-btn" data-tab="headers">Headers</button>
<button class="tab-btn" data-tab="images">Images</button>
<button class="tab-btn" data-tab="links">Links</button>
<button class="tab-btn" data-tab="schema">Schema</button>
</div>
<div class="seo-content">
<div class="tab-content active" id="tab-meta"></div>
<div class="tab-content" id="tab-headers"></div>
<div class="tab-content" id="tab-images"></div>
<div class="tab-content" id="tab-links"></div>
<div class="tab-content" id="tab-schema"></div>
</div>
`;
document.body.appendChild(floatingWindow);
addStyles();
attachEventListeners();
}
function addStyles() {
const style = document.createElement('style');
style.textContent = `
#seo-analyzer-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800px;
max-height: 80vh;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
z-index: 999999;
display: none;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
.seo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.seo-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #111827;
}
.close-btn {
background: none;
border: none;
font-size: 28px;
color: #6b7280;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
border-radius: 6px;
transition: all 0.2s;
}
.close-btn:hover {
background: #f3f4f6;
color: #111827;
}
.seo-tabs {
display: flex;
padding: 0 24px;
border-bottom: 1px solid #e5e7eb;
gap: 8px;
}
.tab-btn {
background: none;
border: none;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab-btn:hover {
color: #111827;
}
.tab-btn.active {
color: #2563eb;
border-bottom-color: #2563eb;
}
.seo-content {
overflow-y: auto;
padding: 24px;
max-height: calc(80vh - 140px);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.seo-section {
margin-bottom: 24px;
}
.seo-section h3 {
font-size: 14px;
font-weight: 600;
color: #111827;
margin: 0 0 12px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.seo-field {
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 6px;
border-left: 3px solid #e5e7eb;
}
.seo-field.warning {
border-left-color: #f59e0b;
background: #fffbeb;
}
.seo-field.error {
border-left-color: #ef4444;
background: #fef2f2;
}
.seo-field.success {
border-left-color: #10b981;
background: #f0fdf4;
}
.seo-label {
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.seo-value {
font-size: 14px;
color: #111827;
word-wrap: break-word;
}
.seo-value code {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
font-family: 'Courier New', monospace;
}
.serp-preview {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
margin-top: 8px;
}
.serp-url {
color: #1a0dab;
font-size: 14px;
margin-bottom: 4px;
}
.serp-title {
color: #1a0dab;
font-size: 18px;
font-weight: 400;
margin-bottom: 4px;
line-height: 1.3;
}
.serp-description {
color: #545454;
font-size: 13px;
line-height: 1.4;
}
table {
width: 100%;
border-collapse: collapse;
background: #ffffff;
border-radius: 6px;
overflow: hidden;
margin-top: 8px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
font-size: 13px;
}
th {
background: #f9fafb;
font-weight: 600;
color: #111827;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.5px;
}
td {
color: #374151;
}
tr:last-child td {
border-bottom: none;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 8px;
}
.stat-card {
background: #f9fafb;
padding: 16px;
border-radius: 6px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.stat-card:hover {
background: #f3f4f6;
transform: translateY(-2px);
}
.stat-card.selected {
background: #eff6ff;
border-color: #2563eb;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15);
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #111827;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #6b7280;
font-weight: 500;
}
.link-list {
max-height: 300px;
overflow-y: auto;
margin-top: 8px;
}
.link-item {
padding: 8px 12px;
background: #f9fafb;
border-radius: 4px;
margin-bottom: 6px;
font-size: 13px;
word-break: break-all;
transition: all 0.3s;
}
.link-item.hidden {
display: none;
}
.link-item.broken {
background: #fef2f2;
color: #ef4444;
}
.hierarchy-item {
margin-bottom: 8px;
padding: 8px 12px;
background: #f9fafb;
border-radius: 4px;
}
.hierarchy-level {
display: inline-block;
width: 40px;
font-weight: 600;
color: #2563eb;
}
.schema-block {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 16px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s;
}
.schema-block:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
.schema-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.schema-type {
font-weight: 600;
color: #2563eb;
font-size: 14px;
padding: 4px 8px;
background: #eff6ff;
border-radius: 4px;
display: inline-block;
}
.schema-toggle {
color: #6b7280;
font-size: 20px;
transition: transform 0.2s;
}
.schema-toggle.expanded {
transform: rotate(180deg);
}
.schema-code {
background: #1f2937;
color: #f3f4f6;
padding: 12px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 12px;
display: none;
}
.schema-code.visible {
display: block;
}
.schema-list {
background: #f9fafb;
border-radius: 6px;
padding: 16px;
}
.schema-list-item {
padding: 8px 12px;
background: #ffffff;
border-left: 3px solid #2563eb;
border-radius: 4px;
margin-bottom: 8px;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.schema-list-type {
font-weight: 600;
color: #111827;
}
.schema-list-format {
font-size: 12px;
color: #6b7280;
background: #f3f4f6;
padding: 2px 8px;
border-radius: 3px;
}
.no-schema {
text-align: center;
padding: 40px;
color: #6b7280;
}
.selection-counter {
background: #2563eb;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
display: inline-block;
margin-bottom: 16px;
}
.selection-counter.hidden {
display: none;
}
.filterable-section {
transition: all 0.3s;
}
.filterable-section.hidden {
display: none;
}
.header-row {
cursor: pointer;
transition: all 0.2s;
}
.header-row:hover {
background: #f3f4f6;
}
.header-row.selected {
background: #eff6ff;
font-weight: 600;
}
.clickable-link {
color: #374151;
text-decoration: none;
transition: all 0.2s;
}
.clickable-link:hover {
text-decoration: underline;
font-weight: 700;
color: #2563eb;
}
`;
document.head.appendChild(style);
}
function attachEventListeners() {
document.getElementById('close-seo').addEventListener('click', toggleWindow);
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const tab = e.target.dataset.tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
e.target.classList.add('active');
document.getElementById(`tab-${tab}`).classList.add('active');
});
});
}
function getMetaTag(name) {
const meta = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
return meta ? meta.getAttribute('content') : 'Not found';
}
function analyzePageSEO() {
analyzeMetaTab();
analyzeHeadersTab();
analyzeImagesTab();
analyzeLinksTab();
analyzeSchemaTab();
}
function analyzeMetaTab() {
const title = document.title || 'No title';
const description = getMetaTag('description');
const canonical = document.querySelector('link[rel="canonical"]')?.href || 'Not found';
const robots = getMetaTag('robots');
const viewport = getMetaTag('viewport');
const wordCount = document.body.innerText.split(/\s+/).length;
const ogTitle = getMetaTag('og:title');
const ogDescription = getMetaTag('og:description');
const ogImage = getMetaTag('og:image');
const twitterCard = getMetaTag('twitter:card');
const twitterTitle = getMetaTag('twitter:title');
const twitterDescription = getMetaTag('twitter:description');
const twitterImage = getMetaTag('twitter:image');
const twitterSite = getMetaTag('twitter:site');
const twitterCreator = getMetaTag('twitter:creator');
const twitterUrl = getMetaTag('twitter:url');
const twitterDomain = getMetaTag('twitter:domain');
const url = window.location.href;
const displayUrl = new URL(url).hostname + new URL(url).pathname;
document.getElementById('tab-meta').innerHTML = `
<div class="seo-section">
<div class="seo-field ${title.length > 60 ? 'warning' : 'success'}">
<div class="seo-label">Meta Title (${title.length} chars)</div>
<div class="seo-value">${title}</div>
</div>
<div class="seo-field ${description === 'Not found' ? 'error' : description.length > 160 ? 'warning' : 'success'}">
<div class="seo-label">Meta Description (${description.length} chars)</div>
<div class="seo-value">${description}</div>
</div>
<div class="seo-field">
<div class="seo-label">SERP Preview</div>
<div class="serp-preview">
<div class="serp-url">${displayUrl}</div>
<div class="serp-title">${title.substring(0, 60)}${title.length > 60 ? '...' : ''}</div>
<div class="serp-description">${description.substring(0, 160)}${description.length > 160 ? '...' : ''}</div>
</div>
</div>
<div class="seo-field ${canonical === 'Not found' ? 'warning' : 'success'}">
<div class="seo-label">Canonical URL</div>
<div class="seo-value">${canonical}</div>
</div>
<div class="seo-field">
<div class="seo-label">Robots</div>
<div class="seo-value">${robots}</div>
</div>
<div class="seo-field">
<div class="seo-label">Viewport</div>
<div class="seo-value">${viewport}</div>
</div>
<div class="seo-field">
<div class="seo-label">Word Count</div>
<div class="seo-value">${wordCount} words</div>
</div>
</div>
<div class="seo-section">
<h3>Open Graph Tags</h3>
<div class="seo-field">
<div class="seo-label">OG:Title</div>
<div class="seo-value">${ogTitle}</div>
</div>
<div class="seo-field">
<div class="seo-label">OG:Description</div>
<div class="seo-value">${ogDescription}</div>
</div>
<div class="seo-field">
<div class="seo-label">OG:Image</div>
<div class="seo-value">${ogImage}</div>
</div>
</div>
<div class="seo-section">
<h3>Twitter Card Tags</h3>
<div class="seo-field">
<div class="seo-label">Twitter:Card</div>
<div class="seo-value">${twitterCard}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Title</div>
<div class="seo-value">${twitterTitle}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Description</div>
<div class="seo-value">${twitterDescription}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Image</div>
<div class="seo-value">${twitterImage}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Site</div>
<div class="seo-value">${twitterSite}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Creator</div>
<div class="seo-value">${twitterCreator}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Url</div>
<div class="seo-value">${twitterUrl}</div>
</div>
<div class="seo-field">
<div class="seo-label">Twitter:Domain</div>
<div class="seo-value">${twitterDomain}</div>
</div>
</div>
`;
}
function analyzeHeadersTab() {
const headers = [];
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => {
document.querySelectorAll(tag).forEach(el => {
headers.push({
level: tag.toUpperCase(),
text: el.innerText.trim()
});
});
});
const headerCounts = {
H1: document.querySelectorAll('h1').length,
H2: document.querySelectorAll('h2').length,
H3: document.querySelectorAll('h3').length,
H4: document.querySelectorAll('h4').length,
H5: document.querySelectorAll('h5').length,
H6: document.querySelectorAll('h6').length
};
const tableRows = Object.entries(headerCounts)
.map(([level, count]) => `<tr class="header-row" data-header-level="${level}"><td>${level}</td><td>${count}</td></tr>`)
.join('');
const hierarchyHTML = headers
.map(h => `<div class="hierarchy-item filterable-section" data-header-type="${h.level}"><span class="hierarchy-level">${h.level}</span>${h.text}</div>`)
.join('');
document.getElementById('tab-headers').innerHTML = `
<div class="selection-counter hidden" id="header-counter">0 selected</div>
<div class="seo-section">
<h3>Header Count (Click to filter)</h3>
<table>
<thead>
<tr>
<th>Header Level</th>
<th>Count</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>
</div>
<div class="seo-section">
<h3>Heading Hierarchy</h3>
${hierarchyHTML || '<p>No headers found</p>'}
</div>
`;
// Add click handlers for header filtering
document.querySelectorAll('.header-row').forEach(row => {
row.addEventListener('click', function() {
const level = this.dataset.headerLevel;
filterHeaders(level, this);
});
});
// Restore saved selections
selectedFilters.headers.forEach(level => {
const row = document.querySelector(`.header-row[data-header-level="${level}"]`);
if (row) {
row.classList.add('selected');
}
});
updateHeaderDisplay();
}
function filterHeaders(level, row) {
const index = selectedFilters.headers.indexOf(level);
if (index > -1) {
selectedFilters.headers.splice(index, 1);
row.classList.remove('selected');
} else {
if (selectedFilters.headers.length >= 3) {
return;
}
selectedFilters.headers.push(level);
row.classList.add('selected');
}
savePreferences();
updateHeaderDisplay();
}
function updateHeaderDisplay() {
const counter = document.getElementById('header-counter');
const count = selectedFilters.headers.length;
if (count > 0) {
counter.textContent = `${count} selected`;
counter.classList.remove('hidden');
} else {
counter.classList.add('hidden');
}
document.querySelectorAll('.hierarchy-item').forEach(item => {
const type = item.dataset.headerType;
if (count === 0 || selectedFilters.headers.includes(type)) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}
function analyzeImagesTab() {
const images = Array.from(document.querySelectorAll('img'));
const withAlt = images.filter(img => img.alt && img.alt.trim() !== '');
const withoutAlt = images.filter(img => !img.alt || img.alt.trim() === '');
const imageTypes = {};
images.forEach(img => {
const ext = img.src.split('.').pop().split('?')[0].toLowerCase();
imageTypes[ext] = (imageTypes[ext] || 0) + 1;
});
const withAltRows = withAlt.map((img, idx) => {
const ext = img.src.split('.').pop().split('?')[0].toLowerCase();
return `<tr class="filterable-section" data-image-type="${ext}" data-alt-status="with"><td>${img.alt}</td><td>${ext}</td><td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><a href="${img.src}" target="_blank" class="clickable-link">${img.src}</a></td></tr>`;
}).join('');
const withoutAltRows = withoutAlt.map((img, idx) => {
const ext = img.src.split('.').pop().split('?')[0].toLowerCase();
return `<tr class="filterable-section" data-image-type="${ext}" data-alt-status="without"><td>${ext}</td><td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><a href="${img.src}" target="_blank" class="clickable-link">${img.src}</a></td></tr>`;
}).join('');
const typeStats = Object.entries(imageTypes)
.map(([type, count]) => `<div class="stat-card" data-image-filter="${type}"><div class="stat-number">${count}</div><div class="stat-label">${type.toUpperCase()}</div></div>`)
.join('');
document.getElementById('tab-images').innerHTML = `
<div class="selection-counter hidden" id="image-counter">0 selected</div>
<div class="seo-section">
<h3>Image Statistics (Click to filter by alt status)</h3>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">${images.length}</div>
<div class="stat-label">Total Images</div>
</div>
<div class="stat-card" data-image-filter="with">
<div class="stat-number">${withAlt.length}</div>
<div class="stat-label">With Alt</div>
</div>
<div class="stat-card" data-image-filter="without">
<div class="stat-number">${withoutAlt.length}</div>
<div class="stat-label">Without Alt</div>
</div>
</div>
</div>
<div class="seo-section">
<h3>Image Types (Click to filter by type)</h3>
<div class="stats-grid">
${typeStats}
</div>
</div>
<div class="seo-section">
<h3>Images with Alt Tags</h3>
<table>
<thead>
<tr>
<th>Alt Text</th>
<th>Type</th>
<th>Source</th>
</tr>
</thead>
<tbody id="with-alt-tbody">
${withAltRows || '<tr><td colspan="3">No images with alt tags</td></tr>'}
</tbody>
</table>
</div>
<div class="seo-section">
<h3>Images without Alt Tags</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Source</th>
</tr>
</thead>
<tbody id="without-alt-tbody">
${withoutAltRows || '<tr><td colspan="2">All images have alt tags!</td></tr>'}
</tbody>
</table>
</div>
`;
// Add click handlers for image filtering
document.querySelectorAll('[data-image-filter]').forEach(card => {
card.addEventListener('click', function() {
const type = this.dataset.imageFilter;
filterImages(type, this);
});
});
// Restore saved selections
selectedFilters.images.forEach(filter => {
const card = document.querySelector(`[data-image-filter="${filter}"]`);
if (card) {
card.classList.add('selected');
}
});
updateImageDisplay();
}
function filterImages(type, card) {
const index = selectedFilters.images.indexOf(type);
if (index > -1) {
selectedFilters.images.splice(index, 1);
card.classList.remove('selected');
} else {
if (selectedFilters.images.length >= 3) {
return;
}
selectedFilters.images.push(type);
card.classList.add('selected');
}
savePreferences();
updateImageDisplay();
}
function updateImageDisplay() {
const counter = document.getElementById('image-counter');
const count = selectedFilters.images.length;
if (count > 0) {
counter.textContent = `${count} selected`;
counter.classList.remove('hidden');
} else {
counter.classList.add('hidden');
}
document.querySelectorAll('#with-alt-tbody .filterable-section, #without-alt-tbody .filterable-section').forEach(item => {
const imageType = item.dataset.imageType;
const altStatus = item.dataset.altStatus;
let shouldShow = false;
if (count === 0) {
shouldShow = true;
} else {
for (const filter of selectedFilters.images) {
if (filter === imageType || filter === altStatus) {
shouldShow = true;
break;
}
}
}
if (shouldShow) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}
function analyzeLinksTab() {
const links = Array.from(document.querySelectorAll('a[href]'));
const currentHost = window.location.hostname;
const internal = links.filter(a => {
try {
const url = new URL(a.href, window.location.href);
return url.hostname === currentHost;
} catch {
return false;
}
});
const external = links.filter(a => {
try {
const url = new URL(a.href, window.location.href);
return url.hostname !== currentHost;
} catch {
return false;
}
});
const unique = [...new Set(links.map(a => a.href))];
const noFollow = links.filter(a => a.rel.includes('nofollow'));
const noAnchor = links.filter(a => !a.innerText.trim());
const internalList = internal.map(a => `<div class="link-item filterable-section" data-link-type="internal"><a href="${a.href}" target="_blank" class="clickable-link">${a.href}</a></div>`).join('');
const externalList = external.map(a => `<div class="link-item filterable-section" data-link-type="external"><a href="${a.href}" target="_blank" class="clickable-link">${a.href}</a></div>`).join('');
document.getElementById('tab-links').innerHTML = `
<div class="selection-counter hidden" id="link-counter">0 selected</div>
<div class="seo-section">
<h3>Link Statistics (Click to filter)</h3>
<div class="stats-grid">
<div class="stat-card" data-link-filter="all">
<div class="stat-number">${links.length}</div>
<div class="stat-label">Total Links</div>
</div>
<div class="stat-card" data-link-filter="internal">
<div class="stat-number">${internal.length}</div>
<div class="stat-label">Internal Links</div>
</div>
<div class="stat-card" data-link-filter="external">
<div class="stat-number">${external.length}</div>
<div class="stat-label">External Links</div>
</div>
<div class="stat-card" data-link-filter="unique">
<div class="stat-number">${unique.length}</div>
<div class="stat-label">Unique Links</div>
</div>
<div class="stat-card" data-link-filter="nofollow">
<div class="stat-number">${noFollow.length}</div>
<div class="stat-label">NoFollow Links</div>
</div>
<div class="stat-card" data-link-filter="noanchor">
<div class="stat-number">${noAnchor.length}</div>
<div class="stat-label">No Anchor Text</div>
</div>
</div>
</div>
<div class="seo-section">
<h3>Internal Links</h3>
<div class="link-list">
${internalList || '<p>No internal links found</p>'}
</div>
</div>
<div class="seo-section">
<h3>External Links</h3>
<div class="link-list">
${externalList || '<p>No external links found</p>'}
</div>
</div>
`;
window.linkData = {
all: links,
internal: internal,
external: external,
unique: unique.map(href => links.find(a => a.href === href)),
nofollow: noFollow,
noanchor: noAnchor
};
document.querySelectorAll('[data-link-filter]').forEach(card => {
card.addEventListener('click', function() {
const type = this.dataset.linkFilter;
filterLinks(type, this);
});
});
// Restore saved selections
selectedFilters.links.forEach(filter => {
const card = document.querySelector(`[data-link-filter="${filter}"]`);
if (card) {
card.classList.add('selected');
}
});
updateLinkDisplay();
}
function filterLinks(type, card) {
const index = selectedFilters.links.indexOf(type);
if (index > -1) {
selectedFilters.links.splice(index, 1);
card.classList.remove('selected');
} else {
if (selectedFilters.links.length >= 3) {
return;
}
selectedFilters.links.push(type);
card.classList.add('selected');
}
savePreferences();
updateLinkDisplay();
}
function updateLinkDisplay() {
const counter = document.getElementById('link-counter');
const count = selectedFilters.links.length;
if (count > 0) {
counter.textContent = `${count} selected`;
counter.classList.remove('hidden');
} else {
counter.classList.add('hidden');
}
if (!window.linkData) return;
let filteredLinks = new Set();
if (count === 0) {
document.querySelectorAll('.link-list .filterable-section').forEach(item => {
item.classList.remove('hidden');
});
return;
}
selectedFilters.links.forEach(filter => {
if (window.linkData[filter]) {
window.linkData[filter].forEach(link => {
filteredLinks.add(link.href);
});
}
});
document.querySelectorAll('.link-list .filterable-section').forEach(item => {
const link = item.querySelector('a');
if (link && filteredLinks.has(link.href)) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}
function analyzeSchemaTab() {
const schemas = [];
const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
jsonLdScripts.forEach((script, index) => {
try {
const data = JSON.parse(script.textContent);
const type = data['@type'] || (Array.isArray(data) ? data.map(d => d['@type']).join(', ') : 'Unknown');
schemas.push({
type: type,
format: 'JSON-LD',
content: JSON.stringify(data, null, 2)
});
} catch (e) {
schemas.push({
type: 'Invalid JSON-LD',
format: 'JSON-LD',
content: 'Error parsing JSON-LD schema'
});
}
});
const microdataItems = document.querySelectorAll('[itemscope]');
if (microdataItems.length > 0) {
const microdataTypes = [...new Set(Array.from(microdataItems).map(item => {
return item.getAttribute('itemtype') || 'No type specified';
}))];
microdataTypes.forEach(type => {
schemas.push({
type: type.split('/').pop() || 'Microdata',
format: 'Microdata',
content: `Found ${Array.from(microdataItems).filter(i => i.getAttribute('itemtype') === type).length} instance(s)`
});
});
}
const rdfaItems = document.querySelectorAll('[vocab], [typeof]');
if (rdfaItems.length > 0) {
const rdfaTypes = [...new Set(Array.from(rdfaItems).map(item => {
return item.getAttribute('typeof') || 'No type specified';
}))];
rdfaTypes.forEach(type => {
schemas.push({
type: type || 'RDFa',
format: 'RDFa',
content: `Found ${Array.from(rdfaItems).filter(i => i.getAttribute('typeof') === type).length} instance(s)`
});
});
}
let schemaHTML = '';
if (schemas.length === 0) {
schemaHTML = '<div class="no-schema">No schema markup found on this page</div>';
} else {
const stats = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">${schemas.length}</div>
<div class="stat-label">Total Schemas</div>
</div>
<div class="stat-card">
<div class="stat-number">${schemas.filter(s => s.format === 'JSON-LD').length}</div>
<div class="stat-label">JSON-LD</div>
</div>
<div class="stat-card">
<div class="stat-number">${schemas.filter(s => s.format === 'Microdata').length}</div>
<div class="stat-label">Microdata</div>
</div>
<div class="stat-card">
<div class="stat-number">${schemas.filter(s => s.format === 'RDFa').length}</div>
<div class="stat-label">RDFa</div>
</div>
</div>
`;
const schemaList = schemas.map((schema, index) => `
<div class="schema-list-item">
<span class="schema-list-type">${schema.type}</span>
<span class="schema-list-format">${schema.format}</span>
</div>
`).join('');
const schemaBlocks = schemas.map((schema, index) => `
<div class="schema-block" data-schema-index="${index}">
<div class="schema-header">
<div class="schema-type">${schema.format}: ${schema.type}</div>
<div class="schema-toggle">▼</div>
</div>
<div class="schema-code" id="schema-code-${index}">${schema.content}</div>
</div>
`).join('');
schemaHTML = `
<div class="seo-section">
<h3>Schema Statistics</h3>
${stats}
</div>
<div class="seo-section">
<h3>All Schemas Found</h3>
<div class="schema-list">
${schemaList}
</div>
</div>
<div class="seo-section">
<h3>Schema Markup Details</h3>
${schemaBlocks}
</div>
`;
}
document.getElementById('tab-schema').innerHTML = schemaHTML;
document.querySelectorAll('.schema-block').forEach(block => {
block.addEventListener('click', function() {
const index = this.dataset.schemaIndex;
const codeBlock = document.getElementById(`schema-code-${index}`);
const toggle = this.querySelector('.schema-toggle');
codeBlock.classList.toggle('visible');
toggle.classList.toggle('expanded');
});
});
}
})();