UNPKG

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
"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