Claude Codeで未レビューのサイト変更を避けてPageSpeed SEO監査を実行する方法
Claude Codeは、PageSpeedの結果が実装上の確認箇所を示した後に特に役立ちます。リポジトリを調べ、繰り返し現れる監査結果をテンプレートやアセットパイプラインに結び付け、テスト可能なパッチを準備できます。ただし、その力には明確な関門が必要です。証拠収集は読み取り専用で行い、サイト変更は人が対象を絞った計画を承認してから始めます。
このチュートリアルでは、リポジトリにローカルSkill、証拠ディレクトリ、短いポリシーを追加します。目標は自動最適化ボットではありません。PageSpeed Insightsの証拠から、承認済みでレビュー可能なdiffまでを繰り返し辿れる流れです。
この記事をClaude Codeへ送ってSkillを作成する
記事の公開URLが用意できたら、次の依頼をClaude Codeに貼り付けてください。サイトをテストしたり、アプリケーションコードを変えたりせずに、このワークフローだけをインストールします。
Read [THIS ARTICLE URL] and install its project-local PageSpeed audit workflow.
First inspect this repository for CLAUDE.md, CLAUDE.local.md, .claude rules,
and project conventions. Explain where these files will go before writing them:
.claude/skills/pagespeed-evidence/SKILL.md
.claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.py
.claude/rules/page-speed-audits.md
Create each file from the complete code blocks in the article. Do not change
application code, package files, lock files, CI, infrastructure, or deployment
configuration. Do not call PageSpeed Insights or inspect public URLs.
Run a Python syntax check on the runner. Report the paths, confirm audit output
is ignored by Git, and tell me to configure PAGESPEED_API_KEY in my approved
local secret environment without revealing or requesting the value. Stop there.
Claude Codeでは、CLAUDE.mdと.claudeのファイルを継続的なプロジェクト指示として使えます。これは有用なコンテキストですが、それだけで安全境界になるわけではありません。エージェントの判断に関係なく特定のツール操作を必ず止める必要があるチームは、現在の公式ドキュメントに従ってClaude Codeの権限またはPreToolUseフックを使ってください。以下のワークフローは監査コマンドを読み取り専用に保ち、編集の前に別途承認を求めます。
完成条件とリポジトリ境界
項目 | 完成条件 |
|---|---|
対象読者 | Webサイトのリポジトリで作業する開発者、SEO責任者、技術コンテンツ担当者 |
成果 | プロジェクトローカルの |
入力 | 1つの公開URL、URLファイル、または制御されたサイトマップサンプル |
前提条件 | Claude Code、Python 3.9以上、PageSpeed Insights APIへのアクセス、Gitリポジトリ |
所要時間 | インストールとベースラインは約35分。コード所有者が修正を承認した場合だけ追加時間が必要 |
完了の定義 | raw responseと |
違いは単純です。reports/pagespeed/にはAPIの証拠を保存します。修正計画はその証拠をコード候補につなげます。Git diffは実装作業です。これらの状態を1回のエージェント依頼に混ぜず、見える形で分離してください。
初回監査の前にリポジトリ境界を準備する
Gitが無視する証拠保存先を作成します。これによりClaude Codeは監査出力を調べられますが、API応答をプロダクトのソースコードとして扱ったり、誤ってコミットしたりしません。
mkdir -p reports/pagespeed
printf 'reports/pagespeed/
' >> .gitignore
期待される出力は、git status --shortに.gitignoreの変更だけが表示されることです。品質確認として、一時的なパスを作成したうえでgit check-ignore -v reports/pagespeed/example/report.mdを実行し、新しい無視ルールを指していることを確認します。リポジトリに生成物用の承認済みルールがあれば、その場所を使い、Skillポリシーも一致させてください。既定ではレポートをsrc/、デプロイディレクトリ、追跡対象のdocs/フォルダに置かないでください。
次のルールを.claude/rules/page-speed-audits.mdとして保存します。リポジトリが.claude/rulesを使用していない場合は、既存のCLAUDE.mdのポリシー節に同じ内容を置きます。
# PageSpeed audit policy
- PageSpeed work starts as read-only evidence collection. Write audit output only under reports/pagespeed/, which must stay ignored by Git.
- Read PAGESPEED_API_KEY only from an approved local environment or secret mechanism. Never print it, add it to a command transcript, write it to a report, or commit it.
- Do not edit application source, content, build files, CI, infrastructure, deployment configuration, or a CMS while collecting or interpreting an audit.
- After an audit, create an implementation brief that names evidence, candidate files, risk, tests, acceptance criteria, and rollback condition. Wait for explicit approval before making a diff.
- Never deploy. After an approved implementation, show the Git diff and run only agreed local validation commands.
このポリシーは、プロジェクトでClaude Codeがどう作業するかを伝えるものです。組織全体の権限ポリシーを上書きするものではありません。機密性の高いリポジトリでは、チーム承認済みの権限設定とフックによって同じ境界を強制してください。

