seo-audit
Daily on-page and technical SEO audit of every page on a site - discovers URLs from the sitemap, scores each page, adds cross-page checks (duplicate titles, canonicals, sitemap gaps), diffs against yesterday, and sends the score line plus any regressions
git clone --depth 1 https://github.com/aeonfun/aeon /tmp/seo-audit && cp -r /tmp/seo-audit/skills/seo-audit ~/.claude/skills/seo-auditSKILL.md
Today is ${today}.
> **${var}** — the target **sites**, comma-separated (an origin, not a page:
> `https://www.example.com`). Each site is audited whole and independently. A
> bare host gets `https://` prepended. A path in `${var}` (`…/docs`) still works
> — its origin is what gets crawled.
>
> **Empty `${var}` → there is nothing to audit.** This skill has no built-in
> default site (it ships general-purpose). Log `SEO_NO_TARGET`, send **no**
> notification, and exit clean — a daily "set a target" ping would just get
> muted. Set the target once in the dashboard (the skill's `var`) and it runs
> from the next tick.
## What this does
Discovers every page a site publishes, audits all of them, compares the result
against the previous run, and reports. The heavy lifting (discovery, fetching,
parsing, checking) happens in `scripts/seo-audit.mjs`, which returns
machine-readable findings so you reason over facts instead of eyeballing HTML.
Discovery is sitemap-first: `robots.txt` → `sitemap.xml` (following sitemap
indexes), honouring the robots `*` `Disallow` rules, falling back to a one-hop
crawl of the homepage's internal links when a site has no sitemap. That is the
same list Google works from, so auditing it is auditing what actually ranks.
Auditing the whole site — not three sample templates — is what makes the
cross-page checks possible: duplicate titles, duplicate meta descriptions,
canonicals pointing away from their own page, and pages linked but missing from
the sitemap. None of those are visible from inside a single page, and all of
them are ways a site quietly loses search traffic.
This is on-page and technical SEO only — no keyword research, rank tracking, or
backlink analysis. If a finding would need those, say so rather than guessing.
## Capability notes (read before editing this skill)
This skill is `mode: read-only`, and that is load-bearing:
- The auditor is **Node, not Python**, because `Bash(node:*)` is in the read-only
capability base while `Bash(python3:*)` is write-tier only
(`scripts/skill_mode.sh`). Porting it back to Python would force `mode: write`.
- It is **dependency-free stdlib Node**, matching every other helper in
`scripts/`. There is no install step. If you find yourself wanting a package,
you are about to break the thing that makes this skill cheap and safe.
- **You have no Write or Edit tool.** Every file this skill produces is written
with a shell redirection (`>`/`>>`) from an allowed command. The workflow's
read-only guard reverts writes to code/config paths but **preserves `memory/`
and `output/`**, which is exactly where this skill writes.
- The guard also appends its own `## seo-audit (read-only)` run-log line, so the
log entry below is the detail under it, not a duplicate.
`PAGESPEED_API_KEY` is optional. When set, the script folds in Core Web Vitals —
it reads the env var itself, so **never pass the key as a command-line argument**;
the workflow's permission analyzer blocks `$SECRET` expansions on the Bash line.
It prefers **real-user field data** (Chrome UX Report p75 — LCP, **INP** since it
replaced FID in 2024, and CLS — the metrics Google's page-experience signal
actually uses), preferring page-level then origin-level, and falls back to the lab
Lighthouse score only when a URL has too little traffic for field data (the
finding says which). In site mode it samples the entry page only (PageSpeed is
slow and quota-limited; per-page Lighthouse runs would dominate the runtime). When
unset, Core Web Vitals are simply absent. That is a degraded run, not a failed one
— don't mention the missing key in the notification.
## Workflow
1. **Resolve the targets.** Split `${var}` on commas and trim. No confirmation
step — this runs unattended, so treat `${var}` as final. **If `${var}` is
empty, there is no site to audit:** log `SEO_NO_TARGET` (Step 8), send no
notification, and exit. Do not invent a default origin.
2. **Audit each site:**
```bash
node scripts/seo-audit.mjs --site <origin> --format json
```
Useful flags: `--max N` (page cap, default 50) and `--concurrency N`
(default 4 — politeness as much as speed). Add `--check-links` on the
**first** site only; it probes up to 40 links and is the slow part. In site
mode links are deduped across every page first, so a footer link is checked
once for the whole site rather than once per page.
A site where no page could be fetched returns `"ok": false` and exits 1.
Record it and keep going — one dead site must not abort the others. Individual
pages that fail are reported inside the run as the `pages_fetched` finding.
3. **Read the findings, don't re-derive them.** The report has two levels:
- `pages[]` — one per URL, each with its own `score`, `summary`, and
`findings` (same `check` / `status` / `message` / `detail` shape as before).
- `site_findings[]` — the cross-page checks: `pages_fetched`,
`duplicate_titles`, `duplicate_descriptions`, `canonical_targets`,
`sitemap_coverage`.
The headline `score` is the **mean page score**, and `discovery` records how
the URLs were found (`sitemap` or `crawl`), how many existed, how many were
audited, and whether the `--max` cap truncated the run.
4. **Diff against the previous run.** Every run writes its own timestamped
snapshot (step 5), so the baseline is the **newest snapshot that already
exists** — i.e. the previous run's, since this run hasn't written its own yet.
Filenames sort chronologically, so:
```bash
PREV=$(ls -1 memory/seo-audit/*.json 2>/dev/null | sort | tail -1)
```
`$PREV` is the baseline (empty on the very first run). Match pages **by URL**,
per site, and compare:
- **New fails** — a check that was `pass`/`warn` yesterday and is `fail` today.
- **Site score movement** — any change of 3+ points, either direction.
- **New / removed pages** — URLs in today's sitemap that weren't in
yesterday's, and vice versa. A pagSet up and run an Aeon agent instance — get started from scratch, pick which skills to turn on or install more from packs, reschedule or change what runs, edit what an existing skill does, fix a skill that isn't firing, set the STRATEGY.md north star and soul/ voice, turn a coding-agent chat into a scheduled Aeon skill, and mine past coding-agent conversations for recurring work worth automating as a skill. Use when the user mentions Aeon, aeon.yml, an Aeon skill / instance / routine / pack, asks to schedule, enable, edit, or debug an agent that runs on a cron, or asks what of their repeated/manual work Aeon could take over.
Mention/keyword sweep on social platforms for [REPLACE: KEYWORDS] — trends, sentiment, top posts
5 concrete real-life actions, leverage-scored against open loops with specificity and anti-fluff gates
Static config-correctness linter for this instance - catches the silent-failure class (unquoted schedules, duplicate keys, unconfigured skills, mode typos, broken requires/MCP refs) that no run-based health skill can see. Notifies only on problems.
Pull framework updates from the upstream Aeon repo into this instance - 3-way merges canon's new commits into a PR, never clobbering operator config.
Write a publication-ready article in one of three angles - a trending long-form piece, a watched-repo thesis, or a project-through-a-lens essay. Optional Replicate hero image with --visual.
Automatically merge open PRs that have passing CI, no blocking reviews, and no conflicts
Two-mode aeon.yml workflow builder - analyze inspects URLs and emits a tiered, signal-verified skill-enablement plan plus an aeon.yml diff; enable flips slugs to enabled:true and opens a PR.