Como ejecutar una auditoria SEO PageSpeed con Claude Code sin cambios web sin revisar
Claude Code puede relacionar una auditoria PageSpeed con plantillas, componentes y pipelines de activos del repositorio. La evidencia, el plan, el diff y la nueva medicion deben ser estados separados: la auditoria es de solo lectura y un cambio llega solo tras aprobacion humana.
Instala el flujo local del proyecto
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.
Contrato de finalizacion
Elemento | Condicion de finalizacion |
|---|---|
Lector | Desarrollador, responsable SEO o propietario tecnico que trabaja en un repositorio web |
Resultado | Skill local |
Entradas | Una URL publica, un archivo de URL o una muestra controlada de sitemap |
Requisitos | Claude Code, Python 3.9+, acceso a PageSpeed Insights y un repositorio Git |
Tiempo | Unos 35 minutos para instalar y medir una linea base; mas tiempo solo despues de aprobar una correccion |
Terminado | Existen respuestas raw y |
La separacion importa: reports/pagespeed/ contiene evidencia de API; un plan conecta esa evidencia con candidatos de codigo; un diff de Git es trabajo de implementacion. No los combines en una sola solicitud al agente.
Prepara el limite del repositorio antes de la primera auditoria
mkdir -p reports/pagespeed
printf 'reports/pagespeed/
' >> .gitignore
# 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.
Crea un lugar de evidencia ignorado para que Claude Code pueda inspeccionar resultados sin tratar respuestas de API como codigo de producto ni confirmarlas por accidente. Tras crear una ruta temporal, git check-ignore -v reports/pagespeed/example/report.md debe apuntar a la regla nueva. Si el repositorio ya tiene una ubicacion aprobada para artefactos generados, usa esa ubicacion y ajusta la politica; no coloques informes por defecto en src/, directorios de despliegue o docs/ rastreado.
Guarda la politica de PageSpeed en .claude/rules/page-speed-audits.md, o en la seccion equivalente de CLAUDE.md si el proyecto no usa reglas. Es guia persistente, no sustituye permisos o hooks corporativos para acciones que deban bloquearse tecnicamente.
Instala una skill de evidencia, no un bot de reparacion
---
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.
#!/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())
La skill debe leer primero esa politica, leer PAGESPEED_API_KEY solo del entorno local, hacer GET solo a PageSpeed Insights y sitemaps publicos, y escribir unicamente bajo reports/pagespeed/. Debe medir movil y escritorio, conservar cada respuesta raw, etiquetar Lighthouse como datos de laboratorio y distinguir CrUX de pagina y de origen solo cuando la API los devuelva. Una muestra de sitemap no es un rastreo: declara el limite y las URL elegidas; para paginas criticas usa un archivo de URL seleccionado.
Produce un paquete de evidencia y luego detente
python3 .claude/skills/pagespeed-evidence/scripts/pagespeed_evidence.py \
--url "https://www.example.com/" \
--out reports/pagespeed/homepage-baseline
Configura PAGESPEED_API_KEY en el shell o gestor de secretos aprobado, nunca en el repositorio. La linea base produce report.md, summary.json y JSON raw movil/escritorio. Comprueba que git status --short no muestre artefactos de informe y lee el informe antes de pedir candidatos de codigo. Si la URL final difiere de la solicitada, registra esa redireccion en el brief.
Un 403 suele indicar configuracion de Google API o restricciones de clave. CrUX vacio no es un fallo del runner: la API no devolvio datos de campo elegibles. Un 429 o 5xx queda registrado tras reintentos acotados; vuelve a ejecutar mas tarde con el mismo alcance y no mezcles resultados parciales con otra auditoria.
Convierte el informe en un brief de implementacion revisable
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.
La primera solicitud tras la auditoria sigue siendo de solo lectura. Pide que cite filas del informe o respuestas raw, nombre plantillas, componentes, herramientas de activos o configuraciones candidatas y explique por que. Debe proponer el cambio seguro mas pequeno, con beneficio esperado, riesgo, validacion local, criterio de aceptacion de produccion, rollback e incertidumbre.
Un plan que salta de recursos que bloquean renderizado a reescribir un framework no esta listo. Limitalo a una plantilla, un mecanismo y un cambio reversible, o pide a un desarrollador que revise primero el JSON raw.
Crea el diff solo despues de la aprobacion
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.
Cuando el propietario de codigo apruebe un item concreto, Claude Code modifica solo las rutas aprobadas y conserva el comportamiento actual. Antes de editar repite aceptacion y rollback; despues muestra git diff, ejecuta solo la prueba local acordada e informa fallos. No hace commit, push, PR, cambios de infraestructura ni despliegue.
El diff debe ser mas pequeno que el plan y explicar la relacion entre prueba local e hipotesis de rendimiento. Una prueba verde no demuestra una mejora de Core Web Vitals: vuelve a medir el mismo alcance despues de una vista previa o lanzamiento aprobado.
Cadena de evidencia a cambio
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
Conserva como estados separados la respuesta de PageSpeed, la evidencia ignorada, el mapeo de repositorio de solo lectura y brief, la aprobacion humana explicita, un diff pequeno, pruebas acordadas, vista previa/nueva medicion y rollback si hace falta. Claude Code trabaja cerca del codigo; esa separacion lo hace revisable.
Escribe evidencia solo bajoreports/pagespeed/, que debe estar ignorado por Git. La politica debe exigir que la clave se lea unicamente desde un entorno local aprobado, que el runner haga GET solo a PageSpeed y sitemaps publicos, guarde raw JSON, y no edite fuente, contenido, configuracion, CI, infraestructura ni despliegue mientras interpreta la auditoria.

