#!/usr/bin/env node /** * Lighthouse Audit for All Locales * Runs PageSpeed Insights API tests for all three language versions (de, en, sr) * Documents Core Web Vitals (LCP, CLS, INP) for both mobile and desktop modes */ import https from 'https'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // Disable SSL verification for local testing (network proxy issue) process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const API_KEY = 'AIzaSyA4qo0LfLy_ps-SyTQTQpFrFRjDnAUn_K4'; const BASE_URL = 'https://www.damjan-savic.com'; const LOCALES = ['de', 'en', 'sr']; const STRATEGIES = ['mobile', 'desktop']; const REPORTS_DIR = path.join(__dirname, '..', '.auto-claude', 'specs', '001-suche-nach-optimierungsvorschl-gen', 'lighthouse-reports'); function httpsGet(url) { return new Promise((resolve, reject) => { https.get(url, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(new Error(`JSON Parse Error: ${e.message}`)); } }); }).on('error', reject); }); } async function runPageSpeedTest(url, strategy = 'mobile') { const apiUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&key=${API_KEY}&strategy=${strategy}&category=performance&category=accessibility&category=best-practices&category=seo`; console.log(`\n Testing ${url} (${strategy})...`); console.log(' Please wait, this may take 30-60 seconds...\n'); try { const data = await httpsGet(apiUrl); if (data.error) { throw new Error(`API Error: ${data.error.message}`); } return data; } catch (error) { console.error(` Error testing ${url}:`, error.message); throw error; } } function extractCoreWebVitals(data) { const lighthouse = data.lighthouseResult; const categories = lighthouse.categories; const audits = lighthouse.audits; const fcp = audits['first-contentful-paint']; const lcp = audits['largest-contentful-paint']; const tbt = audits['total-blocking-time']; const cls = audits['cumulative-layout-shift']; const si = audits['speed-index']; const inp = audits['interaction-to-next-paint'] || audits['experimental-interaction-to-next-paint']; // Extract opportunities const opportunities = Object.values(audits) .filter(audit => audit.details?.type === 'opportunity' && audit.details?.overallSavingsMs > 0) .sort((a, b) => (b.details?.overallSavingsMs || 0) - (a.details?.overallSavingsMs || 0)) .map(opp => ({ title: opp.title, description: opp.description, savings_ms: opp.details?.overallSavingsMs || 0, savings_bytes: opp.details?.overallSavingsBytes || 0 })); // Extract diagnostics const diagnostics = Object.values(audits) .filter(audit => audit.details?.type === 'table' && audit.score !== null && audit.score < 0.9) .map(diag => ({ title: diag.title, description: diag.description, score: diag.score })); return { url: lighthouse.finalUrl, fetchTime: lighthouse.fetchTime, userAgent: lighthouse.userAgent, scores: { performance: Math.round(categories.performance.score * 100), accessibility: Math.round(categories.accessibility.score * 100), bestPractices: Math.round(categories['best-practices'].score * 100), seo: Math.round(categories.seo.score * 100) }, coreWebVitals: { FCP: { value: fcp?.numericValue || null, displayValue: fcp?.displayValue || 'N/A', score: fcp?.score || null }, LCP: { value: lcp?.numericValue || null, displayValue: lcp?.displayValue || 'N/A', score: lcp?.score || null }, CLS: { value: cls?.numericValue || null, displayValue: cls?.displayValue || 'N/A', score: cls?.score || null }, TBT: { value: tbt?.numericValue || null, displayValue: tbt?.displayValue || 'N/A', score: tbt?.score || null }, INP: { value: inp?.numericValue || null, displayValue: inp?.displayValue || 'N/A', score: inp?.score || null }, SpeedIndex: { value: si?.numericValue || null, displayValue: si?.displayValue || 'N/A', score: si?.score || null } }, opportunities: opportunities.slice(0, 10), diagnostics: diagnostics.slice(0, 10) }; } function formatTime(ms) { if (ms === null || ms === undefined) return 'N/A'; if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(2)}s`; } function getScoreEmoji(score) { if (score >= 90) return 'green'; if (score >= 50) return 'yellow'; return 'red'; } function generateMarkdownReport(allResults) { const timestamp = new Date().toISOString(); let markdown = `# Lighthouse Audit Report - Core Web Vitals **Generated:** ${timestamp} **Base URL:** ${BASE_URL} **Locales Tested:** ${LOCALES.join(', ')} --- ## Executive Summary This report documents Core Web Vitals (LCP, CLS, INP/TBT) measurements for all three language versions of the portfolio website in both mobile and desktop modes. ### Key Metrics Explained: - **LCP (Largest Contentful Paint):** Measures loading performance. Good: < 2.5s, Needs Improvement: 2.5-4s, Poor: > 4s - **CLS (Cumulative Layout Shift):** Measures visual stability. Good: < 0.1, Needs Improvement: 0.1-0.25, Poor: > 0.25 - **INP/TBT (Interaction to Next Paint / Total Blocking Time):** Measures interactivity. TBT Good: < 200ms, INP Good: < 200ms - **FCP (First Contentful Paint):** Measures perceived load speed. Good: < 1.8s - **Speed Index:** How quickly content is visually displayed. Good: < 3.4s --- ## Results Summary Table ### Mobile Results | Locale | Performance | LCP | CLS | TBT | FCP | Accessibility | SEO | |--------|-------------|-----|-----|-----|-----|---------------|-----| `; // Add mobile results LOCALES.forEach(locale => { const result = allResults[`${locale}_mobile`]; if (result) { const cwv = result.coreWebVitals; markdown += `| ${locale.toUpperCase()} | ${result.scores.performance}% | ${cwv.LCP.displayValue} | ${cwv.CLS.displayValue} | ${cwv.TBT.displayValue} | ${cwv.FCP.displayValue} | ${result.scores.accessibility}% | ${result.scores.seo}% |\n`; } }); markdown += ` ### Desktop Results | Locale | Performance | LCP | CLS | TBT | FCP | Accessibility | SEO | |--------|-------------|-----|-----|-----|-----|---------------|-----| `; // Add desktop results LOCALES.forEach(locale => { const result = allResults[`${locale}_desktop`]; if (result) { const cwv = result.coreWebVitals; markdown += `| ${locale.toUpperCase()} | ${result.scores.performance}% | ${cwv.LCP.displayValue} | ${cwv.CLS.displayValue} | ${cwv.TBT.displayValue} | ${cwv.FCP.displayValue} | ${result.scores.accessibility}% | ${result.scores.seo}% |\n`; } }); markdown += ` --- ## Detailed Results by Locale `; // Detailed results for each locale LOCALES.forEach(locale => { markdown += `### ${locale.toUpperCase()} - ${locale === 'de' ? 'German' : locale === 'en' ? 'English' : 'Serbian'} **URL:** ${BASE_URL}/${locale} `; STRATEGIES.forEach(strategy => { const result = allResults[`${locale}_${strategy}`]; if (result) { const cwv = result.coreWebVitals; markdown += `#### ${strategy.charAt(0).toUpperCase() + strategy.slice(1)} **Scores:** - Performance: ${result.scores.performance}% - Accessibility: ${result.scores.accessibility}% - Best Practices: ${result.scores.bestPractices}% - SEO: ${result.scores.seo}% **Core Web Vitals:** | Metric | Value | Score | |--------|-------|-------| | First Contentful Paint (FCP) | ${cwv.FCP.displayValue} | ${cwv.FCP.score !== null ? Math.round(cwv.FCP.score * 100) + '%' : 'N/A'} | | Largest Contentful Paint (LCP) | ${cwv.LCP.displayValue} | ${cwv.LCP.score !== null ? Math.round(cwv.LCP.score * 100) + '%' : 'N/A'} | | Cumulative Layout Shift (CLS) | ${cwv.CLS.displayValue} | ${cwv.CLS.score !== null ? Math.round(cwv.CLS.score * 100) + '%' : 'N/A'} | | Total Blocking Time (TBT) | ${cwv.TBT.displayValue} | ${cwv.TBT.score !== null ? Math.round(cwv.TBT.score * 100) + '%' : 'N/A'} | | Speed Index | ${cwv.SpeedIndex.displayValue} | ${cwv.SpeedIndex.score !== null ? Math.round(cwv.SpeedIndex.score * 100) + '%' : 'N/A'} | `; if (result.opportunities && result.opportunities.length > 0) { markdown += `**Optimization Opportunities:** `; result.opportunities.slice(0, 5).forEach(opp => { markdown += `- **${opp.title}**: Potential savings of ${formatTime(opp.savings_ms)}`; if (opp.savings_bytes > 0) { markdown += ` / ${(opp.savings_bytes / 1024).toFixed(1)} KB`; } markdown += `\n`; }); markdown += '\n'; } if (result.diagnostics && result.diagnostics.length > 0) { markdown += `**Diagnostics (Items Needing Attention):** `; result.diagnostics.slice(0, 5).forEach(diag => { markdown += `- ${diag.title} (Score: ${Math.round(diag.score * 100)}%)\n`; }); markdown += '\n'; } } }); }); markdown += `--- ## Core Web Vitals Analysis ### LCP (Largest Contentful Paint) Summary `; // LCP analysis const lcpValues = []; Object.entries(allResults).forEach(([key, result]) => { if (result && result.coreWebVitals.LCP.value) { lcpValues.push({ test: key, value: result.coreWebVitals.LCP.value, displayValue: result.coreWebVitals.LCP.displayValue }); } }); if (lcpValues.length > 0) { const avgLCP = lcpValues.reduce((sum, item) => sum + item.value, 0) / lcpValues.length; const maxLCP = Math.max(...lcpValues.map(item => item.value)); const minLCP = Math.min(...lcpValues.map(item => item.value)); markdown += `- **Average LCP:** ${formatTime(avgLCP)} - **Best LCP:** ${formatTime(minLCP)} - **Worst LCP:** ${formatTime(maxLCP)} - **Target:** < 2.5s for Good score `; } markdown += `### CLS (Cumulative Layout Shift) Summary `; // CLS analysis const clsValues = []; Object.entries(allResults).forEach(([key, result]) => { if (result && result.coreWebVitals.CLS.value !== null) { clsValues.push({ test: key, value: result.coreWebVitals.CLS.value, displayValue: result.coreWebVitals.CLS.displayValue }); } }); if (clsValues.length > 0) { const avgCLS = clsValues.reduce((sum, item) => sum + item.value, 0) / clsValues.length; const maxCLS = Math.max(...clsValues.map(item => item.value)); markdown += `- **Average CLS:** ${avgCLS.toFixed(3)} - **Worst CLS:** ${maxCLS.toFixed(3)} - **Target:** < 0.1 for Good score `; } markdown += `### TBT (Total Blocking Time) Summary `; // TBT analysis const tbtValues = []; Object.entries(allResults).forEach(([key, result]) => { if (result && result.coreWebVitals.TBT.value !== null) { tbtValues.push({ test: key, value: result.coreWebVitals.TBT.value, displayValue: result.coreWebVitals.TBT.displayValue }); } }); if (tbtValues.length > 0) { const avgTBT = tbtValues.reduce((sum, item) => sum + item.value, 0) / tbtValues.length; const maxTBT = Math.max(...tbtValues.map(item => item.value)); markdown += `- **Average TBT:** ${formatTime(avgTBT)} - **Worst TBT:** ${formatTime(maxTBT)} - **Target:** < 200ms for Good score `; } markdown += `--- ## Recommendations Summary Based on the audit results, here are the key areas for improvement: ### High Priority (Performance Impact) 1. **Largest Contentful Paint (LCP):** Focus on optimizing the hero image and main content loading 2. **Total Blocking Time (TBT):** Reduce JavaScript execution time, consider code splitting 3. **First Contentful Paint (FCP):** Optimize critical rendering path ### Medium Priority (User Experience) 1. **Cumulative Layout Shift (CLS):** Ensure images have explicit dimensions, avoid layout shifts from fonts 2. **Speed Index:** Optimize above-the-fold content delivery ### Common Opportunities Across All Tests `; // Aggregate opportunities const allOpportunities = {}; Object.values(allResults).forEach(result => { if (result && result.opportunities) { result.opportunities.forEach(opp => { if (!allOpportunities[opp.title]) { allOpportunities[opp.title] = { count: 0, totalSavings: 0, title: opp.title }; } allOpportunities[opp.title].count++; allOpportunities[opp.title].totalSavings += opp.savings_ms; }); } }); const sortedOpportunities = Object.values(allOpportunities) .sort((a, b) => b.totalSavings - a.totalSavings) .slice(0, 10); sortedOpportunities.forEach((opp, index) => { markdown += `${index + 1}. **${opp.title}** - Appeared in ${opp.count} tests, avg. savings: ${formatTime(opp.totalSavings / opp.count)}\n`; }); markdown += ` --- ## Raw Data Files Individual JSON files with complete audit data are saved in the \`lighthouse-reports\` directory: `; Object.keys(allResults).forEach(key => { markdown += `- \`${key}.json\`\n`; }); markdown += ` --- *Report generated by lighthouse-audit-all-locales.js* `; return markdown; } async function main() { console.log('\n========================================'); console.log(' Lighthouse Audit - All Locales'); console.log('========================================\n'); console.log(`Base URL: ${BASE_URL}`); console.log(`Locales: ${LOCALES.join(', ')}`); console.log(`Strategies: ${STRATEGIES.join(', ')}`); console.log(`Reports Directory: ${REPORTS_DIR}`); console.log('\n----------------------------------------\n'); // Ensure reports directory exists if (!fs.existsSync(REPORTS_DIR)) { fs.mkdirSync(REPORTS_DIR, { recursive: true }); } const allResults = {}; const errors = []; // Run tests for each locale and strategy for (const locale of LOCALES) { const url = `${BASE_URL}/${locale}`; console.log(`\n>>> Testing ${locale.toUpperCase()} (${url})`); for (const strategy of STRATEGIES) { const key = `${locale}_${strategy}`; try { const data = await runPageSpeedTest(url, strategy); const results = extractCoreWebVitals(data); allResults[key] = results; // Save individual JSON file const jsonPath = path.join(REPORTS_DIR, `${key}.json`); fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2)); console.log(` Saved: ${key}.json`); // Display quick summary console.log(` Performance: ${results.scores.performance}% | LCP: ${results.coreWebVitals.LCP.displayValue} | CLS: ${results.coreWebVitals.CLS.displayValue}`); } catch (error) { console.error(` FAILED: ${key} - ${error.message}`); errors.push({ key, error: error.message }); } } } // Generate and save markdown report console.log('\n----------------------------------------'); console.log('Generating comprehensive report...'); const markdownReport = generateMarkdownReport(allResults); const reportPath = path.join(REPORTS_DIR, 'LIGHTHOUSE_REPORT.md'); fs.writeFileSync(reportPath, markdownReport); console.log(`Report saved: ${reportPath}`); // Save combined JSON const combinedPath = path.join(REPORTS_DIR, 'all_results.json'); fs.writeFileSync(combinedPath, JSON.stringify({ timestamp: new Date().toISOString(), baseUrl: BASE_URL, locales: LOCALES, strategies: STRATEGIES, results: allResults, errors: errors }, null, 2)); console.log(`Combined data saved: ${combinedPath}`); // Final summary console.log('\n========================================'); console.log(' AUDIT COMPLETE'); console.log('========================================\n'); console.log('Results Summary:'); console.log('----------------'); LOCALES.forEach(locale => { console.log(`\n${locale.toUpperCase()}:`); STRATEGIES.forEach(strategy => { const result = allResults[`${locale}_${strategy}`]; if (result) { console.log(` ${strategy.padEnd(8)}: Performance ${result.scores.performance}% | LCP ${result.coreWebVitals.LCP.displayValue.padEnd(6)} | CLS ${result.coreWebVitals.CLS.displayValue}`); } else { console.log(` ${strategy.padEnd(8)}: FAILED`); } }); }); if (errors.length > 0) { console.log(`\n${errors.length} test(s) failed. Check the logs above for details.`); } console.log('\n----------------------------------------'); console.log(`Reports saved to: ${REPORTS_DIR}`); console.log('----------------------------------------\n'); } main().catch(error => { console.error('Fatal error:', error); process.exit(1); });