UNPKG

@casoon/auditmysite

Version:

Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe

836 lines โ€ข 35.2 kB
"use strict"; /** * ๐Ÿ”ง UNIFIED Page Analysis Event System * * โœจ MAIN EVENT SYSTEM - Replaces multiple parallel event systems * * Event-driven system where analyzers attach to page load events * and contribute their data to a unified result structure. * * ๐ŸŽฏ CONSOLIDATES: * - AccessibilityChecker event callbacks * - EventDrivenQueue callbacks * - ParallelTestManager events * - Direct callback patterns in bin/audit.js * * ๐Ÿ“‹ BACKWARD COMPATIBILITY: * - Supports all existing callback patterns via adapters * - Maintains existing APIs while using unified backend * * ๐Ÿš€ FEATURES: * - Parallel analyzer execution * - Resource monitoring integration * - Backpressure control integration * - Progress tracking * - Error handling & fallbacks * - State persistence */ Object.defineProperty(exports, "__esModule", { value: true }); exports.seoAnalyzer = exports.performanceAnalyzer = exports.accessibilityAnalyzer = exports.PageAnalysisEmitter = void 0; const events_1 = require("events"); const backpressure_controller_1 = require("../backpressure-controller"); const resource_monitor_1 = require("../resource-monitor"); /** * ๐ŸŽฏ UNIFIED PAGE ANALYSIS EMITTER - Main Event System * * Replaces multiple parallel event systems with a single, comprehensive solution. * Maintains backward compatibility while providing enhanced features. */ class PageAnalysisEmitter extends events_1.EventEmitter { constructor(options = {}) { super(); this.analyzers = new Map(); this.callbacks = {}; this.isInitialized = false; this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.options = { enableResourceMonitoring: options.enableResourceMonitoring ?? true, enableBackpressure: options.enableBackpressure ?? true, maxConcurrent: options.maxConcurrent ?? 3, maxRetries: options.maxRetries ?? 3, retryDelay: options.retryDelay ?? 2000, verbose: options.verbose ?? false, ...options }; this.callbacks = options.callbacks || {}; // Initialize stats this.stats = { total: 0, pending: 0, inProgress: 0, completed: 0, failed: 0, retrying: 0, progress: 0, averageDuration: 0, estimatedTimeRemaining: 0, activeWorkers: 0, memoryUsage: 0, cpuUsage: 0 }; // Initialize system metrics this.systemMetrics = { memoryUsageMB: 0, heapUsedMB: 0, cpuUsagePercent: 0, eventLoopDelayMs: 0, activeHandles: 0, gcCount: 0, uptimeSeconds: 0 }; } /** * ๐Ÿš€ Initialize the unified event system with integrated monitoring */ async initialize() { if (this.isInitialized) return; try { // Setup resource monitoring if (this.options.enableResourceMonitoring) { this.setupResourceMonitoring(); } // Setup backpressure control if (this.options.enableBackpressure) { this.setupBackpressureControl(); } this.isInitialized = true; if (this.options.verbose) { console.log(`๐Ÿš€ Unified Page Analysis System initialized (${this.sessionId})`); console.log(` ๐Ÿ“Š Resource Monitoring: ${this.options.enableResourceMonitoring ? 'โœ…' : 'โŒ'}`); console.log(` ๐Ÿƒ Backpressure Control: ${this.options.enableBackpressure ? 'โœ…' : 'โŒ'}`); console.log(` ๐Ÿ”„ Max Concurrent: ${this.options.maxConcurrent}`); } } catch (error) { console.error(`โŒ Failed to initialize unified event system: ${error}`); throw error; } } /** * ๐Ÿ“‹ Register an analyzer that will run when a page is loaded * * BACKWARD COMPATIBLE: Maintains existing analyzer registration pattern */ registerAnalyzer(name, analyzer) { this.analyzers.set(name, analyzer); if (this.options.verbose) { console.log(`๐Ÿ“‹ Registered analyzer: ${name} (total: ${this.analyzers.size})`); } this.emit('analyzer-registered', { name, total: this.analyzers.size }); } /** * ๐ŸŽฏ Set unified event callbacks * * This replaces/consolidates: * - TestOptions.eventCallbacks * - EventDrivenQueueOptions.eventCallbacks * - Direct callback patterns */ setEventCallbacks(callbacks) { this.callbacks = { ...this.callbacks, ...callbacks }; if (this.options.verbose) { const callbackNames = Object.keys(callbacks).join(', '); console.log(`๐ŸŽฏ Event callbacks configured: ${callbackNames}`); } } /** * ๐Ÿ“Š Setup resource monitoring integration */ setupResourceMonitoring() { try { this.resourceMonitor = new resource_monitor_1.ResourceMonitor({ enabled: true, samplingIntervalMs: 2000, memoryWarningThresholdMB: 1024, memoryCriticalThresholdMB: 1536 }); // Connect resource events to unified callbacks this.resourceMonitor.on('memoryWarning', (data) => { this.callbacks.onResourceWarning?.(data.current, data.threshold, 'memory'); }); this.resourceMonitor.on('memoryCritical', (data) => { this.callbacks.onResourceCritical?.(data.current, data.max, 'memory'); }); this.resourceMonitor.on('cpuWarning', (data) => { this.callbacks.onResourceWarning?.(data.current, data.threshold, 'cpu'); }); // Update system metrics this.resourceMonitor.on('metricsUpdate', (metrics) => { this.systemMetrics = { memoryUsageMB: metrics.rssMemoryMB, heapUsedMB: metrics.heapUsedMB, cpuUsagePercent: metrics.cpuUsagePercent, eventLoopDelayMs: metrics.eventLoopDelayMs, activeHandles: 0, // Would need additional monitoring gcCount: metrics.gcCount || 0, uptimeSeconds: metrics.uptimeSeconds }; this.callbacks.onSystemMetrics?.(this.systemMetrics); }); this.resourceMonitor.start(); } catch (error) { console.warn(`โš ๏ธ Resource monitoring setup failed: ${error}`); } } /** * ๐Ÿƒ Setup backpressure control integration */ setupBackpressureControl() { try { this.backpressureController = new backpressure_controller_1.AdaptiveBackpressureController({ enabled: true, maxMemoryUsageMB: 1536, maxCpuUsagePercent: 85 }); // Connect backpressure events to unified callbacks this.backpressureController.on('backpressureActivated', (data) => { this.callbacks.onBackpressureActivated?.(data.reason || 'Resource pressure detected'); }); this.backpressureController.on('backpressureDeactivated', () => { this.callbacks.onBackpressureDeactivated?.(); }); this.backpressureController.on('gcTriggered', (data) => { this.callbacks.onGarbageCollection?.(data.beforeMB, data.afterMB); }); } catch (error) { console.warn(`โš ๏ธ Backpressure control setup failed: ${error}`); } } /** * ๐Ÿ“ˆ Update and emit progress statistics */ updateProgress() { // Calculate progress percentage if (this.stats.total > 0) { this.stats.progress = ((this.stats.completed + this.stats.failed) / this.stats.total) * 100; } // Emit progress update this.callbacks.onProgressUpdate?.(this.stats); this.emit('progress-update', this.stats); } /** * ๐ŸŽฏ Get current progress statistics */ getProgressStats() { return { ...this.stats }; } /** * ๐Ÿš€ Get system metrics */ getSystemMetrics() { return { ...this.systemMetrics }; } /** * ๐Ÿงช Cleanup resources (enhanced version) */ async cleanup() { try { if (this.resourceMonitor) { this.resourceMonitor.stop(); } if (this.backpressureController) { // Cleanup backpressure controller if it has cleanup method } this.emit('cleanup-complete'); if (this.options.verbose) { console.log(`๐Ÿงช Unified event system cleanup completed (${this.sessionId})`); } } catch (error) { console.error(`โŒ Error during cleanup: ${error}`); } } /** * ๐ŸŽฏ UNIFIED PAGE ANALYSIS - Enhanced version with all features * * BACKWARD COMPATIBLE: Maintains existing analyzePage signature * ENHANCED: Integrates resource monitoring, backpressure, progress tracking */ async analyzePage(url, page, options = {}, contextOptions = {}) { const startTime = Date.now(); // Initialize result structure const result = { url, title: '', status: 'success', duration: 0, accessibility: { passed: true, score: 100, errors: [], warnings: [], issues: [], basicChecks: { imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, } } }; const context = { url, page, options, result, startTime, ...contextOptions }; try { // Navigate to page if (options.verbose) console.log(` ๐ŸŒ Loading: ${url}`); await page.goto(url, { waitUntil: options.waitUntil || 'domcontentloaded', timeout: options.timeout || 10000, }); // Get basic page info result.title = await page.title(); if (options.verbose) console.log(` ๐Ÿ“‹ Title: ${result.title}`); // Emit page-loaded event and run all analyzers this.emit('page-loaded', context); // Run all registered analyzers in parallel const analyzerPromises = Array.from(this.analyzers.entries()).map(async ([name, analyzer]) => { try { if (options.verbose) console.log(` ๐Ÿ” Running ${name} analysis...`); await analyzer(context); } catch (error) { console.error(` โŒ ${name} analysis failed: ${error}`); // Add error but don't fail the whole analysis result.accessibility.warnings.push({ code: `${name.toUpperCase()}_ANALYSIS_ERROR`, message: `${name} analysis failed: ${error}`, type: 'warning' }); } }); await Promise.all(analyzerPromises); } catch (error) { console.error(` ๐Ÿ’ฅ Page loading failed: ${error}`); result.status = 'crashed'; result.accessibility.passed = false; result.accessibility.errors.push({ code: 'PAGE_LOAD_ERROR', message: `Failed to load page: ${error}`, type: 'error' }); } finally { result.duration = Date.now() - startTime; // Determine overall status if (result.accessibility.errors.length > 0) { result.accessibility.passed = false; if (result.status === 'success') { result.status = 'failed'; } } } return result; } /** * Get list of registered analyzers */ getRegisteredAnalyzers() { return Array.from(this.analyzers.keys()); } } exports.PageAnalysisEmitter = PageAnalysisEmitter; /** * Comprehensive accessibility analysis using WCAG 2.1 principles * Categorizes Pa11y/Axe issues into structured compliance metrics */ async function performComprehensiveAccessibilityAnalysis(page, result) { // Initialize comprehensive analysis structures result.accessibility.wcagAnalysis = { perceivable: { colorContrast: { violations: 0, score: 100 }, textAlternatives: { violations: 0, score: 100 }, captions: { violations: 0, score: 100 }, adaptable: { violations: 0, score: 100 } }, operable: { keyboardAccessible: { violations: 0, score: 100 }, seizures: { violations: 0, score: 100 }, navigable: { violations: 0, score: 100 }, inputModalities: { violations: 0, score: 100 } }, understandable: { readable: { violations: 0, score: 100 }, predictable: { violations: 0, score: 100 }, inputAssistance: { violations: 0, score: 100 } }, robust: { compatible: { violations: 0, score: 100 }, parsing: { violations: 0, score: 100 } } }; result.accessibility.ariaAnalysis = { totalViolations: 0, landmarks: { present: [], missing: [], score: 100 }, roles: { correct: 0, incorrect: 0, missing: 0, score: 100 }, properties: { correct: 0, incorrect: 0, missing: 0, score: 100 }, liveRegions: { present: 0, appropriate: 0, score: 100 } }; result.accessibility.formAnalysis = { totalElements: 0, labeling: { proper: 0, missing: 0, inadequate: 0, score: 100 }, validation: { accessible: 0, inaccessible: 0, score: 100 }, focusManagement: { proper: 0, issues: 0, score: 100 } }; result.accessibility.keyboardAnalysis = { focusIndicators: { visible: 0, missing: 0, score: 100 }, tabOrder: { logical: 0, problematic: 0, score: 100 }, keyboardTraps: { detected: 0, score: 100 } }; // Perform comprehensive DOM analysis await performDOMAccessibilityAnalysis(page, result); } /** * Analyze DOM for comprehensive accessibility metrics */ async function performDOMAccessibilityAnalysis(page, result) { try { // Get comprehensive accessibility data from the browser const domAnalysis = await page.evaluate(() => { const analysis = { landmarks: [], forms: { total: 0, labeled: 0, unlabeled: 0 }, images: { total: 0, withAlt: 0, withoutAlt: 0, decorative: 0 }, headings: { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, structure: [] }, links: { total: 0, withText: 0, withoutText: 0 }, buttons: { total: 0, labeled: 0, unlabeled: 0 }, tables: { total: 0, withHeaders: 0, withoutHeaders: 0 }, ariaElements: { roles: 0, landmarks: 0, liveRegions: 0 }, focusableElements: { total: 0, withTabindex: 0, withFocusIndicator: 0 }, colorContrast: { potentialIssues: 0 } }; // Analyze landmarks const landmarkSelectors = ['main', 'nav', 'aside', 'header', 'footer', '[role="banner"]', '[role="navigation"]', '[role="main"]', '[role="contentinfo"]', '[role="complementary"]']; landmarkSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { analysis.landmarks.push({ type: el.tagName.toLowerCase() === 'main' ? 'main' : el.getAttribute('role') || el.tagName.toLowerCase(), hasLabel: !!(el.getAttribute('aria-label') || el.getAttribute('aria-labelledby')) }); }); }); // Analyze forms const forms = document.querySelectorAll('input, select, textarea'); analysis.forms.total = forms.length; forms.forEach(input => { const hasLabel = !!(input.getAttribute('aria-label') || input.getAttribute('aria-labelledby') || document.querySelector(`label[for="${input.id}"]`) || input.closest('label')); if (hasLabel) analysis.forms.labeled++; else analysis.forms.unlabeled++; }); // Analyze images const images = document.querySelectorAll('img'); analysis.images.total = images.length; images.forEach(img => { const alt = img.getAttribute('alt'); if (alt === '') analysis.images.decorative++; else if (alt) analysis.images.withAlt++; else analysis.images.withoutAlt++; }); // Analyze headings ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => { const count = document.querySelectorAll(tag).length; analysis.headings[tag] = count; if (count > 0) { analysis.headings.structure.push({ level: parseInt(tag[1]), count }); } }); // Analyze links const links = document.querySelectorAll('a[href]'); analysis.links.total = links.length; links.forEach(link => { const text = (link.textContent || '').trim(); const ariaLabel = link.getAttribute('aria-label'); if (text || ariaLabel) analysis.links.withText++; else analysis.links.withoutText++; }); // Analyze buttons const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], input[type="reset"]'); analysis.buttons.total = buttons.length; buttons.forEach(button => { const text = (button.textContent || '').trim(); const ariaLabel = button.getAttribute('aria-label'); const value = button.getAttribute('value'); if (text || ariaLabel || value) analysis.buttons.labeled++; else analysis.buttons.unlabeled++; }); // Analyze tables const tables = document.querySelectorAll('table'); analysis.tables.total = tables.length; tables.forEach(table => { const hasHeaders = table.querySelectorAll('th, [scope]').length > 0; if (hasHeaders) analysis.tables.withHeaders++; else analysis.tables.withoutHeaders++; }); // Analyze ARIA usage analysis.ariaElements.roles = document.querySelectorAll('[role]').length; analysis.ariaElements.landmarks = document.querySelectorAll('[role="banner"], [role="navigation"], [role="main"], [role="contentinfo"], [role="complementary"]').length; analysis.ariaElements.liveRegions = document.querySelectorAll('[aria-live], [role="status"], [role="alert"]').length; // Analyze focusable elements const focusableSelectors = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; const focusableElements = document.querySelectorAll(focusableSelectors); analysis.focusableElements.total = focusableElements.length; focusableElements.forEach(el => { if (el.getAttribute('tabindex')) analysis.focusableElements.withTabindex++; // Note: Focus indicator detection would require CSS analysis }); // Basic color contrast analysis (simplified) const textElements = document.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button, label'); let potentialContrastIssues = 0; textElements.forEach(el => { const style = window.getComputedStyle(el); const backgroundColor = style.backgroundColor; const color = style.color; // Simplified check - in reality, you'd need proper contrast calculation if ((backgroundColor === 'rgb(255, 255, 255)' || backgroundColor === 'rgba(0, 0, 0, 0)') && (color === 'rgb(128, 128, 128)' || color.includes('rgba'))) { potentialContrastIssues++; } }); analysis.colorContrast.potentialIssues = potentialContrastIssues; return analysis; }); // Process DOM analysis results into structured metrics processAccessibilityMetrics(domAnalysis, result); } catch (error) { console.error('DOM accessibility analysis failed:', error); } } /** * Process DOM analysis into structured accessibility metrics */ function processAccessibilityMetrics(domAnalysis, result) { const wcag = result.accessibility.wcagAnalysis; const aria = result.accessibility.ariaAnalysis; const form = result.accessibility.formAnalysis; const keyboard = result.accessibility.keyboardAnalysis; // WCAG Perceivable Principle wcag.perceivable.textAlternatives.violations = domAnalysis.images.withoutAlt; wcag.perceivable.textAlternatives.score = calculateScore(domAnalysis.images.withAlt, domAnalysis.images.total); wcag.perceivable.colorContrast.violations = domAnalysis.colorContrast.potentialIssues; wcag.perceivable.colorContrast.score = Math.max(0, 100 - (domAnalysis.colorContrast.potentialIssues * 5)); // WCAG Operable Principle wcag.operable.navigable.violations = domAnalysis.links.withoutText + (domAnalysis.headings.structure.length === 0 ? 1 : 0); wcag.operable.navigable.score = calculateScore(domAnalysis.links.withText, domAnalysis.links.total); // ARIA Analysis aria.totalViolations = domAnalysis.buttons.unlabeled + domAnalysis.forms.unlabeled; aria.landmarks.present = domAnalysis.landmarks.map((l) => l.type); aria.landmarks.missing = ['main', 'navigation'].filter(type => !aria.landmarks.present.includes(type)); aria.landmarks.score = aria.landmarks.present.length > 0 ? 100 : 50; aria.roles.correct = domAnalysis.ariaElements.roles; aria.roles.score = Math.min(100, domAnalysis.ariaElements.roles * 10); // Form Analysis form.totalElements = domAnalysis.forms.total; form.labeling.proper = domAnalysis.forms.labeled; form.labeling.missing = domAnalysis.forms.unlabeled; form.labeling.score = calculateScore(domAnalysis.forms.labeled, domAnalysis.forms.total); // Keyboard Analysis keyboard.focusIndicators.visible = domAnalysis.focusableElements.total - domAnalysis.focusableElements.withTabindex; keyboard.focusIndicators.missing = domAnalysis.focusableElements.withTabindex; keyboard.focusIndicators.score = calculateScore(keyboard.focusIndicators.visible, domAnalysis.focusableElements.total); // Update basic checks with enhanced contrast information result.accessibility.basicChecks.contrastIssues = domAnalysis.colorContrast.potentialIssues; } /** * Calculate accessibility score based on compliant vs total elements */ function calculateScore(compliant, total) { if (total === 0) return 100; return Math.round((compliant / total) * 100); } // Default analyzer functions const accessibilityAnalyzer = async (context) => { const { page, result } = context; // Enhanced accessibility analysis with comprehensive metrics await performComprehensiveAccessibilityAnalysis(page, result); // Legacy basic checks for backward compatibility result.accessibility.basicChecks.imagesWithoutAlt = await page.locator('img:not([alt])').count(); result.accessibility.basicChecks.buttonsWithoutLabel = await page .locator('button:not([aria-label])') .filter({ hasText: '' }) .count(); result.accessibility.basicChecks.headingsCount = await page.locator('h1, h2, h3, h4, h5, h6').count(); // Add warnings for basic issues (legacy support) if (result.accessibility.basicChecks.imagesWithoutAlt > 0) { result.accessibility.warnings.push({ code: 'MISSING_ALT_ATTRIBUTE', message: `${result.accessibility.basicChecks.imagesWithoutAlt} images without alt attribute`, type: 'warning' }); } if (result.accessibility.basicChecks.buttonsWithoutLabel > 0) { result.accessibility.warnings.push({ code: 'MISSING_BUTTON_LABEL', message: `${result.accessibility.basicChecks.buttonsWithoutLabel} buttons without aria-label`, type: 'warning' }); } if (result.accessibility.basicChecks.headingsCount === 0) { result.accessibility.errors.push({ code: 'NO_HEADINGS', message: 'No headings found', type: 'error' }); } // Run pa11y tests try { const pa11y = require('pa11y'); const pa11yResult = await pa11y(context.url, { timeout: 15000, wait: 2000, standard: 'WCAG2AA', includeNotices: true, includeWarnings: true, runners: ['axe'], chromeLaunchConfig: { args: [ '--disable-web-security', '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu' ] } }); // Add pa11y issues if (pa11yResult.issues) { pa11yResult.issues.forEach((issue) => { const detailedIssue = { code: issue.code, message: issue.message, type: issue.type, selector: issue.selector, context: issue.context, impact: issue.impact, }; result.accessibility.issues.push(detailedIssue); if (issue.type === 'error') { result.accessibility.errors.push({ code: issue.code, message: issue.message, type: 'error' }); } else if (issue.type === 'warning') { result.accessibility.warnings.push({ code: issue.code, message: issue.message, type: 'warning' }); } }); } // Calculate score if (pa11yResult.issues && pa11yResult.issues.length > 0) { const errors = pa11yResult.issues.filter((i) => i.type === 'error').length; const warnings = pa11yResult.issues.filter((i) => i.type === 'warning').length; result.accessibility.score = Math.max(10, 100 - (errors * 5) - (warnings * 2)); } } catch (error) { // pa11y failed, use fallback score let score = 100; score -= result.accessibility.errors.length * 15; score -= result.accessibility.warnings.length * 5; result.accessibility.score = Math.max(0, score); } }; exports.accessibilityAnalyzer = accessibilityAnalyzer; const performanceAnalyzer = async (context) => { const { page, result, options } = context; if (!options.enablePerformanceAnalysis) return; try { // Get performance metrics using browser's Performance API const metrics = await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; const paint = performance.getEntriesByType('paint'); const fcp = paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0; const lcp = performance.getEntriesByType('largest-contentful-paint')[0]?.startTime || 0; return { loadTime: navigation?.loadEventEnd - navigation?.loadEventStart || 0, domContentLoaded: navigation?.domContentLoadedEventEnd - navigation?.domContentLoadedEventStart || 0, renderTime: navigation?.loadEventEnd - navigation?.fetchStart || 0, fcp, lcp: lcp || fcp * 1.5, // Fallback estimate cls: 0, // Would need special measurement inp: 0, // Would need interaction ttfb: navigation?.responseStart - navigation?.fetchStart || 0, }; }); // Calculate performance score (simplified) let score = 100; if (metrics.lcp > 4000) score -= 30; else if (metrics.lcp > 2500) score -= 15; if (metrics.fcp > 3000) score -= 20; else if (metrics.fcp > 1800) score -= 10; if (metrics.ttfb > 600) score -= 15; const grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 50 ? 'D' : 'F'; result.performance = { score: Math.max(0, score), grade, coreWebVitals: { lcp: Math.round(metrics.lcp), fcp: Math.round(metrics.fcp), cls: metrics.cls, inp: metrics.inp, ttfb: Math.round(metrics.ttfb), }, timing: { loadTime: Math.round(metrics.loadTime), domContentLoaded: Math.round(metrics.domContentLoaded), renderTime: Math.round(metrics.renderTime), } }; } catch (error) { console.error('Performance analysis failed:', error); } }; exports.performanceAnalyzer = performanceAnalyzer; const seoAnalyzer = async (context) => { const { page, result, options } = context; if (!options.enableSEOAnalysis) return; try { const seoData = await page.evaluate(() => { // Meta tags const title = document.querySelector('title')?.textContent || ''; const description = document.querySelector('meta[name="description"]')?.getAttribute('content') || ''; const keywords = document.querySelector('meta[name="keywords"]')?.getAttribute('content') || ''; // Open Graph const ogTags = {}; document.querySelectorAll('meta[property^="og:"]').forEach(meta => { const property = meta.getAttribute('property')?.replace('og:', ''); const content = meta.getAttribute('content'); if (property && content) ogTags[property] = content; }); // Twitter Card const twitterTags = {}; document.querySelectorAll('meta[name^="twitter:"]').forEach(meta => { const name = meta.getAttribute('name')?.replace('twitter:', ''); const content = meta.getAttribute('content'); if (name && content) twitterTags[name] = content; }); // Headings const h1 = Array.from(document.querySelectorAll('h1')).map(h => h.textContent || ''); const h2 = Array.from(document.querySelectorAll('h2')).map(h => h.textContent || ''); const h3 = Array.from(document.querySelectorAll('h3')).map(h => h.textContent || ''); // Images const images = document.querySelectorAll('img'); const missingAlt = Array.from(images).filter(img => !img.getAttribute('alt')).length; const emptyAlt = Array.from(images).filter(img => img.getAttribute('alt') === '').length; return { title, description, keywords: keywords ? keywords.split(',').map(k => k.trim()) : [], ogTags, twitterTags, h1, h2, h3, totalImages: images.length, missingAlt, emptyAlt, }; }); // Calculate SEO score let score = 100; const issues = []; if (!seoData.title || seoData.title.length < 10) { score -= 20; issues.push('Missing or too short title tag'); } else if (seoData.title.length > 60) { score -= 10; issues.push('Title tag too long'); } if (!seoData.description || seoData.description.length < 120) { score -= 15; issues.push('Missing or too short meta description'); } else if (seoData.description.length > 160) { score -= 5; issues.push('Meta description too long'); } if (seoData.h1.length === 0) { score -= 15; issues.push('Missing H1 heading'); } else if (seoData.h1.length > 1) { score -= 10; issues.push('Multiple H1 headings'); } const grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 50 ? 'D' : 'F'; result.seo = { score: Math.max(0, score), grade, metaTags: { title: seoData.title ? { content: seoData.title, length: seoData.title.length, optimal: seoData.title.length >= 10 && seoData.title.length <= 60 } : undefined, description: seoData.description ? { content: seoData.description, length: seoData.description.length, optimal: seoData.description.length >= 120 && seoData.description.length <= 160 } : undefined, keywords: seoData.keywords, openGraph: seoData.ogTags, twitterCard: seoData.twitterTags }, headings: { h1: seoData.h1, h2: seoData.h2, h3: seoData.h3, issues }, images: { total: seoData.totalImages, missingAlt: seoData.missingAlt, emptyAlt: seoData.emptyAlt } }; } catch (error) { console.error('SEO analysis failed:', error); } }; exports.seoAnalyzer = seoAnalyzer; //# sourceMappingURL=page-analysis-emitter.js.map