mcpField
S
seo-analytics

seo-analytics

github.com/rebyteai-templateマーケティング・グロース1768 回インストール19 週間前に更新
リポジトリ

seo-analytics について

Webサイト運営者や分析担当者向けの SEO 分析スキルです。Google PageSpeed Insights API を使って性能と SEO を調査し、API キーを用いてコンソール概要と操作可能な HTML レポートを生成します。

主な機能

  1. 01性能、アクセシビリティ、ベストプラクティス、SEO の分析
  2. 02Core Web Vitals の評価
  3. 03モバイルとデスクトップの指標表示
  4. 04操作可能な HTML グラフレポートの生成
  5. 05Google PageSpeed Insights API キーが必要

活用シーン

  1. 01Webページの速度確認
  2. 02Core Web Vitals レポートの作成
  3. 03SEO と Lighthouse 指標の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rebyteai-template/rebyte-skills --skill seo-analytics

インストール後、Claude Code で/seo-analyticsを入力して読み込まれたか確認します。

同梱ファイルanalyze.tsREADME.md

詳細

SEO 分析スキル

Google PageSpeed Insights API を使用して、あらゆるウェブサイトの包括的な SEO およびパフォーマンス分析レポートを生成します。

使用するタイミング

  • 「https://example.com の SEO を分析して」
  • 「ウェブサイトのパフォーマンスを確認して」
  • 「Core Web Vitals レポートを生成して……」
  • 「……のページ速度スコアは?」
  • 「……の SEO 監査をして」

API キーの要件

重要:このスキルには Google PageSpeed Insights API キーが必要です。ユーザーから API キーが提供されていない場合は、先に必ずキーを求めてください。

API キーが提供されていない場合

ユーザーが API キーを提供せずに SEO 分析を依頼した場合は、次のように応答してください。


SEO 分析を実行するには、Google PageSpeed Insights API キーが必要です。

API キーの取得方法(約 2 分):

  1. Google Cloud Console にアクセスします
  2. 新しいプロジェクトを作成するか、既存のプロジェクトを選択します
  3. APIs & Services > Library に移動します
  4. "PageSpeed Insights API" を検索し、Enable をクリックします
  5. APIs & Services > Credentials に移動します
  6. Create Credentials > API Key をクリックします
  7. 新しい API キーをコピーします

キーを取得したら、提供してください。分析を実行します。


API キーの使用方法

API キーは次の 2 つの方法で指定できます。

  1. コマンドライン引数として指定:npx ts-node analyze.ts <URL> <API_KEY>
  2. 環境変数として指定:PAGESPEED_API_KEY

クイックスタート

User: "Analyze https://eng0.ai"

# If no API key provided, ask user for one first

# With API key:
Output:
- ./seo-report.html (interactive report with charts)
- Console summary with key metrics

実行方法

ステップ 1:分析スクリプトを実行する
npx ts-node analyze.ts https://example.com YOUR_API_KEY

または、環境変数を使う場合:

PAGESPEED_API_KEY=your_key npx ts-node analyze.ts https://example.com
ステップ 2:インラインで実行する(別の方法)

現在のディレクトリに analyze.ts を作成して実行します。

// analyze.ts - SEO Analysis Script
const url = process.argv[2];
const apiKey = process.argv[3] || process.env.PAGESPEED_API_KEY;

if (!url) {
  console.error('Usage: npx ts-node analyze.ts <URL> [API_KEY]');
  console.error('Or set PAGESPEED_API_KEY environment variable');
  process.exit(1);
}

interface AuditResult {
  title?: string;
  description?: string;
  score?: number | null;
  displayValue?: string;
  numericValue?: number;
}

interface PageSpeedResult {
  lighthouseResult: {
    categories: {
      performance: { score: number };
      accessibility: { score: number };
      'best-practices': { score: number };
      seo: { score: number };
    };
    audits: {
      [key: string]: AuditResult;
    };
  };
}

