UNPKG

@weppa-cloud/mcp-search-console

Version:

MCP server for Google Search Console with growth-focused metrics

309 lines (308 loc) 11.9 kB
// 🚀 Growth-focused Search Console Tools export async function getSEOPulse(client, siteUrl, days = 7) { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const previousEndDate = new Date(startDate); const previousStartDate = new Date(); previousStartDate.setDate(previousStartDate.getDate() - days * 2); // Current period const currentData = await client.searchanalytics.query({ siteUrl, requestBody: { startDate: startDate.toISOString().split('T')[0], endDate: endDate.toISOString().split('T')[0], dimensions: ['date'], type: 'web', }, }); // Previous period for comparison const previousData = await client.searchanalytics.query({ siteUrl, requestBody: { startDate: previousStartDate.toISOString().split('T')[0], endDate: previousEndDate.toISOString().split('T')[0], dimensions: ['date'], type: 'web', }, }); const current = aggregateMetrics(currentData.data.rows || []); const previous = aggregateMetrics(previousData.data.rows || []); return { visibility: { impressions: current.impressions, change: calculateChange(current.impressions, previous.impressions), alert: getAlert(current.impressions, previous.impressions), }, traffic: { clicks: current.clicks, change: calculateChange(current.clicks, previous.clicks), alert: getAlert(current.clicks, previous.clicks), }, performance: { ctr: `${(current.ctr * 100).toFixed(1)}%`, avgPosition: current.position.toFixed(1), ctrChange: calculateChange(current.ctr, previous.ctr), positionChange: `${(previous.position - current.position).toFixed(1)}`, }, topIssue: detectSEOIssue(current, previous), topOpportunity: detectSEOOpportunity(currentData.data.rows || []), }; } export async function getKeywordOpportunities(client, siteUrl, minImpressions = 100) { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); const response = await client.searchanalytics.query({ siteUrl, requestBody: { startDate: startDate.toISOString().split('T')[0], endDate: endDate.toISOString().split('T')[0], dimensions: ['query', 'page'], // Remove position filter from API request - will filter programmatically rowLimit: 1000, type: 'web', }, }); const opportunities = (response.data.rows || []) .filter(row => { const position = row.position || 0; // Filter for positions 4-20 programmatically return position >= 4 && position <= 20 && (row.impressions || 0) >= minImpressions; }) .map(row => { const potentialClicks = calculatePotentialClicks(row); const ctr = row.ctr || 0; const position = row.position || 0; const clicks = row.clicks || 0; const impressions = row.impressions || 0; const difficulty = estimateDifficulty(position); return { keyword: row.keys?.[0] || '', page: row.keys?.[1] || '', currentPosition: position, impressions: impressions, clicks: clicks, ctr: `${(ctr * 100).toFixed(1)}%`, potentialClicks, potentialTraffic: `+${potentialClicks - clicks} clicks/month`, difficulty, action: getOptimizationAction(position, difficulty), }; }) .sort((a, b) => b.potentialClicks - a.potentialClicks) .slice(0, 20); return { opportunities, summary: { totalOpportunities: opportunities.length, potentialTraffic: opportunities.reduce((sum, o) => sum + (o.potentialClicks - o.clicks), 0), quickWins: opportunities.filter(o => o.difficulty === 'easy').length, }, }; } export async function getContentPerformance(client, siteUrl, limit = 20) { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); const response = await client.searchanalytics.query({ siteUrl, requestBody: { startDate: startDate.toISOString().split('T')[0], endDate: endDate.toISOString().split('T')[0], dimensions: ['page'], rowLimit: limit, type: 'web', }, }); const pages = (response.data.rows || []).map(row => { const url = row.keys?.[0] || ''; const performance = getPerformanceRating(row); return { url, metrics: { impressions: row.impressions || 0, clicks: row.clicks || 0, ctr: `${((row.ctr || 0) * 100).toFixed(1)}%`, position: (row.position || 0).toFixed(1), }, performance, recommendations: getContentRecommendations(row), estimatedValue: calculateContentValue(row), }; }); return { topPages: pages, insights: generateContentInsights(pages), }; } export async function getCompetitorKeywords(client, siteUrl) { // Get keywords where we rank poorly but have impressions const response = await client.searchanalytics.query({ siteUrl, requestBody: { startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], endDate: new Date().toISOString().split('T')[0], dimensions: ['query'], // Remove position filter from API request - will filter programmatically rowLimit: 500, // Increase limit to get more data before filtering type: 'web', }, }); const competitorKeywords = (response.data.rows || []) .filter(row => { const position = row.position || 0; // Filter for positions 10-50 programmatically return position >= 10 && position <= 50 && (row.impressions || 0) > 50; }) .map(row => ({ keyword: row.keys?.[0] || '', position: row.position || 0, impressions: row.impressions || 0, marketShare: `${(((row.clicks || 0) / (row.impressions || 1)) * 100).toFixed(1)}%`, competitorStrength: getCompetitorStrength(row.position || 0), strategy: getCompetitiveStrategy(row), })); return { keywords: competitorKeywords, summary: { totalKeywords: competitorKeywords.length, highValueTargets: competitorKeywords.filter(k => k.impressions > 1000).length, quickWins: competitorKeywords.filter(k => (k.position || 0) < 20).length, }, }; } // Helper functions function aggregateMetrics(rows) { const totals = rows.reduce((acc, row) => ({ impressions: acc.impressions + (row.impressions || 0), clicks: acc.clicks + (row.clicks || 0), ctr: 0, position: 0, }), { impressions: 0, clicks: 0, ctr: 0, position: 0 }); totals.ctr = totals.impressions > 0 ? totals.clicks / totals.impressions : 0; // Calculate weighted average position const weightedPosition = rows.reduce((sum, row) => sum + ((row.position || 0) * (row.impressions || 0)), 0); totals.position = totals.impressions > 0 ? weightedPosition / totals.impressions : 0; return totals; } function calculateChange(current, previous) { if (previous === 0) return '0%'; const change = ((current - previous) / previous * 100).toFixed(1); return `${Number(change) > 0 ? '+' : ''}${change}%`; } function getAlert(current, previous) { const change = previous === 0 ? 0 : ((current - previous) / previous * 100); if (change >= 0) return '🟢'; if (change >= -10) return '🟡'; return '🔴'; } function detectSEOIssue(current, previous) { if (current.impressions < previous.impressions * 0.8) { return 'Visibility dropped 20%+ - check for algorithm updates or technical issues'; } if (current.ctr < previous.ctr * 0.85) { return 'CTR dropped significantly - review meta descriptions and titles'; } if (current.position > previous.position + 2) { return 'Average position worsened - competitors may be gaining ground'; } return undefined; } function detectSEOOpportunity(rows) { const highImpressionLowCTR = rows.find(r => (r.impressions || 0) > 1000 && (r.ctr || 0) < 0.02); if (highImpressionLowCTR) { return 'High impression keywords with low CTR - optimize meta tags for better clicks'; } return undefined; } function calculatePotentialClicks(row) { const position = row.position || 0; const impressions = row.impressions || 0; // CTR by position benchmarks const ctrByPosition = { 1: 0.285, 2: 0.157, 3: 0.111, 4: 0.085, 5: 0.067, 6: 0.052, 7: 0.041, 8: 0.033, 9: 0.027, 10: 0.023, }; const targetPosition = Math.max(1, Math.floor(position) - 3); const targetCTR = ctrByPosition[targetPosition] || 0.02; return Math.floor(impressions * targetCTR); } function estimateDifficulty(position) { if (position <= 7) return 'easy'; if (position <= 15) return 'medium'; return 'hard'; } function getOptimizationAction(position, difficulty) { if (difficulty === 'easy') { return 'Quick content update + internal links'; } if (difficulty === 'medium') { return 'Expand content + build backlinks'; } return 'Create comprehensive guide + aggressive link building'; } function getPerformanceRating(row) { const ctr = row.ctr || 0; const position = row.position || 0; const score = (ctr * 100) + (50 - position); if (score > 45) return 'excellent'; if (score > 30) return 'good'; if (score > 15) return 'needs improvement'; return 'poor'; } function getContentRecommendations(row) { const recommendations = []; if ((row.ctr || 0) < 0.02) { recommendations.push('Improve meta title and description'); } if ((row.position || 0) > 10) { recommendations.push('Add more comprehensive content'); recommendations.push('Build quality backlinks'); } if ((row.impressions || 0) > 1000 && (row.clicks || 0) < 20) { recommendations.push('Test different titles for better CTR'); } return recommendations; } function calculateContentValue(row) { // Estimated value based on typical conversion rates const estimatedValue = (row.clicks || 0) * 0.02 * 50; // 2% conversion, $50 AOV return `$${estimatedValue.toFixed(0)}/month`; } function generateContentInsights(pages) { const insights = []; const highTrafficLowCTR = pages.filter(p => p.metrics.impressions > 1000 && parseFloat(p.metrics.ctr) < 2); if (highTrafficLowCTR.length > 0) { insights.push(`${highTrafficLowCTR.length} pages with high impressions but low CTR - major opportunity`); } const excellentPages = pages.filter(p => p.performance === 'excellent'); if (excellentPages.length > 0) { insights.push(`${excellentPages.length} top-performing pages - use as templates for new content`); } return insights; } function getCompetitorStrength(position) { if (position < 20) return 'weak'; if (position < 35) return 'moderate'; return 'strong'; } function getCompetitiveStrategy(row) { if (row.position < 15) { return 'Target with focused content campaign - winnable'; } if ((row.impressions || 0) > 1000) { return 'High-value keyword - invest in comprehensive guide'; } return 'Monitor and build domain authority first'; }