Merge origin/master

This commit is contained in:
2026-01-25 19:46:04 +01:00
429 changed files with 77997 additions and 25554 deletions
+527
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/**
* Lighthouse Audit for All Locales
<<<<<<< HEAD
*
* Runs Lighthouse performance audits against all locales (de, en, sr)
* and key pages using Playwright-Lighthouse integration.
@@ -293,4 +294,530 @@ async function main() {
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
=======
* 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);
>>>>>>> origin/master
});