npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@akson/cortex-gsc

v0.5.0

Published

Google Search Console API client and MCP server for search analytics and performance data

Readme

@akson/api-gsc

TypeScript client for Google Search Console API, enabling programmatic SEO monitoring, performance analysis, crawl management, and search optimization.

User Stories

Site Management Stories

As an SEO Manager, I want to programmatically add new sites to Search Console, so that I can scale SEO monitoring across multiple properties.

As a Technical SEO Specialist, I want to verify site ownership automatically, so that I can streamline the onboarding process for new domains.

As a Multi-Site SEO Director, I want to list all sites in my Search Console account, so that I can audit and manage my property portfolio.

As a Digital Marketing Manager, I want to remove outdated sites from monitoring, so that I can keep my Search Console account organized.

Performance Analysis Stories

As an SEO Analyst, I want to retrieve search performance data by query, so that I can identify keyword opportunities and trends.

As a Content Marketing Manager, I want to analyze page performance data, so that I can optimize high-potential pages for better rankings.

As an SEO Director, I want to track click-through rates and impressions over time, so that I can measure the impact of SEO optimizations.

As a Digital Marketing Analyst, I want to segment performance by device and country, so that I can optimize for different user segments and markets.

Search Analytics Stories

As a Keyword Research Specialist, I want to export query performance data, so that I can identify long-tail keyword opportunities.

As an SEO Consultant, I want to compare performance across date ranges, so that I can measure the impact of SEO changes.

As a Content Strategist, I want to identify top-performing content by search metrics, so that I can replicate successful content patterns.

As a Technical SEO Manager, I want to analyze search appearance types (rich snippets, featured snippets), so that I can optimize for SERP features.

Crawl Management Stories

As a Technical SEO Specialist, I want to monitor crawl errors, so that I can quickly identify and fix indexing issues.

As a Site Reliability Engineer, I want to track crawl statistics, so that I can optimize server resources for search bot traffic.

As an SEO Manager, I want to identify blocked resources, so that I can ensure search bots can access important site assets.

As a Web Developer, I want to monitor crawl rate changes, so that I can adjust server capacity during high-traffic periods.

Sitemap Management Stories

As a Technical SEO Manager, I want to submit sitemaps programmatically, so that I can automate sitemap updates for dynamic content sites.

As an E-commerce SEO Specialist, I want to monitor sitemap processing status, so that I can ensure product pages are being discovered.

As a Content Management System Developer, I want to automatically resubmit sitemaps after content updates, so that I can ensure fresh content gets indexed quickly.

As an SEO Consultant, I want to track sitemap errors and warnings, so that I can maintain optimal site crawlability.

URL Inspection Stories

As a Technical SEO Analyst, I want to inspect specific URLs for indexing status, so that I can troubleshoot indexing issues.

As a Content Manager, I want to request indexing for new or updated pages, so that I can expedite their appearance in search results.

As an E-commerce Manager, I want to check product page indexing status, so that I can ensure new products are discoverable.

As a Website Owner, I want to validate page accessibility for search bots, so that I can ensure my content can be properly crawled.

Mobile Usability Stories

As a UX Designer, I want to monitor mobile usability issues, so that I can prioritize mobile optimization efforts.

As a Technical SEO Specialist, I want to track mobile-first indexing status, so that I can ensure mobile versions are properly optimized.

As a Web Developer, I want to identify pages with mobile usability problems, so that I can fix responsive design issues.

Security & Spam Stories

As a Website Security Manager, I want to monitor manual actions and penalties, so that I can quickly respond to security or quality issues.

As an SEO Manager, I want to track spam or hacking notifications, so that I can protect site reputation and rankings.

As a Brand Protection Specialist, I want to monitor for security issues across multiple properties, so that I can maintain brand integrity.

Reporting & Automation Stories

As an SEO Director, I want to generate automated SEO performance reports, so that I can provide regular stakeholder updates.

As a Marketing Operations Manager, I want to integrate Search Console data with other analytics platforms, so that I can create comprehensive performance dashboards.

As a Client Services Manager, I want to automatically alert clients about significant ranking changes, so that I can provide proactive SEO support.

