Cómo crear una skill de auditoría SEO PageSpeed programada con Hermes Agent

Puntos clave

Crea una skill de Hermes Agent que ejecuta líneas base de PageSpeed Insights con alcance limitado, guarda la evidencia original y solo se programa tras una auditoría manual verificada.

Como crear una skill de auditoria SEO PageSpeed programada con Hermes Agent

Hermes Agent debe repetir una medicion acotada, no improvisar cambios en produccion. Esta guia crea una skill de solo lectura que consulta Google PageSpeed Insights, conserva JSON sin procesar y genera un informe fechado. Primero validas una linea base manual; solo despues una persona puede aprobar una ejecucion recurrente.

Envia este articulo a Hermes para instalar la skill

Read [THIS ARTICLE URL] and install the Hermes skill described there.

First inspect my local Hermes configuration and any project guidance. Tell me
the destination you will use. Create only:
~/.hermes/skills/pagespeed-seo-baseline/SKILL.md
~/.hermes/skills/pagespeed-seo-baseline/scripts/pagespeed_baseline.py

Copy the complete code blocks from the article. Do not invent a different
workflow. Verify the Python file with a syntax check and report the two paths.
Tell me how to configure PAGESPEED_API_KEY locally, but never ask me to paste
the value into this chat and never display it.

Do not run PageSpeed, schedule a task, write to my site repository, use CMS or
hosting credentials, edit a skill other than this one, or change a live site.
Stop after installation and verification.

La skill vive en~/.hermes/skills/, fuera del proyecto web: es una herramienta de monitorizacion, no una funcion de la aplicacion.

Resultado: linea base antes que calendario

Elemento

Resultado

Publico

SEO, desarrollo o marketing tecnico que usa Hermes localmente o aislado

Entrega

Skill, JSON bruto, informe Markdown y programacion revisada opcional

Entradas

URL publica, lista seleccionada o muestra de sitemap limitada

Listo cuando

Se registran URL, estrategia, evidencia original y alcance de datos sin cambiar el sitio

La muestra del sitemap revela patrones de plantillas; no demuestra que se hayan auditado todas las URL, paginas huerfanas, canonicals o indexabilidad.

Configura la clave solo en el entorno seguro

export PAGESPEED_API_KEY="replace-with-your-key"

Activa PageSpeed Insights API en Google Cloud y usa una clave restringida. Guardala solo en un mecanismo local aprobado o en el entorno que inicia Hermes, nunca en la skill, informe, chat o Git.

Contrato de seguridad de SKILL.md

~/.hermes/skills/pagespeed-seo-baseline/
SKILL.md
scripts/
pagespeed_baseline.py

---
name: pagespeed-seo-baseline
description: Create a read-only PageSpeed Insights baseline for one public URL, a supplied URL list, or a controlled XML sitemap sample. Save raw JSON and a dated report that distinguishes Lighthouse lab data from CrUX field data. Use for website speed, Core Web Vitals, Lighthouse, and recurring performance-baseline requests. Never edit a site, repository, CMS, hosting configuration, or deployment.
required_environment_variables:
- PAGESPEED_API_KEY
---

# PageSpeed SEO Baseline

This is an evidence-collection skill. It may call the public PageSpeed Insights API and public sitemap URLs, then write only inside the report directory chosen by the operator.

## Boundaries

- Read `PAGESPEED_API_KEY` from the runtime environment only. Never print, message, save, or commit it.
- Do not use browser logins, SSH, CMS, hosting, Git write, deployment, or website-editing tools. A performance report is not approval to repair a site.
- Run both `mobile` and `desktop`. Preserve each successful response under `raw/` before summarizing it.
- Describe Lighthouse as a point-in-time lab measurement. Treat `loadingExperience` as page-level CrUX only when returned, and `originLoadingExperience` as origin-level CrUX only when returned. Do not substitute one for the other.
- For `--sitemap`, say how URLs were sampled and how many were excluded. For important templates, prefer `--urls-file`.
- Never create or change a schedule until a human has reviewed one successful manual report and named the recurring scope, cadence, report path, and delivery destination.

## Commands

Run from this skill directory. The output path must be outside a repository unless the operator explicitly chooses an ignored evidence directory.

python3 scripts/pagespeed_baseline.py \
--url "https://www.example.com/pricing/" \
--out "$HOME/hermes-pagespeed-reports/pricing-baseline"

python3 scripts/pagespeed_baseline.py \
--sitemap "https://www.example.com/sitemap.xml" \
--max-urls 12 \
--out "$HOME/hermes-pagespeed-reports/site-sample"

## Required report handoff

Return the report path and a compact table: requested URL, final URL, device strategy, performance score, LCP, INP, CLS, TBT, and field-data scope. Name failed requests and skipped URLs. Group repeated opportunities by likely mechanism, but label every repair as a hypothesis until a developer verifies it.

When the operator asks for a schedule, show the proposed command, cadence, report retention rule, and delivery target. Wait for explicit confirmation before creating it. If running on a messaging surface, send the report path or attachment; never send the API key or raw command environment.