監査はリポジトリ内に証拠を作りますが、その証拠をプロダクトのソースコードやコミット対象にはしません。
読み取り専用の証拠Skillを作る
.claude/skills/pagespeed-evidence/SKILL.mdを作成します。
---
name: pagespeed-evidence
description: Collect PageSpeed Insights evidence for one public URL, a supplied URL list, or a controlled XML sitemap sample. Save raw JSON and a Markdown report under the project's ignored reports/pagespeed directory. Use for page speed, Core Web Vitals, Lighthouse, and performance SEO investigation. Audit work is read-only: do not edit source, content, configuration, or deployments until the user explicitly approves an implementation brief.
---
# PageSpeed Evidence for This Repository
## Guardrails
- Before running, read .claude/rules/page-speed-audits.md or the equivalent project policy. If it conflicts with this skill, follow the stricter rule.
- Read PAGESPEED_API_KEY only from the local environment. Never expose it.
- Make GET requests only to the public PageSpeed Insights endpoint and public sitemap URLs. Write only beneath reports/pagespeed/.
- Test mobile and desktop. Preserve each raw response. Lighthouse is point-in-time lab data; loadingExperience and originLoadingExperience are CrUX field data only when returned, and have different scopes.
- A sitemap sample is not a crawl. State the sample cap and selected URLs. For release-critical pages, use a curated URL file.
## Collect a baseline
python3 .claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.py \
--url "https://www.example.com/pricing/" \
--out reports/pagespeed/pricing-baseline
For a sitemap sample:
python3 .claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.py \
--sitemap "https://www.example.com/sitemap.xml" \
--max-urls 12 \
--out reports/pagespeed/site-sample
## Interpret before planning
First report scope, final URLs, request failures, and whether field data exists. Then identify repeated opportunities by page template or mechanism. A score alone is not a root cause and does not predict rankings.
## Handoff to an approved implementation task
Do not edit files after reporting. Create a brief with evidence path, affected URLs, laboratory or field-data scope, candidate files and why they are candidates, proposed smallest change, owner, local test, acceptance criteria, and rollback condition. Ask for approval. Once approved, inspect only the named code path, make the smallest diff, show git diff, run agreed tests, and do not deploy.
続いて、Python標準ライブラリだけで動く次のrunnerを.claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.pyとして保存します。
#!/usr/bin/env python3
"""Write PageSpeed audit evidence to an ignored repository directory."""
from __future__ import annotations
import argparse
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from collections import OrderedDict
from datetime import datetime, timezone
from pathlib import Path
API_URL = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
STRATEGIES = ("mobile", "desktop")
CATEGORIES = ("performance", "accessibility", "best-practices", "seo")
AUDITS = ("largest-contentful-paint", "interaction-to-next-paint", "cumulative-layout-shift", "total-blocking-time")
def fetch(url: str, timeout: int = 45) -> bytes:
request = urllib.request.Request(url, headers={"User-Agent": "ClaudeCode-PageSpeed-Evidence/1.0"})
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
def sitemap(url: str) -> list[str]:
try:
root = ET.fromstring(fetch(url))
except (urllib.error.URLError, ET.ParseError) as error:
raise RuntimeError(f"Cannot parse sitemap {url}: {error}") from error
locations = [node.text.strip() for node in root.findall(".//{*}loc") if node.text and node.text.strip()]
if root.tag.lower().endswith("sitemapindex"):
locations = [nested for location in locations for nested in sitemap(location)]
return list(OrderedDict((url, None) for url in locations if urllib.parse.urlparse(url).scheme in {"http", "https"}))
def sample(urls: list[str], maximum: int) -> list[str]:
groups: OrderedDict[str, str] = OrderedDict()
for url in urls:
segment = next((part for part in urllib.parse.urlparse(url).path.split("/") if part), "root")
groups.setdefault(segment, url)
chosen = list(groups.values())
chosen.extend(url for url in urls if url not in chosen)
return chosen[:maximum]
def request_result(url: str, strategy: str, key: str) -> dict:
parameters = [("url", url), ("strategy", strategy), ("key", key)]
parameters.extend(("category", category) for category in CATEGORIES)
endpoint = API_URL + "?" + urllib.parse.urlencode(parameters)
failure = "unknown error"
for attempt in range(3):
try:
return json.loads(fetch(endpoint, timeout=150))
except urllib.error.HTTPError as error:
failure = f"HTTP {error.code}: {error.read().decode('utf-8', 'replace')[:200]}"
if error.code not in {429, 500, 502, 503, 504}:
break
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
failure = str(error)
time.sleep(2 ** attempt)
raise RuntimeError(failure)
def metric(result: dict, audit_id: str) -> str:
return result.get("lighthouseResult", {}).get("audits", {}).get(audit_id, {}).get("displayValue", "n/a")
def field_scope(result: dict, name: str) -> str:
metrics = result.get(name, {}).get("metrics", {})
fields = ("LARGEST_CONTENTFUL_PAINT_MS", "INTERACTION_TO_NEXT_PAINT", "CUMULATIVE_LAYOUT_SHIFT_SCORE")
return " / ".join(str(metrics.get(field, {}).get("percentile", "n/a")) for field in fields) if metrics else "not returned"
def make_report(records: list[dict], description: str, selected: int, total: int) -> str:
header = [
"# PageSpeed evidence", "",
f"- Generated (UTC): {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
f"- Scope: {description}", f"- URLs selected: {selected} of {total}",
"- Lab data: Lighthouse. Field data: CrUX only when returned by the response.", "",
"| Requested URL | Final URL | Device | Performance | LCP | INP | CLS | TBT | Page CrUX | Origin CrUX | Status |",
"| --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |",
]
for record in records:
if record["error"]:
row = [record["url"], "n/a", record["strategy"], "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", record["error"]]
else:
result = record["result"]
lighthouse = result.get("lighthouseResult", {})
perf = lighthouse.get("categories", {}).get("performance", {}).get("score")
row = [record["url"], lighthouse.get("finalUrl", record["url"]), record["strategy"], "n/a" if perf is None else str(round(perf * 100)), *(metric(result, audit) for audit in AUDITS), field_scope(result, "loadingExperience"), field_scope(result, "originLoadingExperience"), "ok"]
header.append("| " + " | ".join(str(item).replace("|", "/") for item in row) + " |")
return "
".join(header) + "
"
def main() -> int:
parser = argparse.ArgumentParser()
choice = parser.add_mutually_exclusive_group(required=True)
choice.add_argument("--url")
choice.add_argument("--urls-file")
choice.add_argument("--sitemap")
parser.add_argument("--max-urls", type=int, default=10)
parser.add_argument("--out", required=True)
args = parser.parse_args()
key = os.environ.get("PAGESPEED_API_KEY")
if not key:
parser.error("PAGESPEED_API_KEY is required in the environment")
output = Path(args.out)
if Path("reports/pagespeed") not in (output, *output.parents):
parser.error("--out must be beneath reports/pagespeed/")
if args.url:
urls, description, total = [args.url], "single URL", 1
elif args.urls_file:
urls = [line.strip() for line in Path(args.urls_file).read_text(encoding="utf-8").splitlines() if line.strip() and not line.startswith("#")]
description, total = "supplied URL list", len(urls)
else:
discovered = sitemap(args.sitemap)
urls, description, total = sample(discovered, args.max_urls), f"sitemap sample from {args.sitemap}", len(discovered)
raw = output / "raw"
raw.mkdir(parents=True, exist_ok=True)
records = []
for number, url in enumerate(urls, 1):
for strategy in STRATEGIES:
try:
result = request_result(url, strategy, key)
(raw / f"{number:03d}-{strategy}.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
records.append({"url": url, "strategy": strategy, "result": result, "error": ""})
except RuntimeError as error:
records.append({"url": url, "strategy": strategy, "result": {}, "error": str(error)})
(output / "report.md").write_text(make_report(records, description, len(urls), total), encoding="utf-8")
(output / "summary.json").write_text(json.dumps({"scope": description, "urls": urls, "records": [{key: value for key, value in record.items() if key != "result"} for record in records]}, indent=2), encoding="utf-8")
print(output / "report.md")
return 0
if __name__ == "__main__":
raise SystemExit(main())
証拠パケットを作成したら、そこで止める
PAGESPEED_API_KEYはリポジトリではなく、シェルまたは組織が承認したシークレット管理機構で設定します。次に小さなベースラインを実行します。
python3 .claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.py \
--url "https://www.example.com/" \
--out reports/pagespeed/homepage-baseline
期待される出力はreport.md、summary.json、モバイル用とデスクトップ用のraw JSONファイルです。品質確認として、git status --shortにレポート成果物が表示されないことを確認します。Claude Codeにソースファイルを探させる前に、まずレポートを読んでください。最終URLが要求URLと異なる場合は、要求したルートがユーザーの見るルートだと決めつけず、そのリダイレクトを実装ブリーフに記録します。
復旧方法:403は通常、Google APIの設定またはAPIキーの制限を確認すべきことを示します。field dataの節が空でもrunnerのエラーではなく、そのスコープに対してAPIが利用可能なCrUXデータを返さなかったという意味です。429または5xxは回数を制限した再試行後に記録されます。後で同じスコープを再実行し、部分的な結果を新しい監査の結果に混ぜず、同条件で比較してください。
レポートをレビュー可能な実装ブリーフにする
監査後にClaude Codeへ渡す最初の依頼も、読み取り専用のままにします。証拠のパスを渡し、コード編集ではなくコード候補を出すよう明示します。
Read reports/pagespeed/homepage-baseline/ as evidence and inspect this repository
read-only. Produce an implementation brief only.
For each prioritized opportunity, cite the relevant report row or raw response,
name candidate templates, components, asset tooling, or configuration files,
and explain why they are candidates. Propose the smallest safe change. Include
expected benefit, risk, local validation, production acceptance criteria, and a
rollback condition. Mark uncertainty clearly.
Do not edit any file, run a formatter, change dependencies, write a test, or
deploy. Wait for my approval of the brief.
期待する判断は、どの仮説を試す価値があるかを示す簡潔な計画です。品質確認として、render-blocking resourcesから広範なフレームワーク書き換えへ直接飛ぶ計画は、まだ実行可能ではありません。復旧方法:Claude Codeに、1つのページテンプレート、1つの仕組み、1つの可逆な変更まで計画を狭めるよう頼むか、先に開発者にraw JSONを調べてもらってください。
承認された後にだけdiffを作る
コード所有者が特定項目を承認してから、Claude Codeへ制約付きの依頼を渡します。
Approved: implement only item P1 from the PageSpeed brief.
Change only these files: [APPROVED PATHS]. Preserve existing behavior. Before
editing, restate the acceptance criteria and rollback condition. After editing,
show git diff, run [APPROVED LOCAL TEST COMMAND], and report any failure.
Do not commit, push, create a pull request, change infrastructure, or deploy.
品質確認として、diffは計画より小さく、より大きくなってはいけません。また、ローカルテストが性能仮説とどう関係するかを説明できる必要があります。単体テストが成功してもCore Web Vitalsの改善を証明するわけではありません。プレビューまたは承認済みリリースの後、同じPageSpeedスコープで再測定し、raw evidenceとfield dataの可用性を慎重に比較してください。
証拠から変更までの連鎖
PageSpeed API response
|
v
ignored reports/pagespeed evidence
|
v
read-only repository mapping and repair brief
|
explicit human approval
|
v
small Git diff -> agreed local tests -> preview/retest -> rollback if needed
Claude Codeにスケジューラやチャットゲートウェイとは別のワークフローが必要なのは、コードベースのすぐ近くで動くためです。証拠、計画、diffを個別の成果物として保つことで、動きの速いエージェントでもレビューしやすくなります。

証拠、計画、diff、再測定は、それぞれ独立してレビューできる状態として扱ってください。
検証チェックリスト
- [ ] Skillがプロジェクトローカルの
.claude/skills/pagespeed-evidence/SKILL.mdにある。 - [ ] リポジトリに見える形のPageSpeed監査ポリシーと、Gitが無視する
reports/pagespeed/の保存先がある。 - [ ] APIキーはローカルの承認済みシークレット環境にだけある。
- [ ] 監査した各URLに、モバイルとデスクトップの結果、raw JSON、最終URL、必要に応じた失敗記録がある。
- [ ] Lighthouseの測定値と、ページ/オリジンのCrUXデータが別々に記録されている。
- [ ] Claude Codeがソースファイルを編集する前に実装ブリーフを作成した。
- [ ] 承認済みの変更に、小さなGit diff、ローカルテスト結果、受入条件、ロールバック条件がある。
- [ ] 監査または実装の依頼でWebサイトをデプロイしない。
よくある質問
CLAUDE.mdだけでClaude Codeによるファイル変更を防げますか?
いいえ。これは価値ある継続指示のコンテキストですが、強制機構ではありません。ある操作を技術的に禁止する必要がある場合は、リポジトリの権限とフックを使ってください。それでもポリシーを残す価値はあります。人とエージェントの両方に、意図した作業モデルを明確に伝えられるからです。
Skillでサイト全体を監査できますか?
より大きなURLリストを使うことはできますが、サイトマップの各URLにPageSpeedテストを行うのが最初の適切な手順であることはほとんどありません。代表的なテンプレート、重要なコンバージョン経路、最近リリースした画面を選んでください。サンプル規則を明示し、結果から必要だと判断できた場合だけ範囲を広げます。
なぜClaude CodeにLighthouseの機会をすべて自動修正させないのですか?
多くのLighthouseの機会は症状を示すもので、普遍的に安全な変更を指すわけではありません。scriptを遅延させると、チェックアウト、同意管理、分析、パーソナライズ、アクセシビリティを壊す可能性があります。監査が作るべきものは仮説です。安全に試せる内容はコード所有者が決めます。
Lighthouseスコアが良くなれば、実ユーザーの体験が改善した証拠になりますか?
いいえ。管理されたテスト条件における証拠を強めるだけです。利用できる場合はCrUXのfield dataを確認し、時間を通じて同じURLと同じリリース条件を比較してください。
公式リファレンス
- Claude Code skills
- Claude Code memory and project instructions
- Claude Code permissions
- Google PageSpeed Insights API
- Chrome UX Report documentation
著者:Julian Mercer、AuspiaのTechnical SEO Practitioner。Julianは、SEOの発見からレビュー済みのWebサイト変更まで、明確な証拠の流れを残す技術ワークフローを専門にしています。




