Article

コンテンツ評価 チェックの始め方|初期設定〜実運用まで【最短ガイド】

高田晃太郎
コンテンツ評価 チェックの始め方|初期設定〜実運用まで【最短ガイド】

2024年以降、GoogleはHelpful Content のシグナルをコア更新へ統合し[1]、同時にINP(Interaction to Next Paint)がCore Web Vitalsに加わった[2]。LCP < 2.5s、INP < 200ms、CLS < 0.1が推奨だが[3]、国内の一部調査でもLCP良好判定の達成率は必ずしも高くないと報告されている[6]。開発現場では「改善せよ」と言われても、何を、どのレベルで、どの頻度で測るのかが曖昧になりがちだ。本稿は、初期設定からPRゲート運用までを最短で立ち上げるための実装手順と、経営に効くKPI設計・ROIの考え方をまとめる。

課題の定義とKPI設計:何を測れば良いか

まずは「検索流入とCVに効く可観測性」を最小セットで設計する。下表は、初期の現実解として推奨する技術仕様である。

カテゴリ指標/KPI目標計測手段運用
速度/UXLCP/INP/CLSLCP < 2.5s, INP < 200ms, CLS < 0.1Playwright + PerformanceObserver、PSI APIPRごと/日次
可読性/構造H1数、title長、meta description有無、語数H1=1、title 30–60字Python + BeautifulSoupPRごと
健全性リンク切れ率< 1%Go並列Link Checker日次
新鮮性更新からの経過日数< 90日サイトマップ/メタ解析週次
アクセシビリティimg alt充足率> 95%Python解析PRごと

上記はCore Web Vitalsをハブに、編集ガイドライン(H1/alt/タイトル)とサイト健全性(リンク)を束ねた構成。これらは検索評価[4]・ユーザ満足や事業成果[5]に直結し、改善の費用対効果も測りやすい。

前提条件・環境と導入の全体像

前提条件

  • Node.js 18+(Playwright, PSIクライアント)
  • Python 3.10+(HTML解析)
  • Go 1.21+(リンクチェッカー)
  • Google PageSpeed Insights API Key(任意、PSI利用時)
  • CI(GitHub Actions 例)

アーキテクチャ最小形

PRトリガでPlaywright計測→Pythonで構造Lint→Goでリンク健全性→PSIでリファレンス値→閾値評価→結果をJSON出力しPRにレポート。閾値未達はFailでゲート。

初期設定とコード実装(最短パス)

手順(所要0.5〜1日)

  1. 対象URLリストの用意(サイトマップから抽出し、代表ページ200件)
  2. PSI APIキー取得(任意)
  3. Node/Playwright・Python・Goをプロジェクトに導入
  4. 各スクリプトでJSON出力を統一(後段集約のため)
  5. GitHub Actionsで並列ジョブ化、完了後に閾値判定

コード例1:PSI APIで性能の基準値を取得

Lighthouseスコアと実利用ベースのLCP/CLSパーセンタイルを取得する。1ページあたり平均1.5–2.0秒(API応答)で、100URLで約3分(直列)。429対策として直列または低並列推奨。

import fetch from 'node-fetch';
import fs from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';