Competitive Analysis Stories

As an SEO Consultant, I want to analyze search visibility trends, so that I can identify competitive opportunities and threats.

As a Digital Marketing Manager, I want to track branded search performance, so that I can measure brand awareness and reputation.

As a Market Research Analyst, I want to analyze search trends by geography, so that I can identify expansion opportunities.

Installation

npm install @akson/api-gsc

Quick Start

import { GSCClient } from '@akson/api-gsc';

const client = new GSCClient({
  config: {
    siteUrl: 'https://example.com',
    serviceAccount: {
      keyFile: 'path/to/service-account.json',
      email: '[email protected]'
    }
  }
});

// Authenticate and get sites
await client.authenticate();
const sites = await client.getSites();

// Get search analytics data
const searchData = await client.getSearchAnalytics({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  dimensions: ['query', 'page'],
  metrics: ['clicks', 'impressions', 'ctr', 'position']
});

// Check URL indexing status
const urlInspection = await client.inspectUrl('https://example.com/important-page');

// Submit sitemap
await client.submitSitemap('https://example.com/sitemap.xml');

Configuration

Environment Variables

GSC_SITE_URL=https://example.com
GSC_SERVICE_ACCOUNT_EMAIL=gsc-service@project.iam.gserviceaccount.com
GSC_SERVICE_ACCOUNT_KEY_FILE=path/to/key.json

Configuration File

Create gsc-config.json:

{
  "siteUrl": "https://example.com",
  "serviceAccount": {
    "email": "[email protected]",
    "keyFile": "path/to/service-account.json"
  }
}

API Reference

Authentication

// Authenticate with service account
const authResult = await client.authenticate();
if (!authResult.success) {
  throw new Error(authResult.error);
}

Site Management

// Get all sites
const sitesResult = await client.getSites();
if (sitesResult.success) {
  sitesResult.data.forEach(site => {
    console.log(`Site: ${site.siteUrl}, Permission: ${site.permissionLevel}`);
  });
}

// Add new site
await client.addSite('https://newsite.com');

// Delete site
await client.deleteSite('https://oldsite.com');

Search Analytics

// Basic search analytics
const analyticsResult = await client.getSearchAnalytics({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  dimensions: ['query'],
  metrics: ['clicks', 'impressions', 'ctr', 'position'],
  rowLimit: 1000
});

// Search analytics with filters
const filteredAnalytics = await client.getSearchAnalytics({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  dimensions: ['query', 'page'],
  filters: [
    {
      dimension: 'country',
      operator: 'equals',
      expression: 'USA'
    },
    {
      dimension: 'device',
      operator: 'equals',
      expression: 'MOBILE'
    }
  ]
});

// Performance by page
const pagePerformance = await client.getPagePerformance({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  page: '/product-category',
  metrics: ['clicks', 'impressions', 'ctr', 'position']
});

// Query performance
const queryPerformance = await client.getQueryPerformance({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  query: 'best running shoes',
  groupBy: ['page', 'device']
});

URL Inspection

// Inspect URL indexing status
const inspectionResult = await client.inspectUrl('https://example.com/product/123');
if (inspectionResult.success) {
  const status = inspectionResult.data;
  console.log('Indexing status:', status.indexStatusResult?.verdict);
  console.log('Coverage state:', status.indexStatusResult?.coverageState);
  console.log('Last crawl time:', status.indexStatusResult?.lastCrawlTime);
}

// Request indexing for URL
const indexingRequest = await client.requestIndexing('https://example.com/new-product');
if (indexingRequest.success) {
  console.log('Indexing requested successfully');
}

Sitemap Management

// List sitemaps
const sitemapsResult = await client.getSitemaps();
if (sitemapsResult.success) {
  sitemapsResult.data.forEach(sitemap => {
    console.log(`Sitemap: ${sitemap.path}, Status: ${sitemap.type}`);
    console.log(`Submitted: ${sitemap.submitted}, Indexed: ${sitemap.indexed}`);
  });
}

// Submit sitemap
await client.submitSitemap('https://example.com/sitemap.xml');

// Delete sitemap
await client.deleteSitemap('https://example.com/old-sitemap.xml');

