n8n-nodes-google-pagespeed
Version:
n8n community node for Google PageSpeed Insights API with comprehensive performance, accessibility, and SEO analysis
364 lines β’ 15.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeAnalyzeSitemap = executeAnalyzeSitemap;
exports.validateSitemapParams = validateSitemapParams;
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");
const sitemapHelpers_1 = require("../helpers/sitemapHelpers");
/**
* Execute sitemap analysis operation
* @param context - n8n execution context
* @param apiKey - Google API key
* @returns Array of analysis results including sitemap metadata
*/
async function executeAnalyzeSitemap(context, apiKey) {
const rawSitemapUrl = context.getNodeParameter('sitemapUrl', 0);
const strategy = context.getNodeParameter('strategy', 0);
const categories = context.getNodeParameter('categories', 0);
const additionalFields = context.getNodeParameter('additionalFields', 0);
const urlFilters = context.getNodeParameter('urlFilters', 0);
if (!rawSitemapUrl) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Sitemap URL is required');
}
console.log(`πΊοΈ Starting sitemap analysis for: ${rawSitemapUrl}`);
try {
// Normalize sitemap URL
let sitemapUrl;
try {
sitemapUrl = (0, urlUtils_1.normalizeUrl)(rawSitemapUrl);
console.log(`β
Sitemap URL normalized: ${rawSitemapUrl} β ${sitemapUrl}`);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Invalid sitemap URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Fetch and filter URLs from sitemap
console.log('π Fetching sitemap...');
const startFetchTime = Date.now();
const extractedUrls = await (0, sitemapHelpers_1.fetchSitemapUrls)(context, sitemapUrl, urlFilters);
const fetchDuration = Date.now() - startFetchTime;
if (extractedUrls.length === 0) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'No valid URLs found in sitemap after filtering');
}
console.log(`β
Sitemap processed in ${Math.round(fetchDuration / 1000)}s: ${extractedUrls.length} URLs to analyze`);
// Analyze URL distribution
const urlAnalysis = analyzeUrlDistribution(extractedUrls);
console.log(`π URL Distribution: ${urlAnalysis.domains.length} domains, deepest level: ${urlAnalysis.maxDepth}`);
// Estimate resource usage
const quotaEstimate = (0, apiUtils_1.estimateQuotaUsage)(extractedUrls.length, [strategy]);
const estimatedTime = estimateAnalysisTime(extractedUrls.length, strategy);
console.log(`π° Estimated API quota usage: ${quotaEstimate} requests`);
console.log(`β±οΈ Estimated completion time: ${Math.round(estimatedTime / 60)} minutes`);
// Create API configurations for all URLs
const configs = createSitemapConfigs(extractedUrls, strategy, categories, additionalFields);
// Progress tracking
let completedCount = 0;
const onProgress = (completed, total) => {
completedCount = completed;
const percentage = Math.round((completed / total) * 100);
const currentUrl = configs[completed - 1]?.url || '';
console.log(`π Sitemap Progress: ${completed}/${total} (${percentage}%) - ${(0, urlUtils_1.shortenUrlForDisplay)(currentUrl)}`);
};
// Execute batch processing
console.log('π Starting sitemap URL analysis...');
const startAnalysisTime = Date.now();
const apiResponses = await (0, apiUtils_1.batchProcessUrls)(context, apiKey, configs, additionalFields, onProgress);
const analysisDuration = Date.now() - startAnalysisTime;
console.log(`β
Sitemap analysis completed in ${Math.round(analysisDuration / 1000)}s`);
// Format responses
const analysisResults = [];
for (let i = 0; i < apiResponses.length; i++) {
const response = apiResponses[i];
const config = configs[i];
const result = (0, responseFormatter_1.formatResponse)(response, additionalFields.outputFormat || 'complete', config.strategy, config.url);
// Add sitemap-specific metadata
result.source = 'sitemap';
result.sitemapUrl = sitemapUrl;
result.urlIndex = Math.floor(i / (strategy === 'both' ? 2 : 1)) + 1;
result.totalUrls = extractedUrls.length;
analysisResults.push(result);
}
// Create comprehensive results
const batchResults = (0, responseFormatter_1.formatBatchResults)(analysisResults);
const results = [];
// Add sitemap metadata as first item
const sitemapMetadata = (0, sitemapHelpers_1.generateSitemapMetadata)(sitemapUrl, extractedUrls.length, extractedUrls.length, urlFilters);
results.push({
json: {
...sitemapMetadata,
analysis: {
totalDuration: fetchDuration + analysisDuration,
fetchDuration,
analysisDuration,
averageTimePerUrl: Math.round(analysisDuration / extractedUrls.length),
quotaUsed: quotaEstimate,
successRate: Math.round((batchResults.summary.successful / batchResults.summary.total) * 100),
},
urlDistribution: urlAnalysis,
performance: {
successful: batchResults.summary.successful,
failed: batchResults.summary.failed,
averageScores: batchResults.summary.averageScores,
},
},
});
// Add domain summary if multiple domains found
if (urlAnalysis.domains.length > 1) {
const domainSummary = createDomainSummary(analysisResults);
results.push({
json: {
type: 'domain-summary',
analysisTime: new Date().toISOString(),
domains: domainSummary,
},
});
}
// Add individual URL results
batchResults.results.forEach(result => {
results.push({ json: result });
});
console.log(`π Sitemap analysis complete: ${results.length} total items`);
console.log(`π Success rate: ${Math.round((batchResults.summary.successful / batchResults.summary.total) * 100)}%`);
return results;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('β Sitemap analysis failed:', errorMessage);
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Sitemap analysis failed: ${errorMessage}`);
}
}
/**
* Create API configurations for sitemap URLs
* @param urls - Array of URLs from sitemap
* @param strategy - Analysis strategy
* @param categories - Categories to analyze
* @param additionalFields - Additional options
* @returns Array of API request configurations
*/
function createSitemapConfigs(urls, strategy, categories, additionalFields) {
const configs = [];
urls.forEach(url => {
if (strategy === 'both') {
// Create configs for both mobile and desktop
configs.push({
url,
strategy: 'mobile',
categories,
locale: additionalFields.locale,
screenshot: additionalFields.screenshot,
timeout: additionalFields.customTimeout,
retryAttempts: additionalFields.retryAttempts,
}, {
url,
strategy: 'desktop',
categories,
locale: additionalFields.locale,
screenshot: additionalFields.screenshot,
timeout: additionalFields.customTimeout,
retryAttempts: additionalFields.retryAttempts,
});
}
else {
// Single strategy
configs.push({
url,
strategy,
categories,
locale: additionalFields.locale,
screenshot: additionalFields.screenshot,
timeout: additionalFields.customTimeout,
retryAttempts: additionalFields.retryAttempts,
});
}
});
return configs;
}
/**
* Analyze URL distribution from sitemap
* @param urls - Array of URLs
* @returns URL distribution analysis
*/
function analyzeUrlDistribution(urls) {
const domains = new Set();
const pathDepths = {};
const urlTypes = { pages: 0, posts: 0, other: 0 };
const fileTypes = {};
let maxDepth = 0;
urls.forEach(url => {
try {
const urlObj = new URL(url);
// Track domains
domains.add(urlObj.hostname);
// Calculate path depth
const pathSegments = urlObj.pathname.split('/').filter(segment => segment.length > 0);
const depth = pathSegments.length;
pathDepths[depth] = (pathDepths[depth] || 0) + 1;
maxDepth = Math.max(maxDepth, depth);
// Categorize URL types
const pathLower = urlObj.pathname.toLowerCase();
if (pathLower.includes('/blog/') || pathLower.includes('/post/') || pathLower.includes('/news/')) {
urlTypes.posts++;
}
else if (pathLower.includes('/product/') || pathLower.includes('/service/') || pathSegments.length <= 1) {
urlTypes.pages++;
}
else {
urlTypes.other++;
}
// Track file extensions
const lastSegment = pathSegments[pathSegments.length - 1] || '';
const extensionMatch = lastSegment.match(/\.([a-z0-9]+)$/i);
if (extensionMatch) {
const extension = extensionMatch[1].toLowerCase();
fileTypes[extension] = (fileTypes[extension] || 0) + 1;
}
else {
fileTypes['no-extension'] = (fileTypes['no-extension'] || 0) + 1;
}
}
catch (error) {
// Skip invalid URLs
}
});
return {
totalUrls: urls.length,
domains: Array.from(domains).sort(),
pathDepths,
maxDepth,
urlTypes,
fileTypes,
};
}
/**
* Create domain-based summary of analysis results
* @param results - Array of analysis results
* @returns Domain summary
*/
function createDomainSummary(results) {
const domainMap = new Map();
// Group results by domain
results.forEach(result => {
const domain = (0, urlUtils_1.extractDomain)(result.url);
if (!domainMap.has(domain)) {
domainMap.set(domain, []);
}
domainMap.get(domain).push(result);
});
// Create summary for each domain
const domainSummaries = Array.from(domainMap.entries()).map(([domain, domainResults]) => {
const successfulResults = domainResults.filter(r => !r.error && r.scores);
const failedResults = domainResults.filter(r => r.error);
let averageScores;
let bestPerforming;
let worstPerforming;
if (successfulResults.length > 0) {
// Calculate average scores
const totalScores = successfulResults.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 / successfulResults.length),
accessibility: Math.round(totalScores.accessibility / successfulResults.length),
bestPractices: Math.round(totalScores.bestPractices / successfulResults.length),
seo: Math.round(totalScores.seo / successfulResults.length),
};
// Find best and worst performing URLs
const sortedByPerformance = successfulResults
.filter(r => r.scores)
.sort((a, b) => (b.scores.performance) - (a.scores.performance));
if (sortedByPerformance.length > 0) {
bestPerforming = sortedByPerformance[0].url;
worstPerforming = sortedByPerformance[sortedByPerformance.length - 1].url;
}
}
// Identify common issues
const issues = [];
if (failedResults.length > 0) {
issues.push(`${failedResults.length} URLs failed analysis`);
}
if (averageScores) {
if (averageScores.performance < 50) {
issues.push('Poor average performance score');
}
if (averageScores.accessibility < 80) {
issues.push('Accessibility improvements needed');
}
}
const successRate = Math.round((successfulResults.length / domainResults.length) * 100);
if (successRate < 90) {
issues.push(`Low success rate: ${successRate}%`);
}
return {
domain,
urlCount: domainResults.length,
averageScores,
bestPerforming,
worstPerforming,
issues,
};
});
return domainSummaries.sort((a, b) => b.urlCount - a.urlCount);
}
/**
* Estimate analysis time for sitemap processing
* @param urlCount - Number of URLs to analyze
* @param strategy - Analysis strategy
* @returns Estimated time in seconds
*/
function estimateAnalysisTime(urlCount, strategy) {
const baseTimePerUrl = 15; // seconds per URL
const multiplier = strategy === 'both' ? 2 : 1;
const concurrency = config_1.PAGESPEED_CONFIG.MAX_CONCURRENT_REQUESTS;
const batchDelay = config_1.PAGESPEED_CONFIG.BATCH_DELAY_MS / 1000;
const totalRequests = urlCount * multiplier;
const numberOfBatches = Math.ceil(totalRequests / concurrency);
const parallelTime = numberOfBatches * baseTimePerUrl;
const delayTime = (numberOfBatches - 1) * batchDelay;
return Math.round(parallelTime + delayTime);
}
/**
* Validate sitemap analysis parameters
* @param context - n8n execution context
* @returns Validation result
*/
function validateSitemapParams(context) {
const rawSitemapUrl = context.getNodeParameter('sitemapUrl', 0);
const urlFilters = context.getNodeParameter('urlFilters', 0);
const warnings = [];
if (!rawSitemapUrl) {
return {
isValid: false,
error: 'Sitemap URL is required'
};
}
// Check sitemap URL format
try {
const sitemapUrl = new URL(rawSitemapUrl);
if (!sitemapUrl.pathname.includes('xml') && !sitemapUrl.pathname.includes('sitemap')) {
warnings.push('URL does not appear to be a sitemap (missing .xml or sitemap in path)');
}
}
catch (error) {
return {
isValid: false,
error: 'Invalid sitemap URL format'
};
}
// Check filter settings
if (urlFilters.maxUrls && urlFilters.maxUrls > 100) {
warnings.push(`Large URL limit (${urlFilters.maxUrls}) may consume significant API quota`);
}
return {
isValid: true,
warnings: warnings.length > 0 ? warnings : undefined
};
}
//# sourceMappingURL=analyzeSitemap.js.map