async function analyzeUrl(targetUrl: string, strategy: 'mobile' | 'desktop'): Promise<PageSpeedResult> {
  let apiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(targetUrl)}&strategy=${strategy}&category=performance&category=accessibility&category=best-practices&category=seo`;

  if (apiKey) {
    apiUrl += `&key=${apiKey}`;
  }

  console.log(`Analyzing ${strategy}...`);
  const response = await fetch(apiUrl);
  if (!response.ok) {
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }
  return response.json() as Promise<PageSpeedResult>;
}

function getScoreColor(score: number): string {
  if (score >= 90) return '#0cce6b';
  if (score >= 50) return '#ffa400';
  return '#ff4e42';
}

function generateReport(mobile: PageSpeedResult, desktop: PageSpeedResult, targetUrl: string): string {
  const m = mobile.lighthouseResult;
  const d = desktop.lighthouseResult;

  const mobileScores = {
    performance: Math.round(m.categories.performance.score * 100),
    accessibility: Math.round(m.categories.accessibility.score * 100),
    bestPractices: Math.round(m.categories['best-practices'].score * 100),
    seo: Math.round(m.categories.seo.score * 100),
  };

  const desktopScores = {
    performance: Math.round(d.categories.performance.score * 100),
    accessibility: Math.round(d.categories.accessibility.score * 100),
    bestPractices: Math.round(d.categories['best-practices'].score * 100),
    seo: Math.round(d.categories.seo.score * 100),
  };

  const webVitals = {
    mobile: {
      lcp: m.audits['largest-contentful-paint'],
      tbt: m.audits['total-blocking-time'],
      cls: m.audits['cumulative-layout-shift'],
      fcp: m.audits['first-contentful-paint'],
      si: m.audits['speed-index'],
      tti: m.audits['interactive'],
    },
    desktop: {
      lcp: d.audits['largest-contentful-paint'],
      tbt: d.audits['total-blocking-time'],
      cls: d.audits['cumulative-layout-shift'],
      fcp: d.audits['first-contentful-paint'],
      si: d.audits['speed-index'],
      tti: d.audits['interactive'],
    },
  };

  // Extract top opportunities
  const opportunities: string[] = [];
  for (const [key, audit] of Object.entries(m.audits)) {
    if (audit.score !== undefined && audit.score < 0.9 && audit.title && audit.description) {
      opportunities.push(`<li><strong>${audit.title}</strong>: ${audit.description.split('.')[0]}.</li>`);
    }
  }

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SEO Report - ${targetUrl}</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; line-height: 1.6; }
    .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
    h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
    .url { color: #666; font-size: 0.9rem; margin-bottom: 2rem; word-break: break-all; }
    .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
    .card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .card h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #444; }
    .scores { display: flex; justify-content: space-around; text-align: center; }
    .score-item { display: flex; flex-direction: column; align-items: center; }
    .score-circle { width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; font-weight: bold; color: white; margin-bottom: 0.5rem; }
    .score-label { font-size: 0.75rem; color: #666; }
    .vitals-table { width: 100%; border-collapse: collapse; }
    .vitals-table th, .vitals-table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #eee; }
    .vitals-table th { font-weight: 500; color: #666; font-size: 0.85rem; }
    .good { color: #0cce6b; }
    .needs-improvement { color: #ffa400; }
    .poor { color: #ff4e42; }
    .chart-container { position: relative; height: 300px; }
    .opportunities { list-style: none; }
    .opportunities li { padding: 0.75rem 0; border-bottom: 1px solid #eee; font-size: 0.9rem; }
    .opportunities li:last-child { border-bottom: none; }
    .timestamp { text-align: center; color: #999; font-size: 0.8rem; margin-top: 2rem; }
  </style>
</head>
<body>
  <div class="container">
    <h1>SEO Analysis Report</h1>
    <p class="url">${targetUrl}</p>

    <div class="grid">
      <div class="card">
        <h2>Mobile Scores</h2>
        <div class="scores">
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(mobileScores.performance)}">${mobileScores.performance}</div>
            <span class="score-label">Performance</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(mobileScores.accessibility)}">${mobileScores.accessibility}</div>
            <span class="score-label">Accessibility</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(mobileScores.bestPractices)}">${mobileScores.bestPractices}</div>
            <span class="score-label">Best Practices</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(mobileScores.seo)}">${mobileScores.seo}</div>
            <span class="score-label">SEO</span>
          </div>
        </div>
      </div>

      <div class="card">
        <h2>Desktop Scores</h2>
        <div class="scores">
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(desktopScores.performance)}">${desktopScores.performance}</div>
            <span class="score-label">Performance</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(desktopScores.accessibility)}">${desktopScores.accessibility}</div>
            <span class="score-label">Accessibility</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(desktopScores.bestPractices)}">${desktopScores.bestPractices}</div>
            <span class="score-label">Best Practices</span>
          </div>
          <div class="score-item">
            <div class="score-circle" style="background: ${getScoreColor(desktopScores.seo)}">${desktopScores.seo}</div>
            <span class="score-label">SEO</span>
          </div>
        </div>
      </div>
    </div>

    <div class="grid">
      <div class="card">
        <h2>Core Web Vitals - Mobile</h2>
        <table class="vitals-table">
          <tr><th>Metric</th><th>Value</th><th>Status</th></tr>
          <tr>
            <td>Largest Contentful Paint (LCP)</td>
            <td>${webVitals.mobile.lcp?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.lcp?.numericValue || 0) <= 2500 ? 'good' : (webVitals.mobile.lcp?.numericValue || 0) <= 4000 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.lcp?.numericValue || 0) <= 2500 ? 'Good' : (webVitals.mobile.lcp?.numericValue || 0) <= 4000 ? 'Needs Work' : 'Poor'}</td>
          </tr>
          <tr>
            <td>Total Blocking Time (TBT)</td>
            <td>${webVitals.mobile.tbt?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.tbt?.numericValue || 0) <= 200 ? 'good' : (webVitals.mobile.tbt?.numericValue || 0) <= 600 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.tbt?.numericValue || 0) <= 200 ? 'Good' : (webVitals.mobile.tbt?.numericValue || 0) <= 600 ? 'Needs Work' : 'Poor'}</td>
          </tr>
          <tr>
            <td>Cumulative Layout Shift (CLS)</td>
            <td>${webVitals.mobile.cls?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.cls?.numericValue || 0) <= 0.1 ? 'good' : (webVitals.mobile.cls?.numericValue || 0) <= 0.25 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.cls?.numericValue || 0) <= 0.1 ? 'Good' : (webVitals.mobile.cls?.numericValue || 0) <= 0.25 ? 'Needs Work' : 'Poor'}</td>
          </tr>
          <tr>
            <td>First Contentful Paint (FCP)</td>
            <td>${webVitals.mobile.fcp?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.fcp?.numericValue || 0) <= 1800 ? 'good' : (webVitals.mobile.fcp?.numericValue || 0) <= 3000 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.fcp?.numericValue || 0) <= 1800 ? 'Good' : (webVitals.mobile.fcp?.numericValue || 0) <= 3000 ? 'Needs Work' : 'Poor'}</td>
          </tr>
          <tr>
            <td>Speed Index</td>
            <td>${webVitals.mobile.si?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.si?.numericValue || 0) <= 3400 ? 'good' : (webVitals.mobile.si?.numericValue || 0) <= 5800 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.si?.numericValue || 0) <= 3400 ? 'Good' : (webVitals.mobile.si?.numericValue || 0) <= 5800 ? 'Needs Work' : 'Poor'}</td>
          </tr>
          <tr>
            <td>Time to Interactive (TTI)</td>
            <td>${webVitals.mobile.tti?.displayValue || 'N/A'}</td>
            <td class="${(webVitals.mobile.tti?.numericValue || 0) <= 3800 ? 'good' : (webVitals.mobile.tti?.numericValue || 0) <= 7300 ? 'needs-improvement' : 'poor'}">${(webVitals.mobile.tti?.numericValue || 0) <= 3800 ? 'Good' : (webVitals.mobile.tti?.numericValue || 0) <= 7300 ? 'Needs Work' : 'Poor'}</td>
          </tr>
        </table>
      </div>

      <div class="card">
        <h2>Score Comparison</h2>
        <div class="chart-container">
          <canvas id="comparisonChart"></canvas>
        </div>
      </div>
    </div>

    <div class="card">
      <h2>Optimization Opportunities</h2>
      <ul class="opportunities">
        ${opportunities.slice(0, 10).join('\n        ') || '<li>No significant issues found. Great job!</li>'}
      </ul>
    </div>

    <p class="timestamp">Generated on ${new Date().toLocaleString()}</p>
  </div>

  <script>
    new Chart(document.getElementById('comparisonChart'), {
      type: 'radar',
      data: {
        labels: ['Performance', 'Accessibility', 'Best Practices', 'SEO'],
        datasets: [
          {
            label: 'Mobile',
            data: [${mobileScores.performance}, ${mobileScores.accessibility}, ${mobileScores.bestPractices}, ${mobileScores.seo}],
            borderColor: '#4285f4',
            backgroundColor: 'rgba(66, 133, 244, 0.2)',
          },
          {
            label: 'Desktop',
            data: [${desktopScores.performance}, ${desktopScores.accessibility}, ${desktopScores.bestPractices}, ${desktopScores.seo}],
            borderColor: '#34a853',
            backgroundColor: 'rgba(52, 168, 83, 0.2)',
          }
        ]
      },
      options: {
        scales: { r: { beginAtZero: true, max: 100 } },
        plugins: { legend: { position: 'bottom' } }
      }
    });
  </script>
</body>
</html>`;
}

