n8n-nodes-google-pagespeed
Version:
n8n community node for Google PageSpeed Insights API with comprehensive performance, accessibility, and SEO analysis
337 lines • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatResponse = formatResponse;
exports.formatBatchResults = formatBatchResults;
const config_1 = require("../config");
/**
* Extract category scores from PageSpeed API response
* @param response - PageSpeed API response
* @returns Formatted scores object
*/
function extractScores(response) {
const categories = response.lighthouseResult?.categories || {};
return {
performance: Math.round((categories.performance?.score || 0) * 100),
accessibility: Math.round((categories.accessibility?.score || 0) * 100),
bestPractices: Math.round((categories['best-practices']?.score || 0) * 100),
seo: Math.round((categories.seo?.score || 0) * 100),
};
}
/**
* Extract Core Web Vitals metrics from PageSpeed API response
* @param response - PageSpeed API response
* @returns Formatted metrics object
*/
function extractMetrics(response) {
const audits = response.lighthouseResult?.audits || {};
const metrics = {
firstContentfulPaint: null,
largestContentfulPaint: null,
cumulativeLayoutShift: null,
speedIndex: null,
timeToInteractive: null,
firstInputDelay: null,
totalBlockingTime: null,
};
// Helper to safely get numeric value or null
const getNumericValue = (auditKey) => {
const audit = audits[auditKey];
if (!audit) {
console.log(`Audit not found: ${auditKey}`);
return null;
}
const value = audit.numericValue;
// Check if value is a valid number (not null, not undefined, not NaN)
return typeof value === 'number' && !isNaN(value) ? value : null;
};
// Extract all metrics
metrics.firstContentfulPaint = getNumericValue('first-contentful-paint');
metrics.largestContentfulPaint = getNumericValue('largest-contentful-paint');
metrics.cumulativeLayoutShift = getNumericValue('cumulative-layout-shift');
metrics.speedIndex = getNumericValue('speed-index');
metrics.timeToInteractive = getNumericValue('interactive');
metrics.firstInputDelay = getNumericValue('max-potential-fid');
// Special handling for TBT to ensure we capture 0 values correctly
const tbtAudit = audits['total-blocking-time'];
if (tbtAudit) {
const tbtValue = tbtAudit.numericValue;
// Explicitly check for undefined/null (not falsy, which would catch 0)
metrics.totalBlockingTime = (typeof tbtValue === 'number' && !isNaN(tbtValue)) ? tbtValue : null;
console.log('TBT Audit Details:', {
numericValue: tbtAudit.numericValue,
displayValue: tbtAudit.displayValue,
score: tbtAudit.score,
scoreDisplayMode: tbtAudit.scoreDisplayMode,
isZero: tbtAudit.numericValue === 0,
isNull: tbtAudit.numericValue === null,
isUndefined: tbtAudit.numericValue === undefined
});
}
else {
console.log('TBT audit not found in response');
}
return metrics;
}
/**
* Extract and format audit details from PageSpeed API response
* @param response - PageSpeed API response
* @returns Formatted audits object
*/
function extractAudits(response) {
const audits = response.lighthouseResult?.audits || {};
const formattedAudits = {};
// Key audits to include in results
const keyAudits = [
'first-contentful-paint',
'largest-contentful-paint',
'speed-index',
'interactive',
'cumulative-layout-shift',
'total-blocking-time',
'server-response-time',
'first-meaningful-paint',
'bootup-time',
'mainthread-work-breakdown',
'network-requests',
'metrics',
'screenshot-thumbnails',
'final-screenshot',
// Accessibility audits
'color-contrast',
'image-alt',
'button-name',
'link-name',
// Best practices audits
'is-on-https',
'uses-http2',
'uses-passive-event-listeners',
// SEO audits
'document-title',
'meta-description',
'hreflang',
'canonical',
];
keyAudits.forEach(auditId => {
if (audits[auditId]) {
const audit = audits[auditId];
formattedAudits[auditId] = {
score: audit.score,
numericValue: audit.numericValue || null,
displayValue: audit.displayValue || '',
title: audit.title || '',
description: audit.description || '',
scoreDisplayMode: audit.scoreDisplayMode || '',
};
}
});
return formattedAudits;
}
/**
* Get performance rating based on score
* @param score - Performance score (0-100)
* @returns Rating string
*/
function getPerformanceRating(score) {
if (score >= config_1.PAGESPEED_CONFIG.PERFORMANCE_THRESHOLDS.GOOD)
return 'Good';
if (score >= config_1.PAGESPEED_CONFIG.PERFORMANCE_THRESHOLDS.NEEDS_IMPROVEMENT)
return 'Needs Improvement';
return 'Poor';
}
/**
* Analyze Core Web Vitals and provide ratings
* @param metrics - Core Web Vitals metrics
* @returns Analysis of each metric
*/
function analyzeWebVitals(metrics) {
return {
firstContentfulPaint: {
value: metrics.firstContentfulPaint,
rating: !metrics.firstContentfulPaint ? 'Unknown' :
metrics.firstContentfulPaint <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.FCP_GOOD ? 'Good' :
metrics.firstContentfulPaint <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.FCP_POOR ? 'Needs Improvement' : 'Poor',
threshold: `Good: ≤${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.FCP_GOOD}ms, Poor: >${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.FCP_POOR}ms`
},
largestContentfulPaint: {
value: metrics.largestContentfulPaint,
rating: !metrics.largestContentfulPaint ? 'Unknown' :
metrics.largestContentfulPaint <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.LCP_GOOD ? 'Good' :
metrics.largestContentfulPaint <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.LCP_POOR ? 'Needs Improvement' : 'Poor',
threshold: `Good: ≤${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.LCP_GOOD}ms, Poor: >${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.LCP_POOR}ms`
},
cumulativeLayoutShift: {
value: metrics.cumulativeLayoutShift,
rating: !metrics.cumulativeLayoutShift ? 'Unknown' :
metrics.cumulativeLayoutShift <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.CLS_GOOD ? 'Good' :
metrics.cumulativeLayoutShift <= config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.CLS_POOR ? 'Needs Improvement' : 'Poor',
threshold: `Good: ≤${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.CLS_GOOD}, Poor: >${config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.CLS_POOR}`
},
};
}
/**
* Generate performance recommendations based on scores and metrics
* @param scores - Category scores
* @param metrics - Core Web Vitals metrics
* @returns Array of recommendations
*/
function generateRecommendations(scores, metrics) {
const recommendations = [];
if (scores.performance < 50) {
recommendations.push('Critical performance issues detected. Focus on optimizing largest contentful paint and cumulative layout shift.');
}
else if (scores.performance < 90) {
recommendations.push('Performance could be improved. Consider optimizing images, reducing JavaScript execution time, and minimizing layout shifts.');
}
if (scores.accessibility < 90) {
recommendations.push('Accessibility improvements needed. Ensure proper color contrast, alt text for images, and semantic HTML structure.');
}
if (scores.bestPractices < 90) {
recommendations.push('Web development best practices could be improved. Use HTTPS, update to HTTP/2, and follow modern web standards.');
}
if (scores.seo < 90) {
recommendations.push('SEO optimization needed. Add meta descriptions, ensure proper heading structure, and optimize for mobile.');
}
// Specific metric recommendations
if (metrics.largestContentfulPaint && metrics.largestContentfulPaint > config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.LCP_POOR) {
recommendations.push('Largest Contentful Paint is slow. Optimize images, improve server response time, and reduce render-blocking resources.');
}
if (metrics.cumulativeLayoutShift && metrics.cumulativeLayoutShift > config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.CLS_POOR) {
recommendations.push('High Cumulative Layout Shift detected. Add size attributes to images and videos, avoid inserting content above existing content.');
}
if (metrics.firstContentfulPaint && metrics.firstContentfulPaint > config_1.PAGESPEED_CONFIG.CORE_WEB_VITALS.FCP_POOR) {
recommendations.push('First Contentful Paint is slow. Reduce server response time, eliminate render-blocking resources, and minify CSS.');
}
return recommendations;
}
/**
* Format PageSpeed API response based on output format preference
* @param response - PageSpeed API response or error response
* @param outputFormat - Desired output format
* @param strategy - Analysis strategy used
* @param originalUrl - Original URL before normalization
* @returns Formatted analysis result
*/
function formatResponse(response, outputFormat = 'complete', strategy, originalUrl) {
// Handle error responses
if ('error' in response) {
return {
url: response.url,
originalUrl: originalUrl || response.url,
strategy: response.strategy,
error: response.errorMessage,
errorType: response.errorType,
contentType: response.contentType,
skipped: true,
analysisTime: response.analysisTime,
retryCount: response.retryCount,
};
}
// Extract data from successful response
const scores = extractScores(response);
const metrics = extractMetrics(response);
const audits = extractAudits(response);
// Get URLs from response
const finalUrl = response.lighthouseResult.finalUrl || response.lighthouseResult.requestedUrl;
// Base result object
const baseResult = {
url: finalUrl,
originalUrl: originalUrl || finalUrl,
strategy: strategy || 'unknown',
analysisTime: new Date().toISOString(),
};
// Format based on output preference
switch (outputFormat) {
case 'scoresOnly':
return {
...baseResult,
scores,
};
case 'coreMetrics':
return {
...baseResult,
scores,
metrics,
};
case 'summary':
const webVitalsAnalysis = analyzeWebVitals(metrics);
const recommendations = generateRecommendations(scores, metrics);
return {
...baseResult,
scores,
metrics,
summary: {
overallPerformance: getPerformanceRating(scores.performance),
webVitalsAnalysis,
recommendations,
keyMetrics: {
performanceScore: scores.performance,
accessibilityScore: scores.accessibility,
bestPracticesScore: scores.bestPractices,
seoScore: scores.seo,
},
},
};
case 'complete':
default:
return {
...baseResult,
scores,
metrics,
audits,
screenshot: response.lighthouseResult?.audits?.['final-screenshot']?.details?.data || null,
lighthouseVersion: response.lighthouseResult.lighthouseVersion,
userAgent: response.lighthouseResult.userAgent,
fetchTime: response.lighthouseResult.fetchTime,
environment: response.lighthouseResult.environment,
};
}
}
/**
* Format batch analysis results with summary statistics
* @param results - Array of analysis results
* @returns Formatted batch results with summary
*/
function formatBatchResults(results) {
const successful = results.filter(r => !r.error);
const failed = results.filter(r => r.error);
// Calculate average scores for successful results
let averageScores;
if (successful.length > 0) {
const totalScores = successful.reduce((acc, result) => {
if (result.scores) {
acc.performance += result.scores.performance;
acc.accessibility += result.scores.accessibility;
acc.bestPractices += result.scores.bestPractices;
acc.seo += result.scores.seo;
}
return acc;
}, { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 });
averageScores = {
performance: Math.round(totalScores.performance / successful.length),
accessibility: Math.round(totalScores.accessibility / successful.length),
bestPractices: Math.round(totalScores.bestPractices / successful.length),
seo: Math.round(totalScores.seo / successful.length),
};
}
// Extract unique domains
const domains = [...new Set(results.map(r => {
try {
return new URL(r.url).hostname;
}
catch {
return 'unknown';
}
}))];
return {
results,
summary: {
total: results.length,
successful: successful.length,
failed: failed.length,
averageScores,
domains,
analysisTime: new Date().toISOString(),
},
};
}
//# sourceMappingURL=responseFormatter.js.map