- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
529 lines
18 KiB
JavaScript
529 lines
18 KiB
JavaScript
#!/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);
|
|
});
|