n8n-nodes-google-pagespeed
Version:
n8n community node for Google PageSpeed Insights API with comprehensive performance, accessibility, and SEO analysis
300 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeCompareUrls = executeCompareUrls;
const n8n_workflow_1 = require("n8n-workflow");
const config_1 = require("../config");
const urlUtils_1 = require("../utils/urlUtils");
const apiUtils_1 = require("../utils/apiUtils");
const responseFormatter_1 = require("../helpers/responseFormatter");
/**
* Calculate score differences between two analyses
* @param baseline - Baseline analysis scores
* @param current - Current analysis scores
* @returns Score differences
*/
function calculateScoreDifferences(baseline, current) {
return {
performance: current.performance - baseline.performance,
accessibility: current.accessibility - baseline.accessibility,
bestPractices: current.bestPractices - baseline.bestPractices,
seo: current.seo - baseline.seo,
};
}
/**
* Calculate metric differences between two analyses
* @param baseline - Baseline analysis metrics
* @param current - Current analysis metrics
* @returns Metric differences (negative = improvement for time-based metrics)
*/
function calculateMetricDifferences(baseline, current) {
const differences = {};
if (baseline.firstContentfulPaint && current.firstContentfulPaint) {
differences.firstContentfulPaint = current.firstContentfulPaint - baseline.firstContentfulPaint;
}
if (baseline.largestContentfulPaint && current.largestContentfulPaint) {
differences.largestContentfulPaint = current.largestContentfulPaint - baseline.largestContentfulPaint;
}
if (baseline.cumulativeLayoutShift && current.cumulativeLayoutShift) {
differences.cumulativeLayoutShift = current.cumulativeLayoutShift - baseline.cumulativeLayoutShift;
}
if (baseline.speedIndex && current.speedIndex) {
differences.speedIndex = current.speedIndex - baseline.speedIndex;
}
if (baseline.timeToInteractive && current.timeToInteractive) {
differences.timeToInteractive = current.timeToInteractive - baseline.timeToInteractive;
}
if (baseline.totalBlockingTime && current.totalBlockingTime) {
differences.totalBlockingTime = current.totalBlockingTime - baseline.totalBlockingTime;
}
return differences;
}
/**
* Determine if there's overall improvement between two analyses
* @param scoreDifferences - Score differences
* @param metricDifferences - Metric differences
* @returns True if overall improvement detected
*/
function determineImprovement(scoreDifferences, metricDifferences) {
// Score improvements (higher is better) - Fixed: Safely handle score values
const scoreEntries = Object.entries(scoreDifferences);
const scoreImprovements = scoreEntries.filter(([_, diff]) => diff > 0).length;
const scoreRegressions = scoreEntries.filter(([_, diff]) => diff < 0).length;
// Metric improvements (lower is better for time-based metrics) - Fixed: Safely handle metric values
const metricEntries = Object.entries(metricDifferences).filter(([_, value]) => typeof value === 'number' && !isNaN(value));
const metricImprovements = metricEntries.filter(([_, diff]) => diff < 0).length;
const metricRegressions = metricEntries.filter(([_, diff]) => diff > 0).length;
// Overall improvement if more improvements than regressions
return (scoreImprovements + metricImprovements) > (scoreRegressions + metricRegressions);
}
/**
* Check if changes are significant enough to report
* @param scoreDifferences - Score differences
* @returns True if changes are significant
*/
function hasSignificantChange(scoreDifferences) {
const scoreEntries = Object.entries(scoreDifferences);
return scoreEntries.some(([_, diff]) => Math.abs(diff) >= config_1.PAGESPEED_CONFIG.SIGNIFICANT_SCORE_CHANGE);
}
/**
* Compare two URL analyses or perform before/after comparison
* @param context - n8n execution context
* @param apiKey - Google API key
* @returns Array of comparison results
*/
async function executeCompareUrls(context, apiKey) {
const operation = context.getNodeParameter('compareOperation', 0);
const strategy = context.getNodeParameter('strategy', 0);
const categories = context.getNodeParameter('categories', 0);
const additionalFields = context.getNodeParameter('additionalFields', 0);
const results = [];
try {
if (operation === 'compareTwo') {
// Compare two different URLs
const url1 = context.getNodeParameter('url1', 0);
const url2 = context.getNodeParameter('url2', 0);
if (!url1 || !url2) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Both URLs are required for comparison');
}
const normalizedUrl1 = (0, urlUtils_1.normalizeUrl)(url1);
const normalizedUrl2 = (0, urlUtils_1.normalizeUrl)(url2);
console.log(`🔄 Comparing URLs: "${url1}" vs "${url2}"`);
// Analyze both URLs
const [result1, result2] = await Promise.all([
analyzeUrlForComparison(context, apiKey, normalizedUrl1, strategy, categories, additionalFields),
analyzeUrlForComparison(context, apiKey, normalizedUrl2, strategy, categories, additionalFields),
]);
if (result1.error || result2.error) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Analysis failed: ${result1.error || result2.error}`);
}
const comparison = createComparison(normalizedUrl1, result1, result2, 'url-comparison');
results.push({ json: comparison });
}
else if (operation === 'beforeAfter') {
// Before/after comparison for same URL
const url = context.getNodeParameter('url', 0);
const normalizedUrl = (0, urlUtils_1.normalizeUrl)(url);
console.log(`📊 Before/after comparison for: "${url}"`);
// Get baseline from input data or parameter
const baselineData = context.getNodeParameter('baselineData', 0, null);
let baseline;
if (baselineData) {
// Use provided baseline
baseline = baselineData;
}
else {
// Get baseline from previous analysis in input data
const inputData = context.getInputData();
if (!inputData[0] || !inputData[0].json) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'No baseline data provided. Please provide baseline analysis or connect previous node.');
}
baseline = inputData[0].json;
}
// Perform current analysis
const current = await analyzeUrlForComparison(context, apiKey, normalizedUrl, strategy, categories, additionalFields);
if (current.error) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Current analysis failed: ${current.error}`);
}
const comparison = createComparison(normalizedUrl, baseline, current, 'before-after');
results.push({ json: comparison });
}
else if (operation === 'batch') {
// Batch comparison of URLs from input
const inputData = context.getInputData();
if (!inputData || inputData.length < 2) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'At least 2 input items required for batch comparison');
}
console.log(`📊 Batch comparison of ${inputData.length} URLs`);
// Compare each item with the first one as baseline
const baseline = inputData[0].json;
for (let i = 1; i < inputData.length; i++) {
const current = inputData[i].json;
const comparison = createComparison(current.url, baseline, current, 'batch-comparison');
results.push({ json: comparison });
}
}
console.log(`✅ URL comparison completed: ${results.length} result(s)`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('❌ URL comparison failed:', errorMessage);
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `URL comparison failed: ${errorMessage}`);
}
return results;
}
/**
* Analyze a URL specifically for comparison purposes
* @param context - n8n execution context
* @param apiKey - Google API key
* @param url - URL to analyze
* @param strategy - Analysis strategy
* @param categories - Categories to analyze
* @param additionalFields - Additional options
* @returns Analysis result
*/
async function analyzeUrlForComparison(context, apiKey, url, strategy, categories, additionalFields) {
const config = {
url,
strategy,
categories,
locale: additionalFields?.locale,
screenshot: additionalFields?.screenshot,
timeout: additionalFields?.customTimeout,
retryAttempts: additionalFields?.retryAttempts,
};
const apiResponse = await (0, apiUtils_1.makePageSpeedRequest)(context, apiKey, config, additionalFields);
return (0, responseFormatter_1.formatResponse)(apiResponse, additionalFields?.outputFormat || 'coreMetrics', strategy, url);
}
/**
* Create comparison result from two analyses
* @param url - URL being compared
* @param baseline - Baseline analysis
* @param current - Current analysis
* @param comparisonType - Type of comparison
* @returns Comparison result
*/
function createComparison(url, baseline, current, comparisonType) {
if (!baseline.scores || !current.scores) {
throw new Error('Both analyses must include scores for comparison');
}
const scoreDifferences = calculateScoreDifferences(baseline.scores, current.scores);
const metricDifferences = calculateMetricDifferences(baseline.metrics || {
firstContentfulPaint: null,
largestContentfulPaint: null,
cumulativeLayoutShift: null,
speedIndex: null,
timeToInteractive: null,
}, current.metrics || {
firstContentfulPaint: null,
largestContentfulPaint: null,
cumulativeLayoutShift: null,
speedIndex: null,
timeToInteractive: null,
});
const improvement = determineImprovement(scoreDifferences, metricDifferences);
const significantChange = hasSignificantChange(scoreDifferences);
return {
url,
baselineAnalysis: baseline,
currentAnalysis: current,
scoreDifferences,
metricDifferences,
improvement,
significantChange,
analysisTime: new Date().toISOString(),
comparisonType,
summary: {
overallImprovement: improvement,
significantChanges: significantChange,
keyChanges: generateChangesSummary(scoreDifferences, metricDifferences),
recommendations: generateImprovementRecommendations(scoreDifferences, metricDifferences),
},
};
}
/**
* Generate summary of key changes
* @param scoreDifferences - Score differences
* @param metricDifferences - Metric differences
* @returns Array of change descriptions
*/
function generateChangesSummary(scoreDifferences, metricDifferences) {
const changes = [];
// Score changes - Fixed: Properly handle typed entries
const scoreEntries = Object.entries(scoreDifferences);
scoreEntries.forEach(([category, diff]) => {
if (Math.abs(diff) >= config_1.PAGESPEED_CONFIG.SIGNIFICANT_SCORE_CHANGE) {
const direction = diff > 0 ? 'improved' : 'decreased';
const categoryName = category.replace(/([A-Z])/g, ' $1').toLowerCase();
changes.push(`${categoryName} score ${direction} by ${Math.abs(diff)} points`);
}
});
// Metric changes
if (metricDifferences.largestContentfulPaint) {
const diff = metricDifferences.largestContentfulPaint;
const direction = diff < 0 ? 'improved' : 'increased';
changes.push(`Largest Contentful Paint ${direction} by ${Math.abs(diff)}ms`);
}
if (metricDifferences.firstContentfulPaint) {
const diff = metricDifferences.firstContentfulPaint;
const direction = diff < 0 ? 'improved' : 'increased';
changes.push(`First Contentful Paint ${direction} by ${Math.abs(diff)}ms`);
}
if (metricDifferences.cumulativeLayoutShift) {
const diff = metricDifferences.cumulativeLayoutShift;
const direction = diff < 0 ? 'improved' : 'increased';
changes.push(`Cumulative Layout Shift ${direction} by ${Math.abs(diff).toFixed(3)}`);
}
return changes;
}
/**
* Generate improvement recommendations based on comparison
* @param scoreDifferences - Score differences
* @param metricDifferences - Metric differences
* @returns Array of recommendations
*/
function generateImprovementRecommendations(scoreDifferences, metricDifferences) {
const recommendations = [];
// Performance regressions
if (scoreDifferences.performance < -config_1.PAGESPEED_CONFIG.SIGNIFICANT_SCORE_CHANGE) {
recommendations.push('Performance has regressed. Review recent changes to scripts, images, and third-party resources.');
}
// Metric-specific recommendations
if (metricDifferences.largestContentfulPaint && metricDifferences.largestContentfulPaint > 500) {
recommendations.push('Largest Contentful Paint has increased significantly. Check for new large images or slow server responses.');
}
if (metricDifferences.cumulativeLayoutShift && metricDifferences.cumulativeLayoutShift > 0.1) {
recommendations.push('Layout shifts have increased. Ensure all images and videos have size attributes.');
}
// Accessibility regressions
if (scoreDifferences.accessibility < -config_1.PAGESPEED_CONFIG.SIGNIFICANT_SCORE_CHANGE) {
recommendations.push('Accessibility has regressed. Review recent changes to color contrast, alt text, and semantic structure.');
}
// SEO regressions
if (scoreDifferences.seo < -config_1.PAGESPEED_CONFIG.SIGNIFICANT_SCORE_CHANGE) {
recommendations.push('SEO score has decreased. Check meta tags, heading structure, and mobile optimization.');
}
if (recommendations.length === 0) {
recommendations.push('Continue monitoring performance and consider A/B testing optimizations.');
}
return recommendations;
}
//# sourceMappingURL=compareUrls.js.map