#!/usr/bin/env python3
"""Collect bounded PageSpeed Insights evidence without third-party packages."""
from __future__ import annotations

import argparse
import json
import os
import sys
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 = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
CATEGORIES = ("performance", "accessibility", "best-practices", "seo")
STRATEGIES = ("mobile", "desktop")
AUDITS = ("largest-contentful-paint", "interaction-to-next-paint", "cumulative-layout-shift", "total-blocking-time")


def get_bytes(url: str) -> bytes:
request = urllib.request.Request(url, headers={"User-Agent": "Hermes-PageSpeed-Baseline/1.0"})
with urllib.request.urlopen(request, timeout=45) as response:
return response.read()


def sitemap_urls(url: str) -> list[str]:
try:
root = ET.fromstring(get_bytes(url))
except (urllib.error.URLError, ET.ParseError) as error:
raise RuntimeError(f"Could not read sitemap {url}: {error}") from error
locs = [n.text.strip() for n in root.findall(".//{*}loc") if n.text and n.text.strip()]
if root.tag.lower().endswith("sitemapindex"):
locs = [item for child in locs for item in sitemap_urls(child)]
return list(OrderedDict((u, None) for u in locs if urllib.parse.urlparse(u).scheme in {"http", "https"}))


def select_urls(urls: list[str], maximum: int) -> list[str]:
groups: OrderedDict[str, list[str]] = OrderedDict()
for url in urls:
parts = [part for part in urllib.parse.urlparse(url).path.split("/") if part]
groups.setdefault(parts[0] if parts else "root", []).append(url)
chosen = [values[0] for values in groups.values()]
chosen.extend(url for url in urls if url not in chosen)
return chosen[:maximum]


def run_api(url: str, strategy: str, key: str) -> dict:
params = [("url", url), ("strategy", strategy), ("key", key)]
params.extend(("category", category) for category in CATEGORIES)
endpoint = API + "?" + urllib.parse.urlencode(params)
last_error = "unknown error"
for attempt in range(3):
try:
request = urllib.request.Request(endpoint, headers={"User-Agent": "Hermes-PageSpeed-Baseline/1.0"})
with urllib.request.urlopen(request, timeout=150) as response:
return json.load(response)
except urllib.error.HTTPError as error:
last_error = f"HTTP {error.code}: {error.read().decode('utf-8', 'replace')[:240]}"
if error.code not in {429, 500, 502, 503, 504}:
break
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:
last_error = str(error)
time.sleep(2 ** attempt)
raise RuntimeError(last_error)


def value(result: dict, audit_id: str) -> str:
return result.get("lighthouseResult", {}).get("audits", {}).get(audit_id, {}).get("displayValue", "n/a")


def score(result: dict) -> str:
raw = result.get("lighthouseResult", {}).get("categories", {}).get("performance", {}).get("score")
return "n/a" if raw is None else str(round(raw * 100))


def field(result: dict, scope: str) -> str:
metrics = result.get(scope, {}).get("metrics", {})
names = ("LARGEST_CONTENTFUL_PAINT_MS", "INTERACTION_TO_NEXT_PAINT", "CUMULATIVE_LAYOUT_SHIFT_SCORE")
return " / ".join(str(metrics.get(name, {}).get("percentile", "n/a")) for name in names) if metrics else "not returned"


def report(records: list[dict], scope: str, selected: int, total: int) -> str:
lines = ["# Hermes PageSpeed baseline", "", f"- Generated (UTC): {datetime.now(timezone.utc).isoformat(timespec='seconds')}", f"- Scope: {scope}", f"- URLs selected: {selected} of {total}", "- Lighthouse is lab data. CrUX appears only when Google returned it.", "", "| URL | Final URL | Device | Perf | LCP | INP | CLS | TBT | Page CrUX (LCP / INP / CLS) | Origin CrUX (LCP / INP / CLS) | Status |", "| --- | --- | --- | ---: | --- | --- | --- | --- | --- | --- | --- |"]
for item in records:
if item["error"]:
row = [item["url"], "n/a", item["strategy"], "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", item["error"]]
else:
data = item["result"]
final_url = data.get("lighthouseResult", {}).get("finalUrl", item["url"])
row = [item["url"], final_url, item["strategy"], score(data), *(value(data, name) for name in AUDITS), field(data, "loadingExperience"), field(data, "originLoadingExperience"), "ok"]
lines.append("| " + " | ".join(str(cell).replace("|", "/") for cell in row) + " |")
return "
".join(lines) + "
"


def main() -> int:
parser = argparse.ArgumentParser()
scope = parser.add_mutually_exclusive_group(required=True)
scope.add_argument("--url")
scope.add_argument("--urls-file")
scope.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 not set in the environment")
if args.url:
urls, label = [args.url], "single URL"
total = len(urls)
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("#")]
label = "supplied URL list"
total = len(urls)
else:
all_urls = sitemap_urls(args.sitemap)
urls, label = select_urls(all_urls, args.max_urls), f"sitemap sample from {args.sitemap}"
total = len(all_urls)
out = Path(args.out)
raw = out / "raw"
raw.mkdir(parents=True, exist_ok=True)
records = []
for number, url in enumerate(urls, start=1):
for strategy in STRATEGIES:
try:
result = run_api(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)})
(out / "report.md").write_text(report(records, label, len(urls), total), encoding="utf-8")
(out / "summary.json").write_text(json.dumps({"scope": label, "urls": urls, "records": [{k: v for k, v in item.items() if k != "result"} for item in records]}, indent=2), encoding="utf-8")
print(out / "report.md")
return 0