La salida incluyereport.md, summary.json y JSON raw para movil y escritorio. Un 403 apunta a configuracion de API o restriccion de clave; CrUX vacio significa que la API no devolvio datos elegibles.
La evidencia precede a la implementacion.
Pide a Claude Code que cite filas del informe o JSON raw, nombre archivos candidatos y explique por que, proponga el cambio mas pequeno, y documente beneficio esperado, riesgo, validacion local, criterio de aceptacion y rollback. No puede editar archivos ni desplegar.
Tras aprobacion, cambia solo rutas nombradas, vuelve a exponer aceptacion y rollback, muestragit diff y ejecuta validacion acordada. Una prueba verde no demuestra una mejora de Core Web Vitals: vuelve a medir el mismo alcance tras una vista previa o lanzamiento aprobado.

Lista de verificacion
- [ ] La skill esta en
.claude/skills/pagespeed-evidence/SKILL.md. - [ ] La carpeta de evidencia esta ignorada y la clave solo existe en el entorno aprobado.
- [ ] Cada URL tiene movil, escritorio, JSON bruto, URL final y fallos.
- [ ] Lighthouse y CrUX se etiquetan por separado.
- [ ] Existe un brief antes de editar y todo diff aprobado tiene prueba y rollback.
- [ ] No se despliega desde el flujo de auditoria.
Preguntas frecuentes
CLAUDE.md basta para impedir cambios de archivo?
No. Es contexto persistente valioso, no un mecanismo de aplicacion. Usa permisos y hooks del repositorio cuando una accion deba bloquearse tecnicamente; conserva la politica para que personas y agente entiendan el modelo de trabajo.
La skill puede auditar todo un sitio?
Puede usar una lista mayor, pero probar cada URL del sitemap rara vez es el primer paso correcto. Elige plantillas representativas, rutas de conversion y superficies publicadas recientemente; declara la regla de muestra y amplia solo si el resultado lo exige.
Por que no permitir que Claude Code corrija automaticamente cada oportunidad de Lighthouse?
Muchas oportunidades describen sintomas, no un cambio universalmente seguro. Retrasar un script puede romper checkout, consentimiento, analitica, personalizacion o accesibilidad. La auditoria genera hipotesis; los propietarios deciden que probar.
Una mejor puntuacion Lighthouse demuestra que mejoraron usuarios reales?
No. Fortalece evidencia para una condicion de prueba controlada. Revisa CrUX cuando exista y compara las mismas URL y condiciones de lanzamiento a lo largo del tiempo.
Referencias oficiales
- Claude Code skills
- Claude Code memory and project instructions
- Claude Code permissions
- Google PageSpeed Insights API
- Chrome UX Report documentation
Author: Julian Mercer, Technical SEO Practitioner at Auspia. Julian se centra en flujos que unen hallazgos SEO y cambios revisados con evidencia clara.
Material completo de instalacion y prompts
A continuacion se conservan todos los bloques copiables del original en ingles. Utilizalos en las rutas y en el orden explicados; no resumas codigo, comandos ni prompts durante la instalacion.