// Get sitemap status
const sitemapStatus = await client.getSitemapStatus('https://example.com/sitemap.xml');

Mobile Usability

// Get mobile usability issues
const mobileIssues = await client.getMobileUsabilityIssues();
if (mobileIssues.success) {
  mobileIssues.data.forEach(issue => {
    console.log(`Issue: ${issue.issueType}`);
    console.log(`Severity: ${issue.severity}`);
    console.log(`Sample URLs: ${issue.sampleUrls?.length || 0}`);
  });
}

Messages & Manual Actions

// Get messages (manual actions, security issues, etc.)
const messagesResult = await client.getMessages();
if (messagesResult.success) {
  messagesResult.data.forEach(message => {
    console.log(`Message: ${message.subject}`);
    console.log(`Type: ${message.messageType}`);
    console.log(`Date: ${message.publishTime}`);
  });
}

// Mark message as read
await client.markMessageAsRead('message-id');

Error Handling

All methods return GSCOperationResult<T>:

const result = await client.getSearchAnalytics({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
  dimensions: ['query']
});

if (result.success) {
  console.log('Data retrieved:', result.data.length, 'rows');
  console.log('Total clicks:', result.data.reduce((sum, row) => sum + row.clicks, 0));
} else {
  console.error('Error retrieving data:', result.error);
  if (result.details) {
    console.error('API Details:', result.details);
  }
}

Advanced Usage

SEO Performance Monitoring

class SEOMonitor {
  constructor(private client: GSCClient) {}

  async dailyPerformanceCheck() {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    const yesterdayStr = yesterday.toISOString().split('T')[0];

    const performance = await this.client.getSearchAnalytics({
      startDate: yesterdayStr,
      endDate: yesterdayStr,
      dimensions: ['query'],
      metrics: ['clicks', 'impressions', 'ctr', 'position'],
      rowLimit: 100
    });

    if (performance.success) {
      const totalClicks = performance.data.reduce((sum, row) => sum + row.clicks, 0);
      const avgPosition = performance.data.reduce((sum, row) => sum + row.position, 0) / performance.data.length;

      console.log(`📊 Yesterday's Performance:`);
      console.log(`- Total clicks: ${totalClicks}`);
      console.log(`- Average position: ${avgPosition.toFixed(1)}`);
      
      // Alert on significant changes
      if (totalClicks < 100) {
        console.warn('⚠️ Low traffic alert: Less than 100 clicks yesterday');
      }
    }
  }

  async trackKeywordRankings(keywords: string[]) {
    const results = [];

    for (const keyword of keywords) {
      const performance = await this.client.getQueryPerformance({
        startDate: '2025-01-01',
        endDate: '2025-01-31',
        query: keyword
      });

      if (performance.success && performance.data.length > 0) {
        results.push({
          keyword,
          position: performance.data[0].position,
          clicks: performance.data[0].clicks,
          impressions: performance.data[0].impressions
        });
      }
    }

    return results;
  }
}

Automated SEO Reporting

class SEOReporter {
  constructor(private client: GSCClient) {}

  async generateMonthlyReport(year: number, month: number) {
    const startDate = new Date(year, month - 1, 1).toISOString().split('T')[0];
    const endDate = new Date(year, month, 0).toISOString().split('T')[0];

    // Get overall performance
    const performance = await this.client.getSearchAnalytics({
      startDate,
      endDate,
      dimensions: ['date'],
      metrics: ['clicks', 'impressions', 'ctr', 'position']
    });

    // Get top queries
    const topQueries = await this.client.getSearchAnalytics({
      startDate,
      endDate,
      dimensions: ['query'],
      metrics: ['clicks', 'impressions', 'ctr', 'position'],
      rowLimit: 50
    });

    // Get top pages
    const topPages = await this.client.getSearchAnalytics({
      startDate,
      endDate,
      dimensions: ['page'],
      metrics: ['clicks', 'impressions', 'ctr', 'position'],
      rowLimit: 50
    });

    const report = {
      period: `${year}-${month.toString().padStart(2, '0')}`,
      summary: {
        totalClicks: performance.success ? performance.data.reduce((sum, d) => sum + d.clicks, 0) : 0,
        totalImpressions: performance.success ? performance.data.reduce((sum, d) => sum + d.impressions, 0) : 0,
        avgCTR: 0,
        avgPosition: 0
      },
      topQueries: topQueries.success ? topQueries.data.slice(0, 20) : [],
      topPages: topPages.success ? topPages.data.slice(0, 20) : []
    };

    if (performance.success && performance.data.length > 0) {
      report.summary.avgCTR = performance.data.reduce((sum, d) => sum + d.ctr, 0) / performance.data.length;
      report.summary.avgPosition = performance.data.reduce((sum, d) => sum + d.position, 0) / performance.data.length;
    }

    return report;
  }

