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,174 lines (1,008 loc) 38 kB
function getComprehensiveHtmlTemplate(data) { const { domain, summary, results, allDetailedIssues, groupedIssues, timestamp, successRate, errorCount, warningCount, noticeCount } = data; return `<!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Accessibility Test Report - ${domain}</title> <meta name="description" content="Comprehensive accessibility test report generated by auditmysite"> <!-- Favicon --> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='%232563eb'/><text x='50' y='65' text-anchor='middle' fill='white' font-size='40' font-weight='bold'>A</text></svg>"> <!-- CSS --> <style> /* CSS Variables for Theming */ :root { --primary-color: #2563eb; --secondary-color: #64748b; --success-color: #10b981; --warning-color: #f59e0b; --error-color: #ef4444; --background-color: #ffffff; --surface-color: #f8fafc; --text-primary: #1e293b; --text-secondary: #64748b; --border-color: #e2e8f0; --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); } /* Dark Mode */ @media (prefers-color-scheme: dark) { :root { --background-color: #0f172a; --surface-color: #1e293b; --text-primary: #f1f5f9; --text-secondary: #94a3b8; --border-color: #334155; } } /* Reset and Base Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; color: var(--text-primary); background-color: var(--background-color); transition: background-color 0.3s ease; } /* Header */ .report-header { background: #f8fafc; color: #1e293b; padding: 1rem 0; box-shadow: var(--shadow-lg); position: sticky; top: 0; z-index: 100; border-bottom: 1px solid #e2e8f0; height: 80px; } .report-nav { max-width: 1200px; margin: 0 auto; padding: 0 1rem; display: flex; justify-content: flex-start; align-items: center; height: 100%; } .logo { display: flex; align-items: center; gap: 0.75rem; font-size: 1.5rem; font-weight: 600; height: 100%; } .logo svg { height: calc(100% - 20px); width: auto; flex-shrink: 0; max-height: 60px; } .logo span { color: #1e293b; font-weight: 600; font-size: 1.25rem; } /* Filter Section */ .filter-section { background: var(--surface-color); border-bottom: 1px solid var(--border-color); padding: 1rem 0; box-shadow: var(--shadow); } .filter-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } .filter-badges { display: flex; list-style: none; gap: 1rem; flex-wrap: wrap; justify-content: center; } .filter-badge { background: white; color: var(--text-primary); border: 2px solid #e2e8f0; padding: 0.5rem 1rem; border-radius: 2rem; cursor: pointer; transition: all 0.2s ease; font-size: 0.875rem; font-weight: 500; user-select: none; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } .filter-badge:hover { background: var(--primary-color); color: white; border-color: var(--primary-color); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .filter-badge.active { background: var(--primary-color); color: white; border-color: var(--primary-color); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .filter-badge.inactive { opacity: 0.6; background: #f8fafc; } /* Main Content */ .main-content { max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; } /* Dashboard */ .dashboard { margin-bottom: 3rem; } .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } .kpi-card { background: var(--surface-color); padding: 1.5rem; border-radius: 0.75rem; box-shadow: var(--shadow); border: 1px solid var(--border-color); transition: transform 0.2s ease, box-shadow 0.2s ease; } .kpi-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); } .kpi-card h3 { color: var(--text-secondary); font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } .kpi-value { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; } .kpi-trend { font-size: 0.875rem; font-weight: 500; } .kpi-trend.positive { color: var(--success-color); } .kpi-trend.negative { color: var(--error-color); } /* Table Styles */ .table-container { background: var(--surface-color); border-radius: 0.75rem; box-shadow: var(--shadow); border: 1px solid var(--border-color); margin-bottom: 2rem; overflow: hidden; } .table-header { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid var(--border-color); background: var(--background-color); } .table-header h3 { font-size: 1.25rem; font-weight: 600; color: var(--text-primary); } .table-wrapper { overflow-x: auto; } .data-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } .data-table th { background: var(--background-color); padding: 1rem; text-align: left; font-weight: 600; color: var(--text-primary); border-bottom: 2px solid var(--border-color); position: sticky; top: 0; z-index: 10; } .data-table td { padding: 1rem; border-bottom: 1px solid var(--border-color); color: var(--text-primary); } .data-table tr:hover { background: var(--background-color); } /* Report Section styling */ .report-section { margin-bottom: 3rem; transition: all 0.3s ease; } .report-section.hidden { display: none; } .section-header { margin-bottom: 2rem; text-align: left; } .section-content { background: var(--surface-color); border-radius: 0.75rem; overflow: hidden; border: 1px solid var(--border-color); } .section-title { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem; } .section-description { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 1.5rem; font-style: italic; } /* Page info styling */ .page-info { line-height: 1.4; } .page-info strong { color: var(--text-primary); font-size: 0.95rem; } .page-title { color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.25rem; } .page-url { color: var(--text-secondary); font-size: 0.75rem; opacity: 0.7; margin-top: 0.125rem; } /* Detailed Issues Styles */ .detailed-issues-container { padding: 1.5rem; } .issues-summary { margin-bottom: 2rem; } .issues-summary h3 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: var(--text-primary); } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .summary-card { background: var(--background-color); padding: 1rem; border-radius: 0.5rem; border: 2px solid var(--border-color); text-align: center; } .summary-card.error { border-color: var(--error-color); background: rgba(239, 68, 68, 0.05); } .summary-card.warning { border-color: var(--warning-color); background: rgba(245, 158, 11, 0.05); } .summary-card.notice { border-color: var(--success-color); background: rgba(16, 185, 129, 0.05); } .summary-card.total { border-color: var(--primary-color); background: rgba(37, 99, 235, 0.05); } .summary-number { font-size: 1.75rem; font-weight: bold; margin-bottom: 0.25rem; } .summary-card.error .summary-number { color: var(--error-color); } .summary-card.warning .summary-number { color: var(--warning-color); } .summary-card.notice .summary-number { color: var(--success-color); } .summary-card.total .summary-number { color: var(--primary-color); } .summary-label { font-size: 0.875rem; color: var(--text-secondary); font-weight: 500; } .category-section { margin-bottom: 2rem; border: 1px solid var(--border-color); border-radius: 0.5rem; background: var(--background-color); } .category-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid var(--border-color); background: var(--surface-color); } .category-title { font-size: 1.1rem; font-weight: 600; color: var(--text-primary); } .issue-count { color: var(--text-secondary); font-weight: normal; } .copy-category-btn { background: var(--primary-color); color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; transition: background-color 0.2s ease; } .copy-category-btn:hover { background: #1d4ed8; } .page-issues-section { border-bottom: 1px solid var(--border-color); } .page-issues-section:last-child { border-bottom: none; } .page-header { padding: 1rem; background: var(--surface-color); border-bottom: 1px solid var(--border-color); } .page-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 0.25rem; } .page-meta { display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; } .page-url { color: var(--text-secondary); } .page-issue-count { color: var(--text-secondary); font-weight: 500; } .issues-list { padding: 0; } .issue-item { padding: 1rem; border-bottom: 1px solid var(--border-color); background: var(--background-color); } .issue-item:last-child { border-bottom: none; } .issue-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } .issue-type-badge { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; color: white; } .issue-type-badge.error { background: var(--error-color); } .issue-type-badge.warning { background: var(--warning-color); } .issue-type-badge.notice { background: var(--success-color); } .issue-source { font-size: 0.75rem; color: var(--text-secondary); font-weight: 500; } .copy-issue-btn { background: transparent; border: 1px solid var(--border-color); padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem; color: var(--text-secondary); transition: all 0.2s ease; } .copy-issue-btn:hover { background: var(--surface-color); border-color: var(--primary-color); } .issue-content { font-size: 0.875rem; line-height: 1.5; } .issue-message { color: var(--text-primary); margin-bottom: 0.5rem; font-weight: 500; } .issue-code { color: var(--text-secondary); margin-bottom: 0.5rem; } .issue-selector { color: var(--text-secondary); margin-bottom: 0.5rem; } .issue-selector code { background: var(--surface-color); padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-family: monospace; font-size: 0.8em; } .issue-context { color: var(--text-secondary); margin-bottom: 0.5rem; } .issue-context code { background: var(--surface-color); padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-family: monospace; font-size: 0.8em; } .issue-help { color: var(--text-primary); margin-bottom: 0.5rem; } .issue-help-url { margin-bottom: 0.5rem; } .issue-help-url a { color: var(--primary-color); text-decoration: none; } .issue-help-url a:hover { text-decoration: underline; } .issue-ai-format { display: none; } /* Toast styles */ .toast { position: fixed; top: 2rem; right: 2rem; background: var(--success-color); color: white; padding: 1rem 1.5rem; border-radius: 0.5rem; box-shadow: var(--shadow-lg); opacity: 0; transform: translateY(-1rem); transition: all 0.3s ease; z-index: 1000; } .toast.show { opacity: 1; transform: translateY(0); } /* Responsive Design */ @media (max-width: 768px) { .table-header { flex-direction: column; gap: 1rem; align-items: flex-start; } .data-table { font-size: 0.75rem; } .data-table th, .data-table td { padding: 0.75rem 0.5rem; } .kpi-grid { grid-template-columns: repeat(2, 1fr); } .summary-grid { grid-template-columns: repeat(2, 1fr); } .category-header { flex-direction: column; gap: 1rem; align-items: flex-start; } .page-meta { flex-direction: column; align-items: flex-start; gap: 0.5rem; } .issue-header { flex-wrap: wrap; gap: 0.5rem; } } </style> </head> <body> <header class="report-header"> <nav class="report-nav"> <div class="logo"> <span>🚀 AuditMySite</span> </div> </nav> <div class="filter-section"> <div class="filter-container"> <div class="filter-badges"> <span class="filter-badge active" data-section="summary">Summary</span> <span class="filter-badge active" data-section="accessibility">Accessibility</span> <span class="filter-badge" data-section="performance">Performance</span> <span class="filter-badge" data-section="seo">SEO</span> <span class="filter-badge" data-section="detailed-issues">Detailed Issues</span> </div> </div> </div> </header> <main class="main-content"> <!-- Dashboard Section --> <section id="summary" class="dashboard"> <h2 class="section-title">Test Summary</h2> <div class="kpi-grid"> <div class="kpi-card"> <h3>Success Rate</h3> <div class="kpi-value">${successRate}%</div> <div class="kpi-trend ${successRate > 0 ? 'positive' : 'negative'}">${successRate > 0 ? 'Good' : 'Needs Work'}</div> </div> <div class="kpi-card"> <h3>Pages Tested</h3> <div class="kpi-value">${summary.testedPages}/${summary.totalPages || summary.testedPages}</div> <div class="kpi-trend">Total Pages</div> </div> <div class="kpi-card"> <h3>Total Issues</h3> <div class="kpi-value">${allDetailedIssues.length}</div> <div class="kpi-trend ${allDetailedIssues.length > 0 ? 'negative' : 'positive'}">Pa11y Issues</div> </div> <div class="kpi-card"> <h3>Test Duration</h3> <div class="kpi-value">${Math.round(summary.totalDuration / 1000) || 0}s</div> <div class="kpi-trend">Time Taken</div> </div> </div> </section> <!-- Accessibility Section --> <section id="accessibility" class="report-section"> <div class="section-header"> <h2 class="section-title">Accessibility Issues</h2> <p class="section-description">Web accessibility compliance and WCAG violations analysis</p> </div> <div class="section-content"> <div class="table-container"> <div class="table-header"> <h3>Accessibility Results by Page</h3> </div> <div class="table-wrapper"> <table class="data-table"> <thead> <tr> <th>Page</th> <th>Errors</th> <th>Warnings</th> <th>Pa11y Score</th> </tr> </thead> <tbody> ${results.map(page => ` <tr> <td>${escapeHtml(page.url)}</td> <td>${page.errors || 0}</td> <td>${page.warnings || 0}</td> <td>${page.pa11yScore || 0}/100</td> </tr> `).join('')} </tbody> </table> </div> </div> </div> </section> <!-- Performance Section --> <section id="performance" class="report-section"> <div class="section-header"> <h2 class="section-title">Performance Metrics</h2> <p class="section-description">Web page performance metrics and loading times</p> </div> <div class="section-content"> <div class="table-container"> <div class="table-header"> <h3>Performance Results by Page</h3> </div> <div class="table-wrapper"> <table class="data-table"> <thead> <tr> <th>Page</th> <th>Performance Score</th> <th>LCP</th> <th>CLS</th> <th>INP</th> </tr> </thead> <tbody> ${results.map(page => ` <tr> <td>${escapeHtml(page.url)}</td> <td>${page.performance?.performanceScore || 'N/A'}</td> <td>${page.performance?.metrics?.largestContentfulPaint || 'N/A'}</td> <td>${page.performance?.metrics?.cumulativeLayoutShift || 'N/A'}</td> <td>${page.performance?.metrics?.interactionToNextPaint || 'N/A'}</td> </tr> `).join('')} </tbody> </table> </div> </div> </div> </section> <!-- SEO Section --> <section id="seo" class="report-section"> <div class="section-header"> <h2 class="section-title">SEO Analysis</h2> <p class="section-description">Search engine optimization analysis and content structure</p> </div> <div class="section-content"> <div class="table-container"> <div class="table-header"> <h3>SEO Results by Page</h3> </div> <div class="table-wrapper"> <table class="data-table"> <thead> <tr> <th>Page & Title</th> <th>SEO Score</th> <th>Headings</th> <th>Images w/o Alt</th> <th>Buttons w/o Label</th> </tr> </thead> <tbody> ${results.map(page => ` <tr> <td> <div class="page-info"> <strong>${getPageName(page.url)}</strong><br> <small class="page-title">${escapeHtml(page.title || 'No title')}</small><br> <small class="page-url">${escapeHtml(page.url)}</small> </div> </td> <td>${page.seo?.seoScore || 'N/A'}</td> <td>${page.issues?.headingsCount || 'N/A'}</td> <td>${page.issues?.imagesWithoutAlt || 'N/A'}</td> <td>${page.issues?.buttonsWithoutLabel || 'N/A'}</td> </tr> `).join('')} </tbody> </table> </div> </div> </div> </section> <!-- Detailed Issues Section --> <section id="detailed-issues" class="report-section"> <div class="section-header"> <h2 class="section-title">Detailed Issues Analysis</h2> <p class="section-description">Comprehensive breakdown of all accessibility issues grouped by category</p> </div> <div class="section-content"> ${generateDetailedIssuesContent({ allDetailedIssues, groupedIssues, errorCount, warningCount, noticeCount })} </div> </section> </main> <!-- Toast Notification --> <div id="toast" class="toast"> <span id="toast-message">Copied to clipboard!</span> </div> <!-- JavaScript for Interactivity --> <script> // Filter Badge System function initFilterSystem() { const badges = document.querySelectorAll('.filter-badge'); const sections = document.querySelectorAll('.report-section, .dashboard'); badges.forEach(badge => { badge.addEventListener('click', function() { const targetSection = this.getAttribute('data-section'); // Toggle badge state this.classList.toggle('active'); // Show/hide corresponding section const section = document.getElementById(targetSection); if (section) { if (this.classList.contains('active')) { section.classList.remove('hidden'); section.style.display = 'block'; } else { section.classList.add('hidden'); section.style.display = 'none'; } } // Update badge appearance based on active state updateBadgeStates(); }); }); // Initialize: Show summary and accessibility by default sections.forEach(function(section) { const sectionId = section.id; const badge = document.querySelector('[data-section="' + sectionId + '"]'); if (sectionId === 'summary' || sectionId === 'accessibility') { section.classList.remove('hidden'); section.style.display = 'block'; if (badge) badge.classList.add('active'); } else { section.classList.add('hidden'); section.style.display = 'none'; if (badge) badge.classList.remove('active'); } }); } function updateBadgeStates() { const badges = document.querySelectorAll('.filter-badge'); const activeBadges = document.querySelectorAll('.filter-badge.active'); badges.forEach(badge => { if (activeBadges.length === 0) { badge.classList.remove('inactive'); } else { if (badge.classList.contains('active')) { badge.classList.remove('inactive'); } else { badge.classList.add('inactive'); } } }); } // Toast Notification function showToast(message) { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toast-message'); toastMessage.textContent = message; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // Copy single issue to clipboard function copyIssue(issueId) { const issueElement = document.getElementById(issueId); if (!issueElement) return; const aiFormatElement = issueElement.querySelector('.issue-ai-format'); if (!aiFormatElement) return; const textarea = document.createElement('textarea'); textarea.value = aiFormatElement.textContent; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); showToast('Issue copied to clipboard!'); } catch (err) { showToast('Failed to copy issue!'); } document.body.removeChild(textarea); } // Copy all issues in a category function copyCategoryIssues(categoryId) { const categoryElement = document.getElementById('category-' + categoryId); if (!categoryElement) return; const issues = categoryElement.querySelectorAll('.issue-ai-format'); let allIssuesText = ''; // Add category header const categoryTitle = categoryElement.querySelector('.category-title'); if (categoryTitle) { allIssuesText += '# ' + categoryTitle.textContent + '\\n\\n'; } // Collect all issues issues.forEach((issueFormat, index) => { allIssuesText += '## Issue ' + (index + 1) + '\\n'; allIssuesText += issueFormat.textContent + '\\n'; }); // Add footer with AI prompt suggestion allIssuesText += '\\n---\\nPlease review these accessibility issues and provide specific code fixes for each one.'; const textarea = document.createElement('textarea'); textarea.value = allIssuesText; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); const categoryName = categoryTitle ? categoryTitle.textContent : 'this category'; showToast('Copied ' + issues.length + ' issues from ' + categoryName + '!'); } catch (err) { showToast('Failed to copy category issues!'); } document.body.removeChild(textarea); } // Initialize everything when DOM is loaded document.addEventListener('DOMContentLoaded', function() { initFilterSystem(); }); </script> </body> </html>`; } function generateDetailedIssuesContent(data) { const { allDetailedIssues, groupedIssues, errorCount, warningCount, noticeCount } = data; if (allDetailedIssues.length === 0) { return ` <div class="detailed-issues-container"> <div class="no-issues"> <h3>🎉 Great! No Issues Found</h3> <p>Your website appears to be free of accessibility issues.</p> </div> </div> `; } let html = '<div class="detailed-issues-container">'; // Issues Summary html += ` <div class="issues-summary"> <h3>Issues Overview</h3> <div class="summary-grid"> <div class="summary-card error"> <div class="summary-number">${errorCount}</div> <div class="summary-label">Errors</div> </div> <div class="summary-card warning"> <div class="summary-number">${warningCount}</div> <div class="summary-label">Warnings</div> </div> <div class="summary-card notice"> <div class="summary-number">${noticeCount}</div> <div class="summary-label">Notices</div> </div> <div class="summary-card total"> <div class="summary-number">${allDetailedIssues.length}</div> <div class="summary-label">Total Issues</div> </div> </div> </div> `; // Generate sections for each category Object.entries(groupedIssues).forEach(([category, issues]) => { html += generateCategorySection(category, issues); }); html += '</div>'; return html; } function generateCategorySection(category, issues) { const sectionId = category.toLowerCase().replace(/[^a-z0-9]/g, '-'); let html = `<div class="category-section" id="category-${sectionId}">`; html += `<div class="category-header">`; html += `<h3 class="category-title">${category} <span class="issue-count">(${issues.length})</span></h3>`; html += `<button class="copy-category-btn" onclick="copyCategoryIssues('${sectionId}')">📋 Copy All Issues</button>`; html += `</div>`; // Group by page within category const pageGroups = {}; issues.forEach(issue => { const pageUrl = issue.pageUrl || 'unknown'; if (!pageGroups[pageUrl]) { pageGroups[pageUrl] = []; } pageGroups[pageUrl].push(issue); }); Object.entries(pageGroups).forEach(([pageUrl, pageIssues]) => { const pageTitle = pageIssues[0]?.pageTitle || 'Unknown Page'; html += generatePageIssuesSection(pageUrl, pageTitle, pageIssues); }); html += '</div>'; return html; } function generatePageIssuesSection(pageUrl, pageTitle, issues) { let html = `<div class="page-issues-section">`; html += `<div class="page-header">`; html += `<h4 class="page-title">${getPageName(pageUrl)}</h4>`; html += `<div class="page-meta">`; html += `<span class="page-url">${pageUrl}</span>`; html += `<span class="page-issue-count">${issues.length} issues</span>`; html += `</div>`; html += `</div>`; html += `<div class="issues-list">`; issues.forEach((issue, index) => { html += generateIssueItem(issue, index); }); html += `</div></div>`; return html; } function generateIssueItem(issue, index) { const severityClass = issue.type || 'info'; const issueId = `issue-${Date.now()}-${index}`; let html = `<div class="issue-item ${severityClass}" id="${issueId}">`; html += `<div class="issue-header">`; html += `<div class="issue-type-badge ${severityClass}">${(issue.type || 'INFO').toUpperCase()}</div>`; html += `<div class="issue-source">${issue.source || 'unknown'}</div>`; html += `<button class="copy-issue-btn" onclick="copyIssue('${issueId}')">📋</button>`; html += `</div>`; html += `<div class="issue-content">`; html += `<div class="issue-message">${escapeHtml(issue.message || '')}</div>`; if (issue.code) { html += `<div class="issue-code"><strong>Code:</strong> ${escapeHtml(issue.code)}</div>`; } if (issue.selector) { html += `<div class="issue-selector"><strong>Element:</strong> <code>${escapeHtml(issue.selector)}</code></div>`; } if (issue.context) { html += `<div class="issue-context"><strong>Context:</strong> <code>${escapeHtml(issue.context)}</code></div>`; } if (issue.help) { html += `<div class="issue-help"><strong>Help:</strong> ${escapeHtml(issue.help)}</div>`; } if (issue.helpUrl) { html += `<div class="issue-help-url"><strong>More Info:</strong> <a href="${escapeHtml(issue.helpUrl)}" target="_blank">${escapeHtml(issue.helpUrl)}</a></div>`; } // AI-friendly format for copying html += `<div class="issue-ai-format" style="display: none;">`; html += `Issue: ${issue.message || ''}\\n`; if (issue.selector) html += `Element: ${issue.selector}\\n`; if (issue.context) html += `HTML Context: ${issue.context}\\n`; if (issue.code) html += `Rule: ${issue.code}\\n`; html += `Page: ${issue.pageUrl || ''}\\n`; html += `Severity: ${issue.type || ''}\\n`; if (issue.help) html += `Fix: ${issue.help}\\n`; html += `---\\n`; html += `</div>`; html += `</div></div>`; return html; } module.exports = { getComprehensiveHtmlTemplate };