if __name__ == "__main__":
raise SystemExit(main())

El runner acepta una URL, un archivo de URL o un sitemap XML; mide movil y escritorio; guardareport.md, summary.json y respuestas raw; y registra fallos en vez de ocultarlos.

Ejecuta una linea base manual en una ubicacion intencionalmente sencilla

mkdir -p "$HOME/hermes-pagespeed-reports"
cd ~/.hermes/skills/pagespeed-seo-baseline
python3 scripts/pagespeed_baseline.py \
--url "https://www.example.com/" \
--out "$HOME/hermes-pagespeed-reports/homepage-2026-07-24"

Empieza con una URL canonica y un directorio de informe nuevo. Asi verificas que la skill lee la variable declarada, alcanza la API publica y escribe solo donde debe. Comprueba URL solicitada y final, ambas estrategias y archivos JSON no vacios. Configura una clave ausente localmente; si falla el sitemap, usa un --urls-file pequeno para aislarlo sin sustituir silenciosamente el alcance.

Lee lo que PageSpeed realmente midio

Senal

Significado

No afirmes

Lighthouse, LCP, INP, CLS, TBT

Medicion de laboratorio puntual

Toda persona vive esto

loadingExperience

CrUX de pagina si llega

Comportamiento de todo el origen

originLoadingExperience

CrUX de origen si llega

Cada plantilla es igual

Sin CrUX

No hubo dato elegible

Nadie visita la pagina

Comparacion entre Lighthouse de laboratorio, CrUX de pagina y CrUX de origen.

Una oportunidad repetida es evidencia para investigar, no permiso para cambiar imagenes, scripts o cache automaticamente.

Programa solo una medicion creible

Create a proposed weekly PageSpeed baseline schedule, but do not activate it yet.

Use the exact manual scope and command from my approved report. Run in the same
isolated environment. Store each run beneath
$HOME/hermes-pagespeed-reports/weekly/YYYY-MM-DD/ and retain reports for 90
days. Deliver only report.md and summary.json to the approved owner channel.

Show the schedule, command, environment assumptions, report path, and failure
notification behavior. Do not include PAGESPEED_API_KEY in any output. Wait for
my explicit approval before writing or enabling the schedule.

Revisa el primer informe con la persona propietaria. Define alcance, frecuencia, retencion y destino. Hermes debe mostrar comando, entorno y notificaciones; espera aprobacion antes de activar el calendario.

Flujo de Hermes: linea base manual, aprobacion humana y reporte semanal.

Usa el informe para crear un brief de reparacion separado

Read this PageSpeed evidence folder: [REPORT PATH]. Create a repair brief only.
For each repeated or high-impact opportunity, state the affected URLs, the lab
or field evidence, a likely mechanism, the developer owner, expected benefit,
risk, test method, and rollback signal. Flag assumptions. Do not edit code,
content, configuration, or a deployment.

El informe solo genera un brief: URL afectadas, evidencia de laboratorio o campo, mecanismo probable, responsable, beneficio, riesgo, prueba, rollback e incertidumbre. La skill no edita codigo, contenido, configuracion ni despliegue.

Lista de verificacion

  • [ ] La clave API no aparece en archivos, chat ni informes.
  • [ ] Cada URL incluye movil, escritorio, JSON bruto, URL final y alcance claro.
  • [ ] Lighthouse y CrUX de pagina u origen no se confunden.
  • [ ] El calendario se revisa antes de activarse y la skill no tiene permisos de CMS, repositorio ni despliegue.

FAQ

Puede Hermes medir todo el sitemap?

Puede procesar mas URL, pero no es un buen primer paso. Empieza con una pagina por plantilla o una muestra estratificada limitada y amplia solo cuando la evidencia lo justifique.

Author: Julian Mercer, Technical SEO Practitioner at Auspia. Julian escribe flujos tecnicos que dejan una cadena clara de evidencia antes de cambiar produccion.

Referencias oficiales

Un mejor puntaje PageSpeed garantiza mejores rankings?

No. Es una medicion de laboratorio para una URL y estrategia, no una promesa de ranking. Trata CrUX de pagina y origen como alcances distintos cuando la API los devuelva.

Por que conservar respuestas PageSpeed raw?

El JSON raw conserva URL final, fallos y detalle para comparar. Permite conectar el brief y la nueva medicion con la misma evidencia.

Un contenedor es mas seguro que ejecutar Hermes localmente?

No necesariamente. El codigo del contenedor puede leer una clave que se le entrega. Para la primera auditoria suele ser menos riesgoso ejecutar localmente con una clave restringida; limita red, escritura y secretos si usas contenedores.

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.

Explora este tema

Sigue la misma línea de crecimiento