  async comparePerformance(period1: string, period2: string) {
    const [start1, end1] = period1.split(' to ');
    const [start2, end2] = period2.split(' to ');

    const performance1 = await this.client.getSummaryData(start1, end1);
    const performance2 = await this.client.getSummaryData(start2, end2);

    if (performance1.success && performance2.success) {
      return {
        clicksChange: ((performance2.data.totalClicks - performance1.data.totalClicks) / performance1.data.totalClicks) * 100,
        impressionsChange: ((performance2.data.totalImpressions - performance1.data.totalImpressions) / performance1.data.totalImpressions) * 100,
        ctrChange: performance2.data.averageCTR - performance1.data.averageCTR,
        positionChange: performance2.data.averagePosition - performance1.data.averagePosition
      };
    }

    return null;
  }
}

Content Optimization Insights

class ContentOptimizer {
  constructor(private client: GSCClient) {}

  async findOptimizationOpportunities() {
    // Get pages with high impressions but low CTR
    const analytics = await this.client.getSearchAnalytics({
      startDate: '2025-01-01',
      endDate: '2025-01-31',
      dimensions: ['page', 'query'],
      metrics: ['clicks', 'impressions', 'ctr', 'position'],
      rowLimit: 1000
    });

    if (!analytics.success) return [];

    const opportunities = analytics.data
      .filter(row => row.impressions > 100 && row.ctr < 0.02) // High impressions, low CTR
      .sort((a, b) => b.impressions - a.impressions)
      .map(row => ({
        page: row.page,
        query: row.query,
        impressions: row.impressions,
        ctr: (row.ctr * 100).toFixed(2) + '%',
        position: row.position.toFixed(1),
        opportunity: 'Low CTR despite high impressions - optimize title/meta description'
      }));

    return opportunities.slice(0, 20);
  }

  async findRankingOpportunities() {
    const analytics = await this.client.getSearchAnalytics({
      startDate: '2025-01-01',
      endDate: '2025-01-31',
      dimensions: ['page', 'query'],
      metrics: ['clicks', 'impressions', 'ctr', 'position'],
      rowLimit: 1000
    });

    if (!analytics.success) return [];

    const opportunities = analytics.data
      .filter(row => row.position > 10 && row.position <= 20 && row.impressions > 50) // Page 2 rankings
      .sort((a, b) => b.impressions - a.impressions)
      .map(row => ({
        page: row.page,
        query: row.query,
        position: row.position.toFixed(1),
        impressions: row.impressions,
        opportunity: 'Page 2 ranking with decent impressions - optimize for first page'
      }));

    return opportunities.slice(0, 20);
  }
}

Site Health Monitoring

class SiteHealthMonitor {
  constructor(private client: GSCClient) {}