async function main() {
  try {
    console.log('Starting SEO analysis for:', url);
    console.log('');

    // Sequential requests to avoid rate limiting
    const mobile = await analyzeUrl(url, 'mobile');
    const desktop = await analyzeUrl(url, 'desktop');

    const report = generateReport(mobile, desktop, url);

    const fs = await import('fs');
    const outputPath = './seo-report.html';
    fs.writeFileSync(outputPath, report);

    // Print summary
    const m = mobile.lighthouseResult;
    const d = desktop.lighthouseResult;

    console.log('');
    console.log('=== SEO Analysis Complete ===');
    console.log('');
    console.log('Mobile Scores:');
    console.log(`  Performance:    ${Math.round(m.categories.performance.score * 100)}/100`);
    console.log(`  Accessibility:  ${Math.round(m.categories.accessibility.score * 100)}/100`);
    console.log(`  Best Practices: ${Math.round(m.categories['best-practices'].score * 100)}/100`);
    console.log(`  SEO:            ${Math.round(m.categories.seo.score * 100)}/100`);
    console.log('');
    console.log('Desktop Scores:');
    console.log(`  Performance:    ${Math.round(d.categories.performance.score * 100)}/100`);
    console.log(`  Accessibility:  ${Math.round(d.categories.accessibility.score * 100)}/100`);
    console.log(`  Best Practices: ${Math.round(d.categories['best-practices'].score * 100)}/100`);
    console.log(`  SEO:            ${Math.round(d.categories.seo.score * 100)}/100`);
    console.log('');
    console.log('Core Web Vitals (Mobile):');
    console.log(`  LCP: ${m.audits['largest-contentful-paint']?.displayValue || 'N/A'}`);
    console.log(`  TBT: ${m.audits['total-blocking-time']?.displayValue || 'N/A'}`);
    console.log(`  CLS: ${m.audits['cumulative-layout-shift']?.displayValue || 'N/A'}`);
    console.log('');
    console.log(`Report saved to: ${outputPath}`);
    console.log('Open the HTML file in a browser to view the interactive report.');

  } catch (error) {
    console.error('Error:', error);
    process.exit(1);
  }
}

