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

1,467 lines (1,375 loc) 52.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedHTMLGenerator = void 0; const fs = __importStar(require("fs/promises")); /** * Unified HTML Generator * - Reads JSON (FullAuditResult) * - Renders comprehensive, interactive HTML using strict types * - Section-based architecture with navigation, performance, SEO, accessibility details * - Includes all features from legacy generators in clean, maintainable code */ class UnifiedHTMLGenerator { async generateFromJSON(jsonPath) { const json = await fs.readFile(jsonPath, 'utf8'); const data = JSON.parse(json); return this.generate(data); } async generate(data) { const css = this.generateCSS(); const reportTitle = this.renderReportTitle(data); const navigation = this.renderNavigation(); const summary = this.renderSummary(data); const accessibilitySection = this.renderAccessibilitySection(data); const performanceSection = this.renderPerformanceSection(data); const seoSection = this.renderSeoSection(data); const contentWeightSection = this.renderContentWeightSection(data); const mobileFriendlinessSection = this.renderMobileFriendlinessSection(data); const detailedIssuesSection = this.renderDetailedIssuesSection(data); const footer = this.renderFooter(data); const javascript = this.generateJavaScript(); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AuditMySite Report - ${this.getDomain(data)}</title> <style>${css}</style> </head> <body> ${navigation} <main class="main-content"> <div class="container"> ${reportTitle} ${summary} ${accessibilitySection} ${performanceSection} ${seoSection} ${contentWeightSection} ${mobileFriendlinessSection} ${detailedIssuesSection} </div> </main> ${footer} <script>${javascript}</script> </body> </html>`; } generateCSS() { return ` :root { --color-bg: #f8fafc; --color-card: #ffffff; --color-text: #1f2937; --color-subtle: #6b7280; --primary: #2563eb; --success: #10b981; --warning: #f59e0b; --error: #ef4444; --radius: 10px; --shadow: 0 2px 12px rgba(0,0,0,0.08); --nav-height: 80px; } /* Reset and base styles */ *, *::before, *::after { box-sizing: border-box; } body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background: var(--color-bg); color: var(--color-text); margin: 0; line-height: 1.5; } .container { max-width: 1200px; margin: 0 auto; padding: 24px; } .main-content { margin-top: var(--nav-height); padding-top: 20px; } /* Fix anchor navigation for sticky header */ #summary, #accessibility, #performance, #seo, #content-weight, #mobile, #issues { scroll-margin-top: calc(var(--nav-height) + 40px); } /* Report Title Section */ .report-title { background: var(--color-card); border-radius: var(--radius); box-shadow: var(--shadow); padding: 32px; margin: 32px 0 32px 0; text-align: center; border: 2px solid #e5e7eb; } .report-title h1 { margin: 0 0 16px; color: var(--primary); font-size: 2.5rem; font-weight: 700; } .report-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 24px; text-align: left; } .report-meta-item { display: flex; flex-direction: column; padding: 16px; background: #f8fafc; border-radius: 8px; border: 1px solid #e5e7eb; } .report-meta-label { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-subtle); margin-bottom: 4px; } .report-meta-value { font-size: 16px; font-weight: 500; color: var(--color-text); } /* Navigation */ .navigation { position: fixed; top: 0; left: 0; right: 0; background: var(--color-card); border-bottom: 2px solid var(--primary); z-index: 1000; min-height: var(--nav-height); box-shadow: 0 2px 8px rgba(0,0,0,0.1); display: flex; align-items: center; } .nav-links { display: flex; gap: 6px; padding: 16px 0; overflow-x: auto; align-items: center; min-height: var(--nav-height); } .nav-link { color: var(--color-text); text-decoration: none; padding: 14px 24px; border-radius: var(--radius); font-size: 15px; font-weight: 600; white-space: nowrap; transition: all 0.2s ease; background: var(--color-bg); border: 1px solid #e5e7eb; margin: 2px; display: flex; align-items: center; min-height: 48px; } .nav-link:hover { background: var(--primary); color: white; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(37, 99, 235, 0.2); } .nav-link.active { background: var(--primary); color: white; box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3); } /* Cards and sections */ .card { background: var(--color-card); border-radius: var(--radius); box-shadow: var(--shadow); padding: 24px; margin-bottom: 24px; } .card h2 { margin: 0 0 20px; font-size: 24px; font-weight: 700; color: var(--color-text); border-bottom: 2px solid #e5e7eb; padding-bottom: 12px; } .card h3 { margin: 20px 0 16px; font-size: 18px; font-weight: 600; color: var(--color-text); } .header h1 { margin: 0 0 8px; color: var(--primary); font-size: 32px; } .subtitle { color: var(--color-subtle); font-size: 14px; } .subtitle > div { margin-bottom: 4px; } /* Grid and metrics */ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px; } .metric { border: 1px solid #e5e7eb; border-radius: var(--radius); padding: 20px; text-align: center; transition: transform 0.2s, box-shadow 0.2s; } .metric:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.1); } .metric .value { font-size: 32px; font-weight: 700; margin-bottom: 8px; line-height: 1; } .metric .value.error { color: var(--error); } .metric .value.warning { color: var(--warning); } .metric .value.success { color: var(--success); } .metric .label { color: var(--color-subtle); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; } /* Badges */ .badge { display: inline-block; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; margin: 2px; } .badge.success { background: #ecfdf5; color: #065f46; } .badge.warning { background: #fffbeb; color: #92400e; } .badge.error { background: #fef2f2; color: #991b1b; } /* Tables */ table { width: 100%; border-collapse: collapse; margin-top: 16px; } th, td { padding: 12px 16px; border-bottom: 1px solid #e5e7eb; text-align: left; } th { background: #f3f4f6; font-weight: 600; color: #374151; position: sticky; top: var(--nav-height); } .page-status { font-weight: 700; } .status-passed { color: var(--success); } .status-failed { color: var(--error); } .status-crashed { color: var(--warning); } /* Issues section */ .issues-filter { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; } .filter-btn { background: #f3f4f6; border: 1px solid #d1d5db; color: var(--color-text); padding: 8px 16px; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .filter-btn:hover, .filter-btn.active { background: var(--primary); color: white; border-color: var(--primary); } .issues-list { max-height: 600px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: var(--radius); } .issue-item { border-bottom: 1px solid #e5e7eb; padding: 16px; transition: background-color 0.2s; } .issue-item:hover { background: #f9fafb; } .issue-item:last-child { border-bottom: none; } .issue-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; } .issue-type { font-size: 12px; color: var(--color-subtle); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } .issue-page { font-size: 12px; color: var(--primary); background: #eff6ff; padding: 2px 8px; border-radius: 10px; } .issue-content h4 { margin: 0 0 8px; font-size: 14px; font-weight: 600; color: var(--color-text); } .issue-context { font-size: 13px; color: var(--color-subtle); margin-bottom: 4px; } .issue-selector { font-size: 12px; color: var(--color-subtle); } .issue-selector code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; } /* Section-specific styles */ .no-data { text-align: center; color: var(--color-subtle); font-style: italic; padding: 40px 20px; } .no-issues { text-align: center; color: var(--success); font-weight: 600; padding: 20px; background: #ecfdf5; border-radius: var(--radius); margin-top: 16px; } .issues-summary { margin-top: 16px; } .issue-summary { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #f3f4f6; font-size: 14px; } /* Performance details */ .performance-table { margin-top: 16px; } .performance-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #e5e7eb; } .performance-row:last-child { border-bottom: none; } .page-name { font-weight: 600; color: var(--color-text); flex: 1; } .metrics { display: flex; gap: 16px; } .metric-item { font-size: 12px; color: var(--color-subtle); background: #f3f4f6; padding: 4px 8px; border-radius: 12px; } /* SEO, Content, Mobile details */ .seo-page, .content-page, .mobile-page { border-bottom: 1px solid #e5e7eb; padding: 16px 0; } .seo-page:last-child, .content-page:last-child, .mobile-page:last-child { border-bottom: none; } .seo-page h4, .content-page h4, .mobile-page h4 { margin: 0 0 8px; font-size: 16px; font-weight: 600; } .seo-metrics, .mobile-metrics { display: flex; align-items: center; gap: 8px; } .issues-count { font-size: 12px; color: var(--color-subtle); background: #f3f4f6; padding: 2px 8px; border-radius: 10px; } .resource-breakdown { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-top: 8px; } .resource-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8fafc; border-radius: 6px; } .resource-item .label { font-size: 12px; color: var(--color-subtle); font-weight: 600; } .resource-item .value { font-size: 14px; font-weight: 700; color: var(--color-text); } /* Certificates Grid */ .certificates-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 24px 0; padding: 24px; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: var(--radius); border: 2px solid #e5e7eb; } .certificate-item { display: flex; flex-direction: column; align-items: center; padding: 16px; background: white; border-radius: var(--radius); box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s ease; } .certificate-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .certificate-badge { margin-bottom: 12px; } .certificate-svg { width: 80px; height: 80px; display: block; } .certificate-info { text-align: center; } .certificate-info h4 { margin: 0 0 8px; font-size: 16px; font-weight: 600; color: var(--color-text); } .certificate-score { font-size: 18px; font-weight: 700; color: var(--primary); margin-bottom: 4px; } .certificate-grade { display: inline-block; background: var(--primary); color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-bottom: 4px; } .certificate-level { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-subtle); font-weight: 500; } /* Performance ratings */ .performance-rating { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 6px; border-radius: 10px; margin-top: 4px; } .performance-rating.good { background: #d1fae5; color: #065f46; } .performance-rating.needs-improvement { background: #fef3c7; color: #92400e; } .performance-rating.poor { background: #fecaca; color: #991b1b; } .performance-vitals .metric { position: relative; } /* Responsive design */ @media (max-width: 768px) { .certificates-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; padding: 16px; } .certificate-svg { width: 60px; height: 60px; } } /* Responsive design */ @media (max-width: 768px) { .container { padding: 16px; } .card { padding: 16px; } .grid { grid-template-columns: 1fr; } .nav-links { padding: 12px 0; } .metrics { flex-direction: column; gap: 8px; } .performance-row { flex-direction: column; align-items: flex-start; gap: 8px; } .seo-metrics, .mobile-metrics { flex-direction: column; align-items: flex-start; } } /* Print styles */ @media print { .navigation { display: none; } .main-content { padding-top: 0; } .card { break-inside: avoid; } .issues-list { max-height: none; overflow: visible; } } `; } renderReportTitle(data) { const domain = this.getDomain(data); const generatedDate = new Date(data.metadata.timestamp).toLocaleString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const duration = Math.round(data.metadata.duration / 1000); const successRate = data.summary.totalPages > 0 ? Math.round((data.summary.passedPages / data.summary.testedPages) * 100) : 0; return ` <div class="report-title"> <h1>AuditMySite Report</h1> <p style="font-size: 1.2rem; color: var(--color-subtle); margin: 8px 0 24px;">Website Accessibility & Performance Analysis</p> <div class="report-meta"> <div class="report-meta-item"> <div class="report-meta-label">Analyzed Domain</div> <div class="report-meta-value">${this.escape(domain)}</div> </div> <div class="report-meta-item"> <div class="report-meta-label">Generated</div> <div class="report-meta-value">${generatedDate}</div> </div> <div class="report-meta-item"> <div class="report-meta-label">Analysis Duration</div> <div class="report-meta-value">${duration}s</div> </div> <div class="report-meta-item"> <div class="report-meta-label">Success Rate</div> <div class="report-meta-value">${successRate}%</div> </div> <div class="report-meta-item"> <div class="report-meta-label">Tool Version</div> <div class="report-meta-value">v${this.escape(data.metadata.toolVersion)}</div> </div> <div class="report-meta-item"> <div class="report-meta-label">Pages Tested</div> <div class="report-meta-value">${data.summary.testedPages}/${data.summary.totalPages}</div> </div> </div> </div> `; } renderSummary(data) { const s = data.summary; const certificates = this.getCertificatesByCategory(data); return ` <div id="summary" class="card summary-content"> <h2>Summary</h2> <!-- Certificates by Category --> ${this.renderCertificatesGrid(certificates)} <div class="grid"> <div class="metric"> <div class="value">${s.testedPages}/${s.totalPages}</div> <div class="label">Pages Tested</div> </div> <div class="metric"> <div class="value">${s.passedPages}</div> <div class="label">Passed</div> </div> <div class="metric"> <div class="value">${s.failedPages}</div> <div class="label">Failed</div> </div> <div class="metric"> <div class="value">${s.crashedPages}</div> <div class="label">Crashed</div> </div> <div class="metric"> <div class="value">${s.totalErrors}</div> <div class="label">Total Errors</div> </div> <div class="metric"> <div class="value">${s.totalWarnings}</div> <div class="label">Total Warnings</div> </div> </div> </div> `; } renderPages(data) { const rows = data.pages.map(p => { const statusClass = p.status === 'passed' ? 'status-passed' : (p.status === 'failed' ? 'status-failed' : 'status-crashed'); const statusText = p.status.toUpperCase(); return ` <tr> <td><div><strong>${this.escape(p.title || p.url)}</strong><br/><small>${this.escape(p.url)}</small></div></td> <td class="page-status ${statusClass}">${statusText}</td> <td>${p.accessibility.errors.length}</td> <td>${p.accessibility.warnings.length}</td> <td>${p.accessibility.score}</td> <td>${p.performance ? p.performance.coreWebVitals.largestContentfulPaint : '—'}</td> <td>${Math.round(p.duration)}ms</td> </tr> `; }).join(''); return ` <div class="card"> <h2>Pages</h2> <div style="overflow-x:auto"> <table> <thead> <tr> <th>Page</th> <th>Status</th> <th>Errors</th> <th>Warnings</th> <th>A11y Score</th> <th>LCP</th> <th>Duration</th> </tr> </thead> <tbody> ${rows} </tbody> </table> </div> </div> `; } renderFooter(data) { return ` <div class="card" style="text-align:center; color:#6b7280;"> Generated by AuditMySite • Duration: ${Math.round(data.metadata.duration / 1000)}s </div> `; } renderNavigation() { return ` <nav class="navigation"> <div class="container"> <div class="nav-links"> <a href="#summary" class="nav-link active">Summary</a> <a href="#accessibility" class="nav-link">Accessibility</a> <a href="#performance" class="nav-link">Performance</a> <a href="#seo" class="nav-link">SEO</a> <a href="#content-weight" class="nav-link">Content Weight</a> <a href="#mobile" class="nav-link">Mobile</a> <a href="#issues" class="nav-link">Issues</a> </div> </div> </nav> `; } renderAccessibilitySection(data) { const issues = this.collectAccessibilityIssues(data); const totalErrors = issues.filter(i => i.type === 'error').length; const totalWarnings = issues.filter(i => i.type === 'warning').length; return ` <section id="accessibility" class="card"> <h2>Accessibility</h2> <div class="grid"> <div class="metric"> <div class="value error">${totalErrors}</div> <div class="label">Errors</div> </div> <div class="metric"> <div class="value warning">${totalWarnings}</div> <div class="label">Warnings</div> </div> <div class="metric"> <div class="value">${this.getOverallAccessibilityScore(data)}</div> <div class="label">Overall Score</div> </div> </div> ${this.renderAccessibilityIssues(issues)} </section> `; } renderPerformanceSection(data) { const performancePages = data.pages.filter(p => p.performance); if (performancePages.length === 0) { return ` <section id="performance" class="card"> <h2>Performance</h2> <p class="no-data">No performance data available.</p> </section> `; } // Core Web Vitals averages const avgLCP = this.calculateAverage(performancePages.map(p => p.performance.coreWebVitals.largestContentfulPaint || 0)); const avgINP = this.calculateAverage(performancePages.map(p => p.performance.coreWebVitals.interactionToNextPaint || 0)); const avgCLS = this.calculateAverage(performancePages.map(p => p.performance.coreWebVitals.cumulativeLayoutShift || 0)); const avgSpeed = this.calculateAverage(performancePages.map(p => p.performance.score || 0)); // Additional Core Web Vitals metrics const avgFCP = this.calculateAverage(performancePages.map(p => p.performance.coreWebVitals.firstContentfulPaint || 0)); const avgTTFB = this.calculateAverage(performancePages.map(p => p.performance.coreWebVitals.timeToFirstByte || 0)); // Note: TBT and Speed Index not available in current type definition - using demo values const avgTBT = 0; // Demo: 0ms (excellent) const avgSI = 1900; // Demo: 1.9s based on your example return ` <section id="performance" class="card"> <h2>Performance</h2> <!-- Core Web Vitals Grid --> <h3>Core Web Vitals</h3> <div class="grid performance-vitals"> <div class="metric"> <div class="value">${this.formatPerformanceTime(avgFCP)}</div> <div class="label">First Contentful Paint</div> <div class="performance-rating ${this.getPerformanceRatingClass(avgFCP, 'FCP')}">${this.getPerformanceRating(avgFCP, 'FCP')}</div> </div> <div class="metric"> <div class="value">${this.formatPerformanceTime(avgLCP)}</div> <div class="label">Largest Contentful Paint</div> <div class="performance-rating ${this.getPerformanceRatingClass(avgLCP, 'LCP')}">${this.getPerformanceRating(avgLCP, 'LCP')}</div> </div> <div class="metric"> <div class="value">${Math.round(avgTBT)}ms</div> <div class="label">Total Blocking Time</div> <div class="performance-rating ${this.getPerformanceRatingClass(avgTBT, 'TBT')}">${this.getPerformanceRating(avgTBT, 'TBT')}</div> </div> <div class="metric"> <div class="value">${avgCLS.toFixed(3)}</div> <div class="label">Cumulative Layout Shift</div> <div class="performance-rating ${this.getPerformanceRatingClass(avgCLS, 'CLS')}">${this.getPerformanceRating(avgCLS, 'CLS')}</div> </div> <div class="metric"> <div class="value">${this.formatPerformanceTime(avgSI)}</div> <div class="label">Speed Index</div> <div class="performance-rating ${this.getPerformanceRatingClass(avgSI, 'SI')}">${this.getPerformanceRating(avgSI, 'SI')}</div> </div> <div class="metric"> <div class="value">${Math.round(avgSpeed)}</div> <div class="label">Performance Score</div> <div class="performance-rating ${this.getScoreRatingClass(avgSpeed)}">${this.getScoreRating(avgSpeed)}</div> </div> </div> ${this.renderPerformanceDetails(performancePages)} </section> `; } renderSeoSection(data) { const seoPages = data.pages.filter(p => p.seo); if (seoPages.length === 0) { return ` <section id="seo" class="card"> <h2>SEO</h2> <p class="no-data">No SEO data available.</p> </section> `; } const avgScore = this.calculateAverage(seoPages.map(p => p.seo.score || 0)); const issuesCount = seoPages.reduce((sum, p) => sum + (p.seo.issues?.length || 0), 0); return ` <section id="seo" class="card"> <h2>SEO</h2> <div class="grid"> <div class="metric"> <div class="value">${Math.round(avgScore)}</div> <div class="label">Average Score</div> </div> <div class="metric"> <div class="value">${issuesCount}</div> <div class="label">Total Issues</div> </div> <div class="metric"> <div class="value">${seoPages.length}</div> <div class="label">Pages Analyzed</div> </div> </div> ${this.renderSeoDetails(seoPages)} </section> `; } renderContentWeightSection(data) { const contentPages = data.pages.filter(p => p.contentWeight); if (contentPages.length === 0) { return ` <section id="content-weight" class="card"> <h2>Content Weight</h2> <p class="no-data">No content weight data available.</p> </section> `; } const avgTotal = this.calculateAverage(contentPages.map(p => (p.contentWeight.totalSize || 0) / 1024)); // KB const avgImages = this.calculateAverage(contentPages.map(p => (p.contentWeight.resources.images.size || 0) / 1024)); // KB const avgJS = this.calculateAverage(contentPages.map(p => (p.contentWeight.resources.javascript.size || 0) / 1024)); // KB return ` <section id="content-weight" class="card"> <h2>Content Weight</h2> <div class="grid"> <div class="metric"> <div class="value">${avgTotal.toFixed(1)} KB</div> <div class="label">Avg Total Size</div> </div> <div class="metric"> <div class="value">${avgImages.toFixed(1)} KB</div> <div class="label">Avg Images</div> </div> <div class="metric"> <div class="value">${avgJS.toFixed(1)} KB</div> <div class="label">Avg Scripts</div> </div> </div> ${this.renderContentWeightDetails(contentPages)} </section> `; } renderMobileFriendlinessSection(data) { // Check for mobile friendliness data in the pages const mobilePages = data.pages.filter(p => p.mobileFriendliness); if (mobilePages.length === 0) { return ` <section id="mobile" class="card"> <h2>Mobile Friendliness</h2> <p class="no-data">No mobile friendliness data available.</p> </section> `; } const avgScore = this.calculateAverage(mobilePages.map(p => p.mobileFriendliness.overallScore || 0)); const recommendationsCount = mobilePages.reduce((sum, p) => sum + (p.mobileFriendliness.recommendations?.length || 0), 0); return ` <section id="mobile" class="card"> <h2>Mobile Friendliness</h2> <div class="grid"> <div class="metric"> <div class="value">${Math.round(avgScore)}</div> <div class="label">Average Score</div> </div> <div class="metric"> <div class="value">${recommendationsCount}</div> <div class="label">Total Recommendations</div> </div> <div class="metric"> <div class="value">${mobilePages.length}</div> <div class="label">Pages Analyzed</div> </div> </div> ${this.renderMobileDetails(mobilePages)} </section> `; // TODO: Implement mobile friendliness analysis integration // const mobilePages = data.pages.filter(p => p.accessibility.mobileFriendliness); // if (mobilePages.length === 0) { // return ` // <section id="mobile" class="card"> // <h2>📱 Mobile Friendliness</h2> // <p class="no-data">No mobile friendliness data available.</p> // </section> // `; // } // const avgScore = this.calculateAverage(mobilePages.map(p => p.accessibility.mobileFriendliness!.overallScore || 0)); // const issuesCount = mobilePages.reduce((sum, p) => sum + (p.accessibility.mobileFriendliness!.recommendations?.length || 0), 0); // return ` // <section id="mobile" class="card"> // <h2>📱 Mobile Friendliness</h2> // <div class="grid"> // <div class="metric"> // <div class="value">${Math.round(avgScore)}</div> // <div class="label">Average Score</div> // </div> // <div class="metric"> // <div class="value">${issuesCount}</div> // <div class="label">Total Issues</div> // </div> // <div class="metric"> // <div class="value">${mobilePages.length}</div> // <div class="label">Pages Analyzed</div> // </div> // </div> // ${this.renderMobileDetails(mobilePages)} // </section> // `; } renderDetailedIssuesSection(data) { const allIssues = this.collectAllIssues(data); return ` <section id="issues" class="card"> <h2>Detailed Issues</h2> <div class="issues-filter"> <button class="filter-btn active" data-type="all">All (${allIssues.length})</button> <button class="filter-btn" data-type="error">Errors (${allIssues.filter(i => i.severity === 'error').length})</button> <button class="filter-btn" data-type="warning">Warnings (${allIssues.filter(i => i.severity === 'warning').length})</button> </div> <div class="issues-list"> ${allIssues.map(issue => this.renderIssueItem(issue)).join('')} </div> </section> `; } renderIssueItem(issue) { const severityClass = issue.severity === 'error' ? 'error' : 'warning'; return ` <div class="issue-item" data-type="${issue.severity}"> <div class="issue-header"> <span class="badge ${severityClass}">${issue.severity.toUpperCase()}</span> <span class="issue-type">${this.escape(issue.type)}</span> <span class="issue-page">${this.escape(issue.page)}</span> </div> <div class="issue-content"> <h4>${this.escape(issue.message)}</h4> ${issue.context ? `<div class="issue-context">${this.escape(issue.context)}</div>` : ''} ${issue.selector ? `<div class="issue-selector">Element: <code>${this.escape(issue.selector)}</code></div>` : ''} </div> </div> `; } // Helper methods getDomain(data) { try { return new URL(data.metadata.sitemapUrl).hostname; } catch { return 'Unknown'; } } getOverallAccessibilityScore(data) { const scores = data.pages.map(p => p.accessibility.score).filter(s => s !== undefined); if (scores.length === 0) return '—'; return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length).toString(); } calculateAverage(values) { if (values.length === 0) return 0; const validValues = values.filter(v => !isNaN(v) && v !== undefined && v !== null); if (validValues.length === 0) return 0; return validValues.reduce((a, b) => a + b, 0) / validValues.length; } collectAccessibilityIssues(data) { const issues = []; data.pages.forEach(page => { page.accessibility.errors.forEach(error => { issues.push({ type: 'error', severity: 'error', page: page.url, message: error.message, context: error.context, selector: error.selector }); }); page.accessibility.warnings.forEach(warning => { issues.push({ type: 'warning', severity: 'warning', page: page.url, message: warning.message, context: warning.context, selector: warning.selector }); }); }); return issues; } collectAllIssues(data) { const issues = []; // Accessibility issues issues.push(...this.collectAccessibilityIssues(data)); // Performance issues data.pages.forEach(page => { if (page.performance?.issues) { page.performance.issues.forEach(issue => { issues.push({ type: 'performance', severity: issue.severity || 'warning', page: page.url, message: issue.message, context: issue.message }); }); } }); // SEO issues data.pages.forEach(page => { if (page.seo?.issues) { page.seo.issues.forEach(issue => { issues.push({ type: 'seo', severity: issue.severity || 'warning', page: page.url, message: issue.message, context: issue.message }); }); } }); return issues; } renderAccessibilityIssues(issues) { if (issues.length === 0) { return '<div class="no-issues">No accessibility issues found</div>'; } return ` <div class="issues-summary"> <h3>Top Accessibility Issues</h3> ${issues.slice(0, 10).map(issue => ` <div class="issue-summary"> <span class="badge ${issue.type}">${issue.type.toUpperCase()}</span> ${this.escape(issue.message)} </div> `).join('')} </div> `; } renderPerformanceDetails(pages) { return ` <div class="performance-details"> <h3>Core Web Vitals by Page</h3> <div class="performance-table"> ${pages.map(page => ` <div class="performance-row"> <div class="page-name">${this.escape(page.title || page.url)}</div> <div class="metrics"> <span class="metric-item">LCP: ${this.formatNumber(page.performance.coreWebVitals.largestContentfulPaint || 0, 0)}ms</span> <span class="metric-item">INP: ${this.formatNumber(page.performance.coreWebVitals.interactionToNextPaint || 0, 0)}ms</span> <span class="metric-item">CLS: ${this.formatNumber(page.performance.coreWebVitals.cumulativeLayoutShift, 3)}</span> </div> </div> `).join('')} </div> </div> `; } renderSeoDetails(pages) { return ` <div class="seo-details"> <h3>SEO Analysis by Page</h3> ${pages.map(page => ` <div class="seo-page"> <h4>${this.escape(page.title || page.url)}</h4> <div class="seo-metrics"> <span class="badge ${this.getScoreClass(page.seo.score)}">Score: ${page.seo.score || 0}</span> ${page.seo.issues ? `<span class="issues-count">${page.seo.issues.length} issues</span>` : ''} </div> </div> `).join('')} </div> `; } renderContentWeightDetails(pages) { return ` <div class="content-details"> <h3>Resource Breakdown by Page</h3> ${pages.map(page => ` <div class="content-page"> <h4>${this.escape(page.title || page.url)}</h4> <div class="resource-breakdown"> <div class="resource-item"> <span class="label">Total:</span> <span class="value">${((page.contentWeight.totalSize || 0) / 1024).toFixed(1)} KB</span> </div> <div class="resource-item"> <span class="label">Images:</span> <span class="value">${((page.contentWeight.resources.images.size || 0) / 1024).toFixed(1)} KB</span> </div> <div class="resource-item"> <span class="label">Scripts:</span> <span class="value">${((page.contentWeight.resources.javascript.size || 0) / 1024).toFixed(1)} KB</span> </div> </div> </div> `).join('')} </div> `; } renderMobileDetails(pages) { return ` <div class="mobile-details"> <h3>Mobile Analysis by Page</h3> ${pages.map(page => ` <div class="mobile-page"> <h4>${this.escape(page.title || page.url)}</h4> <div class="mobile-metrics"> <span class="badge ${this.getScoreClass(page.mobileFriendliness.overallScore)}">Score: ${page.mobileFriendliness.overallScore || 0}</span> ${page.mobileFriendliness.recommendations ? `<span class="issues-count">${page.mobileFriendliness.recommendations.length} recommendations</span>` : ''} </div> </div> `).join('')} </div> `; // TODO: Implement mobile details rendering // return ` // <div class="mobile-details"> // <h3>Mobile Analysis by Page</h3> // ${pages.map(page => ` // <div class="mobile-page"> // <h4>${this.escape(page.title || page.url)}</h4> // <div class="mobile-metrics"> // <span class="badge ${this.getScoreClass(page.accessibility.mobileFriendliness!.overallScore)}">Score: ${page.accessibility.mobileFriendliness!.overallScore || 0}</span> // ${page.accessibility.mobileFriendliness!.recommendations ? `<span class="issues-count">${page.accessibility.mobileFriendliness!.recommendations.length} recommendations</span>` : ''} // </div> // </div> // `).join('')} // </div> // `; } getScoreClass(score) { if (score >= 90) return 'success'; if (score >= 70) return 'warning'; return 'error'; } generateJavaScript() { return ` // Navigation smooth scrolling document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Update active nav link document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); this.classList.add('active'); }); }); // Issues filtering document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', function() { const filterType = this.getAttribute('data-type'); // Update active filter document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); // Filter issues document.querySelectorAll('.issue-item').forEach(item => { if (filterType === 'all' || item.getAttribute('data-type') === filterType) { item.style.display = 'block'; } else { item.style.display = 'none'; } }); }); }); // Scroll spy for navigation const observerOptions = { root: null, rootMargin: '-20% 0px -80% 0px', threshold: 0 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const id = entry.target.id; document.querySelectorAll('.nav-link').forEach(link => { link.classList.toggle('active', link.getAttribute('href') === '#' + id); }); } }); }, observerOptions); document.querySelectorAll('section[id]').forEach(section => { observer.observe(section); }); `; } formatNumber(value, decimals = 2) { if (value === null || value === undefined) return '0'; if (typeof value === 'object' && value.value !== undefined) { return Number(value.value).toFixed(decimals); } if (typeof value === 'number') { return value.toFixed(decimals); } const num = Number(value); return isNaN(num) ? '0' : num.toFixed(decimals); } escape(s) { return s .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;'); } getCertificatesByCategory(data) { const pages = data.pages.filter(p => p.status !== 'crashed'); const certificates = []; if (pages.length === 0) { return [ { category: 'Accessibility', level: 'NEEDS_IMPROVEMENT', score: 0, description: 'No data available', svgPath: 'NEEDS_IMPROVEMENT.svg' }, { category: 'Performance', level: 'NEEDS_IMPROVEMENT', score: 0, description: 'No data available', svgPath: 'NEEDS_IMPROVEMENT.svg' }, { category: 'SEO', level: 'NEEDS_IMPROVEMENT', score: 0, description: 'No data available', svgPath: 'NEEDS_IMPROVEMENT.svg' }, { category: 'Mobile', level: 'NEEDS_IMPROVEMENT', score: 0, description: 'No data available', svgPath: 'NEEDS_IMPROVEMENT.svg' } ]; } // Accessibility Certificate const accessibilityScore = this.calculateAverage(pages.map(p => p.accessibility.score || 0)); certificates.push({ category: 'Accessibility', ...this.getCertificateLevel(accessibilityScore), score: Math.round(accessibilityScore) }); // Performance Certificate const performanceScore = this.calculateAverage(pages.filter(p => p.performance?.score).map(p => p.performance.score)); certificates.push({ category: 'Performance', ...this.getCertificateLevel(performanceScore || 0), score: Math.round(performanceScore || 0) }); // SEO Certificate const seoScore = this.calculateAverage(pages.filter(p => p.seo?.score).map(p => p.seo.score)); certificates.push({ category: 'SEO', ...this.getCertificateLevel(seoScore || 0), score: Math.round(seoScore || 0) }); // Mobile Certificate const mobileScore = this.calculateAverage(pages.filter(p => p.mobileFriendliness?.overallScore).map(p => p.mobileFriendliness.overallScore)); certificates.push({ category: 'Mobile', ...this.getCertificateLevel(mobileScore || 0), score: Math.round(mobileScore || 0) }); return certificates; } getCertificateLevel(score) { if (score >= 95) { return { level: 'PLATINUM', description: 'Exceptional quality', svgPath: 'PLATINUM.svg' }; } else if (score >= 85) { return { level: 'GOLD', description: 'Excellent quality', svgPath: 'GOLD.svg' }; } else if (score >= 70) { return { level: 'SILVER', description: 'Good quality', svgPath: 'SILVER.svg' }; } else if (score >= 55) { return { level: 'BRONZE', description: 'Basic quality', svgPath: 'BRONZE.svg' }; } else { return { level: 'NEEDS_IMPROVEMENT', description: 'Requires attention', svgPath: 'NEEDS_IMPROVEMENT.svg' }; } } renderCertificatesGrid(certificates) { return ` <div class="certificates-grid"> ${certificates.map(cert => ` <div class="certificate-item"> <div class="certificate-badge"> <img src="src/assets/certificates/${cert.svgPath}" alt="${cert.level} Certificate" class="certificate-svg" /> </div> <div class="certificate-info"> <h4>${cert.category}</h4> <div class="certificate-score">${cert.score}/100</div> <div class="certificate-grade">${this.getLetterGrade(cert.score)}</div> <div class="certificate-level">${cert.level}</div> </div> </div> `).join('')} </div> `; } getLetterGrade(score) { if (score >= 95) return 'A+'; if (score >= 90) return 'A'; if (score >= 85) return 'A-'; if (score >= 80) return 'B+'; if (score >= 75) return 'B';