  async checkSiteHealth() {
    const health = {
      indexing: { status: 'unknown', issues: [] },
      mobile: { status: 'unknown', issues: [] },
      sitemaps: { status: 'unknown', issues: [] },
      security: { status: 'unknown', issues: [] }
    };

    // Check for crawl errors
    const crawlErrors = await this.client.getCrawlErrorsCount();
    if (crawlErrors.success) {
      const totalErrors = Object.values(crawlErrors.data).reduce((sum, count) => sum + count, 0);
      health.indexing.status = totalErrors === 0 ? 'healthy' : 'issues';
      if (totalErrors > 0) {
        health.indexing.issues.push(`${totalErrors} crawl errors found`);
      }
    }

    // Check mobile usability
    const mobileIssues = await this.client.getMobileUsabilityIssues();
    if (mobileIssues.success) {
      health.mobile.status = mobileIssues.data.length === 0 ? 'healthy' : 'issues';
      health.mobile.issues = mobileIssues.data.map(issue => issue.issueType);
    }

    // Check sitemaps
    const sitemaps = await this.client.getSitemaps();
    if (sitemaps.success) {
      const hasErrors = sitemaps.data.some(sitemap => sitemap.errors && sitemap.errors > 0);
      health.sitemaps.status = !hasErrors ? 'healthy' : 'issues';
      if (hasErrors) {
        health.sitemaps.issues = sitemaps.data
          .filter(s => s.errors && s.errors > 0)
          .map(s => `${s.path}: ${s.errors} errors`);
      }
    }

    // Check for security issues
    const messages = await this.client.getMessages();
    if (messages.success) {
      const securityMessages = messages.data.filter(m => 
        m.messageType === 'SECURITY_AND_MANUAL_ACTION'
      );
      health.security.status = securityMessages.length === 0 ? 'healthy' : 'critical';
      health.security.issues = securityMessages.map(m => m.subject);
    }

    return health;
  }
}

Integration Examples

SEO Dashboard Integration

import { GSCClient } from '@akson/api-gsc';
import { createDashboard } from './dashboard';

async function updateSEODashboard() {
  const client = new GSCClient();
  await client.authenticate();

  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  const performance = await client.getSearchAnalytics({
    startDate: thirtyDaysAgo.toISOString().split('T')[0],
    endDate: new Date().toISOString().split('T')[0],
    dimensions: ['date'],
    metrics: ['clicks', 'impressions', 'ctr', 'position']
  });

  if (performance.success) {
    await createDashboard({
      totalClicks: performance.data.reduce((sum, d) => sum + d.clicks, 0),
      totalImpressions: performance.data.reduce((sum, d) => sum + d.impressions, 0),
      avgCTR: (performance.data.reduce((sum, d) => sum + d.ctr, 0) / performance.data.length) * 100,
      avgPosition: performance.data.reduce((sum, d) => sum + d.position, 0) / performance.data.length
    });
  }
}

Alert System

class SEOAlertSystem {
  constructor(private client: GSCClient, private thresholds: any) {}

  async checkAlerts() {
    const alerts = [];

    // Check for traffic drops
    const lastWeek = await this.getWeeklyData(-1);
    const previousWeek = await this.getWeeklyData(-2);

    if (lastWeek && previousWeek) {
      const trafficChange = ((lastWeek.clicks - previousWeek.clicks) / previousWeek.clicks) * 100;
      
      if (trafficChange < this.thresholds.trafficDropPercent) {
        alerts.push({
          type: 'traffic_drop',
          severity: 'high',
          message: `Traffic dropped by ${Math.abs(trafficChange).toFixed(1)}% last week`,
          data: { lastWeek: lastWeek.clicks, previousWeek: previousWeek.clicks }
        });
      }
    }

    // Check for ranking drops
    const rankingIssues = await this.checkRankingDrops();
    alerts.push(...rankingIssues);

    return alerts;
  }

  private async getWeeklyData(weeksAgo: number) {
    const endDate = new Date();
    endDate.setDate(endDate.getDate() + (weeksAgo * 7));
    const startDate = new Date(endDate);
    startDate.setDate(startDate.getDate() - 6);

    const result = await this.client.getSummaryData(
      startDate.toISOString().split('T')[0],
      endDate.toISOString().split('T')[0]
    );

    return result.success ? result.data : null;
  }
}

TypeScript Support

Full TypeScript definitions included:

import type {
  GSCSite,
  GSCSearchAnalyticsResponse,
  GSCPerformanceData,
  GSCSitemap,
  GSCUrlInspectionResponse,
  GSCMobileUsabilityIssue,
  GSCMessage,
  GSCOperationResult,
  GSCListResult
} from '@akson/api-gsc';

Requirements

  • Node.js ≥18.0.0
  • Google Search Console API access
  • Service account with Search Console permissions
  • Verified site ownership in Google Search Console

License

MIT