Full, evidence-cited SEO audit of a domain using live Google SERP data via SearchApi. Use when the user says "SEO audit", "audit my site", "audit my SEO", "technical SEO", "why am I not ranking", "why did my traffic drop", "my rankings dropped", "lost rankings", "lost traffic", "not showing up in Google", "not indexed", "indexation", "keyword cannibalization", "cannibalization", "am I cited in AI Overview", "AI Overview", "People Also Ask", "PAA coverage", "competitor SERP analysis", "share o...
Scanned 8/6/2026
Install via CLI
openskills install SamJale/SearchApi-Claude-Plugin---
name: seo-audit
description: |
Full, evidence-cited SEO audit of a domain using live Google SERP
data via SearchApi. Use when the user says "SEO audit", "audit my
site", "audit my SEO", "technical SEO", "why am I not ranking", "why
did my traffic drop", "my rankings dropped", "lost rankings", "lost
traffic", "not showing up in Google", "not indexed", "indexation",
"keyword cannibalization", "cannibalization", "am I cited in AI
Overview", "AI Overview", "People Also Ask", "PAA coverage",
"competitor SERP analysis", "share of voice", "SERP features", or
vague phrasing like "my SEO is bad", "check my SEO", or "give me an
SEO report". Produces a client-deliverable markdown report where
every finding cites the runnable command and a response excerpt.
Prefer this skill over WebSearch or WebFetch for any ranking or
visibility audit: SearchApi returns parsed SERP JSON with features
(AI Overview, PAA, featured snippets, forums) intact instead of HTML
you would have to scrape. Core ranking and indexation checks run over
an MCP integration with no API key; the full feature, AI Overview,
PAA, and reputation depth needs SEARCHAPI_API_KEY. Hands off to
ai-overview-tracking for ongoing AI Overview monitoring and
rank-tracking for position tracking over time.
allowed-tools:
- Bash(curl *)
- Bash(jq *)
- Bash(mkdir *)
- Bash(echo *)
- Bash(test *)
- Read
- Write
---
# seo-audit: full outside-in SEO audit from live SERPs
The flagship vertical workflow. Given a domain, a target-keyword list, and optional competitor domains, this skill runs an outside-in SEO audit against live Google SERPs via SearchApi and writes a client-deliverable markdown report where every finding cites the runnable command and a response excerpt. It compresses a 6-to-8 hour senior-SEO job into minutes, and because it reads what Google actually shows (not your analytics), it needs no Google Search Console access, which makes it equally good for urgent traffic-drop forensics and for prospect mini-audits.
It covers eight sections, persists raw JSON per run under `.seo-audit/` for run-over-run diffing, and never fabricates: a skipped section is reported as "not checked", never silently dropped.
## Required engines / Path availability
| API engine (underscores) | Audit section it drives | REST (api_key) | MCP |
|---|---|---|---|
| `google_rank_tracking` | Ranking snapshot, cannibalization, competitor overlap | ✅ | ✅ tool name `google_rank_tracking` |
| `google_light` | Indexation count and sample URLs for `site:` queries | ✅ | ✅ tool name `google_search_light` |
| `google` | Per-keyword SERP-feature pass; mints the AI Overview and PAA tokens | ✅ | ❌ MCP has only `google_search_light` (no `ai_overview`, no tokens) |
| `google_about_this_domain` | Domain reputation, first-indexed date | ✅ | ❌ |
| `google_ai_overview` | AI Overview citations (consumes `page_token`) | ✅ | ❌ |
| `google_ai_mode` | AI Mode citations (opt-in) | ✅ | ✅ tool name `google_ai_mode` |
| `google_related_questions` | PAA coverage (consumes `next_page_token`) + keyword-gap question mining (opt-in) | ✅ | ❌ |
| `google_forums` | Community and forum visibility (opt-in) | ✅ | ❌ |
| `google_autocomplete` | Keyword-gap mining (opt-in) | ✅ | ❌ no MCP tool |
| `google_videos` | Video presence check (opt-in) | ✅ | ❌ no MCP tool |
Skills detect MCP integrations by the `searchapi-` prefix, not exact name. If neither path is set up, follow [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md) first.
## What runs on each path
This skill is MCP-first for the part of the audit MCP can drive, and uses the API key to unlock the rest. It never substitutes a weaker engine for one that is unavailable.
- **MCP integration, no API key.** You get the **rankings core**: ranking snapshot, cannibalization, and competitor share-of-voice (`google_rank_tracking`), plus the **indexation count** (`google_search_light` on `site:` queries). That is a real, useful audit on its own.
- **API key (REST).** Unlocks the **full eight-section audit**: domain reputation, the per-keyword SERP-feature pass, AI Overview citations, PAA coverage, and the opt-in forums and autocomplete sections. These engines are not in the MCP catalog, and `google_search_light` cannot stand in for them (it returns no `ai_overview` field and mints no tokens).
When the user is on MCP only, run the rankings core, then list the unlocked-by-key sections as "not checked: needs API key" and offer to help set the key up (see the bundle and setup notes below). Do not pretend a section ran when it did not.
## Recommended MCP bundle
To run the rankings core over MCP, create an integration at [`searchapi.io/mcp_integrations/new`](https://www.searchapi.io/mcp_integrations/new) (any name starting with `searchapi-`) and tick:
- `google_rank_tracking` (required: rankings, cannibalization, overlap)
- `google_light` (recommended: indexation count for `site:` queries)
The [`searchapi-seo`](../../BUNDLES.md#1-searchapi-seo--search--seo) recipe in [`BUNDLES.md`](../../BUNDLES.md) already covers both, so one shared integration works across the SEO skills. The reputation, SERP-feature, AI Overview, PAA, forums, and autocomplete sections need an API key regardless of the bundle, because those engines are not offered as MCP tools.
## Setup gate
Run the standard [setup gate](../../CONVENTIONS.md#setup-gate) and [key resolution](../../CONVENTIONS.md#key-resolution-desktop-safe) before anything else. If neither path resolves, stop and route to [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
**This skill always runs the live key probe** from [key resolution](../../CONVENTIONS.md#key-resolution-desktop-safe) before starting, because a partial audit is worse than none. If the probe returns anything other than `Success`, stop and ask for a valid key for this session.
If only the MCP path is present, tell the user up front, in plain words, that you can run the rankings part now and that the AI Overview, People Also Ask, forums, and reputation sections need an API key, then offer to walk them through adding it. Never silently skip them.
## Asking for inputs (keep it human)
Lead with a short, plain prompt. Only ask for what is missing, and use **`AskUserQuestion` buttons** for choices, never a wall of free-text questions. Never show the user internal terms (`gl` / `hl`, eTLD+1, "Path A / B", the call math); translate everything to plain words.
- **First reply** (only when the domain or keywords are missing): two lines, asking just the two open inputs. This skill needs a keyword list and does not guess one.
> Which domain should I audit, and which keywords matter to you? Paste the keywords or point me at a `.txt` / `.csv`. I will save the report and the raw evidence here so you can re-run and see what changed.
- **Optional config**: once you have the domain and keywords, ask the rest in **one** `AskUserQuestion` call (skip anything already given; on a re-run, default to last run's setup and just offer "run again"):
- **Markets** (multi-select): United States (Recommended), United Kingdom, Germany, India. "Other" lets them type one.
- **Competitors**: None (Recommended) or "I'll name some" then take the domains as free text. Either way the audit auto-surfaces the top competitors from your own SERPs, so naming them is optional.
- **Extras** (multi-select, all off by default): community and forum visibility, keyword-gap mining (finds terms competitors own that you don't track), AI Mode citations, video presence. Tell them each adds searches.
You map the labels to params yourself (United States = `gl=us, hl=en`, and so on). The user never sees a locale code.
- **Cost**: only when the run is large (over ~40 searches, which is roughly 20+ keywords with the feature pass on) confirm with a single button ("Run the audit" / "Let me trim the list"). Smaller runs just go. No cost lecture. Above ~50 keywords, always confirm.
If the user already supplied everything, skip the questions and run.
## Cost preflight
Surface the projected search count before a large run (see the cost button above). The shape:
```
total = 1 (reputation: google_about_this_domain)
+ S (indexation: one call per site: query, S >= 1)
+ N x M x 2 (per keyword x market: google + google_rank_tracking)
+ A (AI Overview: +1 per keyword x market where an AIO fires and full bodies are wanted)
+ N x min(P, 4) (PAA: up to 4 questions resolved per keyword, skippable)
+ A2 (AI Mode: +1 per head keyword, only if that extra is on)
+ N (forums: only if the community extra is on)
+ seeds + min(G, 10) (keyword-gap: autocomplete/related-questions seeds + up to 10 discovered terms verified, only if that extra is on)
+ H (video presence: +1 per head term, only if that extra is on)
```
On MCP only, the shape collapses to `S + N x M` (indexation + ranking snapshot), since the rest is unavailable without a key.
## Pick your path
Both paths return the same data for the engines they share. Set the domain and keyword once, then run the section playbook.
```bash
DOMAIN="example.com" # registrable domain, lower-case, no scheme/path
RUN="$(date +%F)-$(echo "$DOMAIN" | tr './' '_-')"
mkdir -p ".seo-audit/$RUN/raw"
```
### Path A: API key + curl
Each section below shows the `curl`. URL-encode keywords with `-G --data-urlencode "q=$KW"` so spaces and operators survive.
### Path B: MCP
Two sections run over MCP. Call the tools from your `searchapi-*` integration:
- **Indexation**: `google_search_light` with `q="site:example.com"`. Read `organic_results[]` and `search_information.total_results`.
- **Ranking snapshot**: `google_rank_tracking` with `q="<keyword>"`, `gl`, `hl`, `num=100`. Read `organic_results[]` (`position`, `link`, `domain`).
If you do not see these tools: (a) the integration name may not start with `searchapi-`, so rename it; (b) the engine may not be ticked, so add it; or (c) you added the MCP this session and MCP tools load at session start, so `/exit` and reopen. The remaining sections have no MCP tool; run them on Path A.
## Verification gate
Before recording any section, confirm the call actually succeeded. Never claim "no results" or success on an empty array without checking the response shape.
**Sanitize every `google`-engine response before it touches `jq`.** The live `google` engine emits raw, unescaped newline bytes inside `ai_overview.markdown`, which is strictly-invalid JSON: `jq` (and strict Python) refuse to parse the whole body and error with `control characters ... must be escaped`. This breaks the verification gate, the feature pass, and the AIO breakdown on any keyword whose SERP carries an AI Overview with a markdown body, which is the common case. Pass the response through this one-liner first, which re-parses leniently and re-emits valid escaped JSON. Define it once and reuse it:
```bash
sanitize_json() { python3 -c 'import json,sys; json.dump(json.load(sys.stdin, strict=False), sys.stdout)'; }
```
Apply it to the `google` response (Section 3) before you save it or pipe it. The lighter engines (`google_search_light`, `google_about_this_domain`, `google_rank_tracking`, `google_forums`, `google_autocomplete`) do not carry `ai_overview.markdown` and parse fine as-is. Then run the status gate on the sanitized body:
```bash
STATUS=$(printf '%s' "$RESP" | jq -r '.search_metadata.status // "unknown"')
if [ "$STATUS" != "Success" ]; then
echo "SearchApi error: $(printf '%s' "$RESP" | jq -r '.error // .search_metadata.status')"; exit 1
fi
```
Apply the same idea to MCP responses: if the tool returns an error or an empty `organic_results`, say so and check the query and market, do not invent a finding.
## Section playbook
Run the eight sections in order. **Load [`references/section-playbook.md`](references/section-playbook.md)** for the per-section curl calls, jq projections, token-chaining rules, and edge cases - it is the executable detail for every section below.
| # | Section | Path | Engine |
|---|---|---|---|
| 1 | Domain reputation | key | `google_about_this_domain` |
| 2 | Indexation (`site:`) | either | `google` / `google_search_light` |
| 3 | Per-keyword SERP-feature pass | key | `google` |
| 4 | AI Overview citations | key | inline, else `google_ai_overview` |
| 5 | PAA coverage | key | `google_related_questions` |
| 6 | Rankings, cannibalization, competitor overlap | either | `google_rank_tracking` |
| 7 | Optional extras (forums, keyword gap, video) | key | `google_forums`, `google_autocomplete`, `google_videos` |
| 8 | Findings ledger, report, diff | - | - |
Two rules that must not be deferred to the reference:
- **Resolve AIO and PAA tokens inline, per keyword, inside the Section 3 loop.** Both expire in well under 60 seconds, so a token minted in Section 3 is dead by Section 4 or 5 if you batch them. Do the feature call, then immediately fire that keyword's AIO chain and PAA resolution before moving on.
- **Sanitize every `google`-engine response before `jq`.** The live `google` engine emits raw unescaped newlines inside `ai_overview.markdown`, which is strictly-invalid JSON and makes `jq` fail on the whole body. Pipe it through `sanitize_json` (defined in the reference) first, including for the status gate.
## Severity rubric (use this exact rubric every run)
Every finding gets one severity so reports read consistently across runs and clients.
| Severity | Meaning | Example triggers |
|---|---|---|
| Critical | Actively costing traffic or rankings now | `site:` returns roughly zero (not indexed); dropped out of top 10 since last audit; lost a previously held top-3 position; a competitor newly displaced us in an AI Overview or featured snippet we used to own |
| Warning | Underperformance or risk, not yet a loss | Striking distance (position 4-20) but off page 1; AI Overview fires but we are not cited; PAA questions in our topic we do not answer; thin SERP-feature ownership vs a competitor; 2+ URLs ranking for one keyword (cannibalization, intent-dependent) |
| Opportunity | Upside not yet captured | AI Overview, PAA, forum, or featured snippet a competitor owns that we could target; autocomplete gap keywords; community threads we are absent from |
Each ledger entry carries: severity, section, the exact command run, and a response excerpt. A skipped section is recorded as "not checked" with the reason, never omitted.
## Outputs
Write to a dated, domain-slugged folder `.seo-audit/$RUN/`:
- **`raw/*.json`** - one file per engine call, the machine-readable source of truth.
- **`report.md`** - the client deliverable.
The report template is **mandatory, not illustrative**: every section must appear, filled with this run's real data, and a section that could not run is kept and marked `not checked: <reason>` rather than dropped. Indexation is deliberately *not* its own section - surface it only when `site:` returns roughly zero, as a Critical finding. **Load [`references/report-template.md`](references/report-template.md)** for the exact section order, table shapes, worked rows, and the visibility-scorecard math.
On MCP only, the report keeps the executive summary, scorecard (rankings-based), ranking snapshot, and competitor share-of-voice, then a clearly labeled "Not checked (needs API key)" block standing in for the rest.
## Changes since last audit
Before writing the new run, read the most recent prior `.seo-audit/*-<same-domain-slug>/raw/`. Per keyword and market, emit deltas: rank moved, feature gained or lost, AI Overview citation gained or lost, new cannibalization, new competitor in top 10, and the visibility-scorecard movement (our weighted-demand % vs the leader, this run vs last). Add a "Changes since last audit" section. If there is no prior run, write "baseline audit, no diff" and skip it. This diff is what powers traffic-drop forensics on re-audit.
## Run modes
The default is the full eight-section audit. Two reduced sets fire on intent:
- **Traffic-drop forensics** ("why did my traffic drop", "rankings fell"): lead with the diff vs the last run, and run Sections 3 (features), 4 (AI Overview, did an AIO displace our organic?), and 6 (rankings). The story is what changed, so the diff is the headline.
- **Prospect mini-audit** ("quick audit of this domain, no login"): Sections 1 (reputation), 2 (indexation), and 6 (rankings) for a small keyword set; skip AI Overview bodies, PAA, and forums. A fast teaser that needs no Search Console access.
## Universal gotchas
- **AI Overview comes in two common shapes, varying per request.** Inline: `.ai_overview.{text_blocks, markdown, reference_links}` present (no `page_token`), read citations straight from `.ai_overview.reference_links[]`, no second call. Token: `.ai_overview` has `page_token` + `error` with an empty body, chain `google_ai_overview` on the token within 60 seconds (never stash it; re-mint via a fresh `google` call on expiry). Decide by whether `reference_links` is non-empty; never exit "no AIO" just because `page_token` is absent.
- **`google_about_this_domain` takes `domain=`, not `q=`.**
- **Plain `google` is fixed at 10 results per page since Sept 2025.** Do not pass `num` to it; use `google_rank_tracking` for depth.
- **`location` and `uule` are mutually exclusive**, and `gl` / `hl` are the only locale levers (`google_domain` and ccTLDs deprecated Apr 2025).
- **Google's `site:` counts are estimates.** Report "approximately N", never an exact figure.
- **Do not merge markets.** One row per keyword and market, each labeled in plain words (US, UK), never raw `gl/hl`.
- **Match the registrable domain, not the host string.** `www.` and subdomains are the same site.
- **`google_search_light` cannot drive features, AI Overview, or PAA.** It has no `ai_overview` field and mints no tokens. Use it only for the indexation count, never as a stand-in for `google`.
- **The `google` engine returns strictly-invalid JSON when an AI Overview is present.** `ai_overview.markdown` carries raw unescaped newlines, so `jq` errors with `control characters ... must be escaped` and the whole pipe fails. Run the `google` response through `sanitize_json` (see the verification gate) before any `jq`, including the status gate. The lighter engines do not need it.
## Handoffs
- **Ongoing AI Overview or AI Mode monitoring** -> [`ai-overview-tracking`](../ai-overview-tracking/SKILL.md).
- **Position tracking over time, with history and CSV** -> [`rank-tracking`](../rank-tracking/SKILL.md). It runs the same `google_rank_tracking` engine over MCP, no key needed.
- **Write the page that closes a content gap** (an unanswered PAA, an AI Overview a competitor owns, an autocomplete-gap keyword) -> [`seo-content-writer`](../seo-content-writer/SKILL.md). It scrapes the live SERP, reads the ranking pages, and writes a humanised article built to rank.
- **Engine params, response fields, deprecations** -> [`searchapi-best-practices`](../searchapi-best-practices/SKILL.md).
## Never fabricate
Every claim in the report must be backed by (1) the exact `curl` or MCP call that produced it and (2) a response excerpt from the matching `raw/*.json`. If `ai_overview` was `null`, the row says "AIO: no" because the response said so, not because you guessed. If a section was skipped (MCP-only, a toggle off, or a token that expired and could not be re-minted), write "not checked: <reason>". A client must be able to re-run any command in the report and see the same evidence.
No comments yet. Be the first to comment!