main();

次のコマンドで実行します。

npx ts-node analyze.ts https://eng0.ai YOUR_API_KEY

出力

このスキルでは、次の内容が生成されます。

  1. コンソールの概要 - すべてのスコアの概要をすばやく確認できます
  2. seo-report.html - 次の内容を含むインタラクティブな HTML レポート:
    • モバイルとデスクトップのスコアカード
    • ステータスインジケーター付き Core Web Vitals 表
    • モバイルとデスクトップを比較するレーダーチャート
    • 優先度の高い最適化の機会

指標の説明

Lighthouse スコア(0~100)
  • パフォーマンス:ページの読み込み速度と操作性
  • アクセシビリティ:障害のあるユーザーにとってのページの利用しやすさ
  • ベストプラクティス:最新のウェブ開発標準
  • SEO:検索エンジン最適化の基本
Core Web Vitals
指標良好要改善不良
LCP(Largest Contentful Paint)< 2.5s2.5-4s> 4s
TBT(Total Blocking Time)< 200ms200-600ms> 600ms
CLS(Cumulative Layout Shift)< 0.10.1-0.25> 0.25

API の詳細

Google PageSpeed Insights API v5 を使用します。

  • 無料:基本的な使用に API キーは不要です
  • レート制限:100 秒あたり約 25 リクエスト
  • データ:Lighthouse の実際の監査結果

出力例

=== SEO Analysis Complete ===

Mobile Scores:
  Performance:    89/100
  Accessibility:  95/100
  Best Practices: 100/100
  SEO:            91/100

Core Web Vitals (Mobile):
  LCP: 1.8 s
  TBT: 150 ms
  CLS: 0.02

Report saved to: ./seo-report.html

コメント