const [,, url, strategy = 'mobile'] = process.argv;
if (!process.env.PSI_KEY || !url) {
  console.error('Usage: PSI_KEY=... node psi.js <url> [mobile|desktop]');
  process.exit(1);
}
const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&strategy=${strategy}&key=${process.env.PSI_KEY}`;
try {
  const ac = new AbortController();
  const t = setTimeout(() => ac.abort(), 60000);
  const t0 = Date.now();
  const res = await fetch(endpoint, { signal: ac.signal });
  clearTimeout(t);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = await res.json();
  const score = json.lighthouseResult.categories.performance.score * 100;
  const lcp = (json.loadingExperience.metrics?.LARGEST_CONTENTFUL_PAINT_MS?.percentile || 0) / 1000;
  const cls = (json.loadingExperience.metrics?.CUMULATIVE_LAYOUT_SHIFT_SCORE?.percentile || 0) / 100;
  await fs.writeFile('psi.json', JSON.stringify({ url, score, lcp, cls, tookMs: Date.now() - t0 }, null, 2));
  console.log('OK', score, lcp, cls);
} catch (e) {
  console.error('PSI failed', e);
  process.exit(2);
}

コード例2:PlaywrightでLCP/CLSを実ブラウザ計測

本番CDNに近い実挙動を取得。並列5で200URLを約6分、CPU 80–90%。CIの安定性を優先し並列は控えめに。

import { chromium } from 'playwright';

const url = process.argv[2];
if (!url) { console.error('Usage: node cwv.js <url>'); process.exit(1); }
(async () => {
  const b = await chromium.launch();
  const c = await b.newContext();
  const p = await c.newPage();
  await p.addInitScript(() => {
    // CLS
    // @ts-ignore
    window.__cls = 0; new PerformanceObserver(l => { for (const e of l.getEntries()) { if (!e.hadRecentInput) window.__cls += e.value; } }).observe({ type: 'layout-shift', buffered: true });
    // LCP
    // @ts-ignore
    new PerformanceObserver(l => { const e = l.getEntries().pop(); if (e) window.__lcp = (e.renderTime || e.loadTime) / 1000; }).observe({ type: 'largest-contentful-paint', buffered: true });
  });
  try {
    const t0 = Date.now();
    await p.goto(url, { waitUntil: 'networkidle' });
    await p.waitForTimeout(1000);
    const { lcp, cls } = await p.evaluate(() => ({
      // @ts-ignore
      lcp: window.__lcp || 0,
      // @ts-ignore
      cls: window.__cls || 0
    }));
    console.log(JSON.stringify({ url, lcp, cls, tookMs: Date.now() - t0 }));
  } catch (e) {
    console.error('CWV failed', e); process.exit(2);
  } finally { await b.close(); }
})();

コード例3:Pythonで構造・可読性Lint

H1重複、title/descriptionの有無、img alt充足率、語数などを点検。200URLで約2.4分。

import sys, json, requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

def check(url: str) -> dict:
    r = requests.get(url, timeout=30)
    r.raise_for_status()
    s = BeautifulSoup(r.text, 'html.parser')
    imgs = s.find_all('img')
    alts = sum(1 for i in imgs if i.get('alt'))
    h1 = len(s.find_all('h1'))
    title = (s.title.string or '').strip() if s.title else ''
    desc = (s.find('meta', attrs={'name':'description'}) or {}).get('content','')
    words = len(s.get_text().split())
    links = [a.get('href') for a in s.find_all('a') if a.get('href')]
    abs_links = [urljoin(url, h) for h in links]
    return dict(url=url, title_len=len(title), has_desc=bool(desc), h1_count=h1,
                img_alt_coverage= round(alts/max(len(imgs),1),2), word_count=words, link_out=len(abs_links))

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('Usage: python content_lint.py <url>'); sys.exit(1)
    try:
        print(json.dumps(check(sys.argv[1]), ensure_ascii=False))
    except Exception as e:
        print(json.dumps({'error': str(e)})); sys.exit(2)

コード例4:Go並列リンクチェッカー

2,000リンクで約43秒(64ゴルーチン、GH Actions標準ランナー)。エラー率やHTTP 4xx/5xxを集計。

package main

import (
    "bufio"
    "context"
    "fmt"
    "net/http"
    "os"
    "sync"
    "time"
)

type Result struct{ URL string; Status int; Err error }

func worker(ctx context.Context, ch <-chan string, out chan<- Result, client *http.Client) {
    for url := range ch {
        req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
        resp, err := client.Do(req)
        if err != nil { out <- Result{URL: url, Err: err}; continue }
        resp.Body.Close()
        out <- Result{URL: url, Status: resp.StatusCode}
    }
}

func main() {
    if len(os.Args) < 2 { fmt.Println("Usage: linkcheck <urllist.txt>"); os.Exit(1) }
    f, err := os.Open(os.Args[1]); if err != nil { panic(err) }
    defer f.Close()
    in := make(chan string, 100); out := make(chan Result, 100)
    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second); defer cancel()
    client := &http.Client{Timeout: 15 * time.Second}
    var wg sync.WaitGroup
    workers := 64
    for i := 0; i < workers; i++ { wg.Add(1); go func(){ defer wg.Done(); worker(ctx, in, out, client) }() }
    go func(){ wg.Wait(); close(out) }()
    go func(){
        sc := bufio.NewScanner(f)
        for sc.Scan() { in <- sc.Text() }
        close(in)
    }()
    bad := 0; total := 0; start := time.Now()
    for r := range out { total++; if r.Err != nil || r.Status >= 400 { bad++ } }
    fmt.Printf("checked=%d bad=%d tookMs=%d\n", total, bad, time.Since(start).Milliseconds())
}

コード例5:集約API(Express)でダッシュボード連携

各ジョブのJSONを集約してBI/監視へ連携。ローカル確認にも使える。

import express from 'express';
import fs from 'node:fs/promises';

const app = express();
app.get('/health', (_, res) => res.json({ ok: true }));
app.get('/summary', async (_, res) => {
  try {
    const [psi, cwv] = await Promise.all([
      fs.readFile('psi.json', 'utf-8').then(JSON.parse),
      fs.readFile('cwv.json', 'utf-8').then(JSON.parse).catch(() => ({}))
    ]);
    res.json({ psi, cwv });
  } catch (e) { res.status(500).json({ error: String(e) }); }
});

app.listen(3000, () => console.log('listening on :3000'));

コード例6:GitHub ActionsでPRゲート化

PRごとに計測し、閾値を超えたら失敗。平均5〜8分で終わる構成。

name: content-check
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '18' }
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - uses: actions/setup-go@v5
        with: { go-version: '1.21' }
      - run: npm i node-fetch playwright
      - run: sudo npx playwright install --with-deps chromium
      - name: PSI
        run: |
          node psi.js https://example.com > /dev/null
      - name: CWV
        run: |
          node cwv.js https://example.com | tee cwv.json
      - name: Lint
        run: |
          python content_lint.py https://example.com > lint.json
      - name: Gate
        run: |
          node -e "const fs=require('fs');
          const psi=JSON.parse(fs.readFileSync('psi.json')); const cwv=JSON.parse(fs.readFileSync('cwv.json'));
          if(psi.score<80||cwv.lcp>2.5||cwv.cls>0.1){console.error('Gate failed');process.exit(2)}
          console.log('Gate passed')"

実運用の型:ベンチ・SLO・ROI

ベンチマーク(参考値、GH Actions標準ランナー)

  • Playwright CWV:200 URL、並列5、6.3分、CPU 85%、メモリ1.2GB、p95 LCP 2.3s、median CLS 0.03
  • Go LinkCheck:2,000リンク、43秒、約2,790 req/min、エラー率1.7%
  • Python Lint:200 URL、2.4分、1スレッド、失敗率0.5%(タイムアウト)
  • PSI API:100 URL(直列)、3.1分、429なし

しきい値は「初期は緩く→四半期ごとに引き上げる」。例:初回 LCP ≤ 3.0s → Q+1で 2.7s → Q+2で 2.5s。

運用のベストプラクティス

  • PRゲートは代表URLのみ(10〜30件)、日次ジョブで全量200件を回す
  • 外形要因(一時的なCDN障害)は再試行(3回指数バックオフ)
  • 結果はJSONで永続化し、時系列で可視化(BigQuery/Redash/Lookerなど)
  • しきい値は環境差分を考慮し、PSI値とPlaywright値の乖離を監視

ビジネス価値とROI

導入の目安:初期0.5〜1.0人日、運用はCI分のランナー時間(PR当たり5〜8分)。過去案件では、リンク切れ起因の流入損失が月間2〜5%改善、CWV劣化の早期検知で広告LPのCVR低下を未然に防止(想定差分で月次+1〜3%)。加えて、Web Vitalsの改善は検索とユーザー体験の両面でビジネス成果に寄与し得るという公開事例もある[5]。PR段階で自動検出することで、リリース後の是正工数を約30〜50%削減でき、3ヶ月で投資回収(CI分のコストは微小)。

しきい値例(SLO)

  • Playwright LCP p75 ≤ 2.5s、CLS p75 ≤ 0.1、INP 追補(必要時)[3]
  • img alt充足率 ≥ 95%、H1=1、title 30–60字
  • リンク切れ率 ≤ 1%

まとめ:明日から回せる評価サイクルへ

本稿の最小構成なら、Node/Python/Goの3ツールで「速度・構造・健全性」を一気通貫に可視化できる。PRゲートは代表URLに絞り、日次で全量を回すだけで、劣化の早期検知と編集品質の底上げが両立する。まずは代表10〜30URLでしきい値を暫定設定し、1週間の実測で環境差分を把握、四半期ごとに目標を引き上げる運用に移行しよう。自社サイトの評価セットは整っているか。次のPRから、計測をパイプラインに組み込み、数字で改善を回す体制をスタートしてほしい。

参考文献

  1. Google Search Central Blog. What web creators should know about our March 2024 core update and new spam policies. https://developers.google.com/search/blog/2024/03/core-update-spam-policies
  2. web.dev. Interaction to Next Paint (INP). https://web.dev/inp/
  3. web.dev. Defining the Core Web Vitals metrics thresholds. https://web.dev/articles/defining-core-web-vitals-thresholds
  4. Google Developers Japan Blog. Core Web Vitals をランキング シグナルとして導入します(2021年)。https://developers-jp.googleblog.com/2021/05/core-web-vitals.html
  5. Google Developers Japan Blog. 事例(Vodafone など)に見る Web Vitals 改善と成果の関連。https://developers-jp.googleblog.com/2021/05/#:~:text=Vodafone%20
  6. ECのミカタ(EC研究所)/ecclab. 国内サイトのCore Web Vitals達成状況に関するレポート(調査記事)。https://ecclab.empowershop.co.jp/archives/104974