Skill: meirk-brd/brightdata-seo-audit (version 1.0.0)

--- name: seo-audit description: When the user wants to audit, review, or diagnose SEO issues on their site. Uses live web data via the Bright Data CLI for accurate detection of JS-injected schema, hreflang, canonicals, and live SERP-based ranking checks. Also use when the user mentions "SEO audit," "technical SEO," "why am I not ranking," "SEO issues," "on-page SEO," "meta tags review," "SEO health check," "my traffic dropped," "lost rankings," "not showing up in Google," "site isn't ranking," "Google update hit me," "page speed," "core web vitals," "crawl errors," or "indexing issues." Use this even if the user just says something vague like "my SEO is bad" or "help with SEO" — start with an audit. For building pages at scale to target keywords, see programmatic-seo. For implementing structured data, see schema-markup. For AI search optimization, see ai-seo. --- # SEO Audit (Bright Data) You are an expert in search engine optimization. Your goal is to identify SEO issues and provide actionable recommendations to improve organic search performance — using the Bright Data CLI (`bdata`) to access live, JavaScript-rendered web data. **Never fabricate findings.** Every finding cites a runnable `bdata` command + an output excerpt as Evidence. If `bdata` cannot directly measure something, route it to the report's `Out-of-Scope Notes` section with a pointer to the right tool (PageSpeed Insights, Google Search Console, Ahrefs, etc.). ## Why Bright Data The inspiration for this skill noted that `web_fetch` and `curl` cannot detect JS-injected schema markup (Yoast, RankMath, AIOSEO, Next.js). `bdata scrape -f html` runs the page through Bright Data's rendering layer, so JS-injected `<script type="application/ld+json">` blocks are visible. Same for client-side hreflang and canonical injection. Same for SERP — `bdata search` returns parsed Google/Bing/Yandex results we can use for indexation, ranking, and cannibalization checks. ## Prerequisites The user must have the Bright Data CLI installed and authenticated: ```bash curl -fsSL https://cli.brightdata.com/install.sh | bash bdata login ``` If `bdata` is missing or unauthenticated, stop and point at the **brightdata-cli** skill — it has the full installation walkthrough including SSH/headless and direct-API-key paths. Don't reproduce that walkthrough here. ## Initial Assessment **Check for product marketing context first:** If `.agents/product-marketing-context.md` exists (or `.claude/product-marketing-context.md` in older setups), read it before asking questions. Use that context and only ask for information not already covered. **Then clarify:** 1. **Site context** — What type of site? Primary business goal for SEO? Priority keywords/topics? 2. **Current state** — Known issues? Current organic traffic level? Recent changes or migrations? 3. **Scope** — Full site audit or specific pages? Search Console / analytics access? ## Mode Selection The skill auto-routes between two modes based on the user's input: - **Mode A — Single-page deep audit.** User gave a single URL and asked about that page (or asked "why isn't this page ranking"). Audit covers the page, its `robots.txt`, its `sitemap.xml`, and the homepage if different. ~5–10 `bdata` calls. - **Mode B — Site-wide audit.** User gave a domain or said "audit my site". Sitemap-stratified sampling, default 10–15 pages, budget configurable. ~20–40 `bdata` calls. If the input is ambiguous (single URL but no page-specific question), default to Mode A and ask whether to expand to Mode B. ## SERP Triggers (mode-independent) `bdata search` runs only when there is a clear signal: - User mentions a target keyword. - User asks "why am I not ranking for X" / "traffic dropped" / similar. - User asks about a specific page's performance. Generic "audit my site" prompts do **not** trigger keyword-ranking SERP queries. The one exception that always fires: a single `bdata search "site:<domain>" --json` for the indexation proxy in Tier 1 (R-12). This is one SERP call total per audit, too cheap to skip. ## Workflow ### 1. Gather (always) - **Mode B**: fetch `robots.txt` (R-01) + `sitemap.xml` (R-02) → URL list → stratified sample 10–15 URLs (R-03) → parallel-fetch sample (R-04). Always parallelize: single Bash message, multiple `bdata scrape` tool calls. - **Mode A**: fetch the target URL + homepage + `robots.txt` + `sitemap.xml`. - Always: indexation proxy (R-12). ### 2. Detect site type (R-15) Apply matching playbook(s) from `references/site-type-playbooks.md`. Multiple playbooks can apply. ### 3. Run framework checks Walk the priority order from `references/audit-framework.md`: 1. Crawlability & Indexation 2. Technical Foundations 3. On-Page Optimization 4. Content Quality 5. Authority & Links (HTML-only) If a Tier-1 issue is critical (e.g., `Disallow: /` in robots.txt), report it as the top priority, caveat all downstream sections, but **continue running lower tiers and report what you find** — the user needs the full picture even when Tier 1 is broken. Per the Hard Rule, every lower-tier finding still needs an Evidence block; if a check cannot run because the Tier-1 blockage prevents fetching the page, omit it rather than fabricate. ### 4. Run signal-driven SERP (if triggered) - R-13 ranking position for each user-supplied target keyword. - R-14 cannibalization for each user-supplied target keyword. ### 5. Format report Use the exact structure from `references/output-templates.md`. Every finding has Issue / Impact / Evidence / Fix / Priority. Evidence cites the `bdata` command + output excerpt. ## Hard Rules 1. **Never claim "no schema found" without running R-07.** `bdata scrape -f html` already renders JavaScript — there is no detection-limitation excuse here. The inspiration skill's biggest pain point doesn't apply to us. 2. **Every finding has Evidence.** Command + output excerpt. No exceptions. No fabricated findings. 3. **Things `bdata` can't measure go to `Out-of-Scope Notes`** with a pointer to the right tool. CWV field data → PageSpeed Insights. Coverage detail → Google Search Console. Backlinks → Ahrefs/Semrush. We provide HTML-level CWV proxies but always caveat them. 4. **Parallelize page fetches** — single Bash message, multiple `bdata scrape` tool calls. Never loop sequentially over the sampled URLs. 5. **Default budget 10–15 pages** for Mode B. The user can request a larger budget in natural language ("audit 30 pages") — there is no `bdata` CLI flag for this; it's an audit-level parameter the skill applies when sampling URLs in R-03. 6. **No SERP fishing** — keyword SERP queries (R-13/R-14) only fire on a user-supplied keyword or diagnostic-prompt signal. The `site:` indexation proxy (R-12) is the only always-on SERP call. 7. **Cite Out-of-Scope Notes for everything we don't measure** — being honest about limits is the skill's contract with the user. ## References - [audit-framework.md](references/audit-framework.md) — Five-tier priority order, every check. - [bdata-recipes.md](references/bdata-recipes.md) — 25 concrete `bdata` recipes (R-01..R-25). - [site-type-playbooks.md](references/site-type-playbooks.md) — SaaS / e-commerce / blog / local / multilingual extras. - [output-templates.md](references/output-templates.md) — Report structure, finding shape, exec-summary rubric. ## Related Skills - **brightdata-cli** — for installation/login walkthrough and full `bdata` command reference. - **scrape** — for ad-hoc scraping outside an audit context. - **search** — for ad-hoc SERP queries outside an audit context. - **schema-markup** — if user wants to *implement* (not audit) structured data; defer. - **competitive-intel** — for cross-competitor analysis (overlaps on SEO content/positioning). - **programmatic-seo** — for building pages at scale to target keywords. - **ai-seo** — for AEO / GEO / LLMO / AI Overview optimization.

path: evals/evals.json
content: {
  "skill_name": "seo-audit",
  "evals": [
    {
      "id": 1,
      "prompt": "Can you do an SEO audit of our SaaS website? We're getting about 2,000 organic visits/month but feel like we should be getting more. URL: https://example.com",
      "expected_output": "Comprehensive SEO audit following the five-tier priority order, with each finding citing a runnable bdata command and output excerpt as Evidence. Should detect JS-injected schema via bdata scrape -f html, sample 10-15 pages via sitemap stratification, and produce the full Executive Summary + Findings + Out-of-Scope + Action Plan structure.",
      "expectations": [
        "Checks for .agents/product-marketing-context.md or .claude/product-marketing-context.md before deep questioning",
        "Asks clarifying questions about priority keywords, Search Console access, recent changes, and competitors",
        "Runs bdata scrape https://example.com/sitemap.xml first for URL discovery, not homepage-only",
        "Applies stratified sampling across path-prefix groups (default 10-15 pages)",
        "Issues all bdata scrape calls for the sample in parallel (single Bash message, multiple tool calls)",
        "Uses bdata scrape -f html to detect JS-injected schema markup (no caveat needed)",
        "Follows audit priority order: crawlability first, then technical, on-page, content, authority",
        "Checks robots.txt and XML sitemap with bdata scrape",
        "Evaluates title tags, meta descriptions, heading structure",
        "Output has Executive Summary with Top priorities and Quick wins",
        "Output has Prioritized Action Plan",
        "Each finding has Issue, Impact, Evidence, Fix, and Priority",
        "Each Evidence field cites a runnable bdata command and an output excerpt"
      ]
    },
    {
      "id": 2,
      "prompt": "Why am I not ranking for 'project management software'? We have a page targeting that keyword but it's stuck on page 3.",
      "expected_output": "Diagnostic audit triggered by the casual 'why am I not ranking' phrasing. Should run bdata search for the keyword to see who's ranking, check the user's domain position, and run a cannibalization check via site: query.",
      "expectations": [
        "Triggers signal-driven SERP: runs bdata search 'project management software' --json to see top results",
        "Identifies user's current rank position (or absence) from the SERP results",
        "Runs cannibalization check: bdata search 'project management software site:<domain>' --json",
        "Checks title tag, H1, URL alignment with target keyword on the user's page",
        "Evaluates content depth vs. top-ranking competitors using their scraped pages",
        "Checks indexation status and canonical tags",
        "Assesses E-E-A-T signals (author byline, dates, outbound citations)",
        "Provides actionable fixes organized by priority",
        "Each finding has Issue, Impact, Evidence, Fix, and Priority"
      ]
    },
    {
      "id": 3,
      "prompt": "We just migrated from WordPress to Next.js and our organic traffic dropped 40% in the last month. Help!",
      "expected_output": "Urgent migration diagnostic. Should check redirect mapping, canonical tags, robots.txt, sitemap, soft 404s, and use bdata search 'site:<domain>' to spot indexation regression.",
      "expectations": [
        "Treats as urgent migration diagnostic",
        "Uses bdata scrape on suspected old URLs to detect 404s vs. 301s vs. soft-404s",
        "Cross-references sitemap-listed URLs with bdata search 'site:<domain>' --json to spot indexation regression",
        "Checks redirect mapping (301s from old URLs to new)",
        "Checks canonical tags on new pages",
        "Checks robots.txt for unintentional blocks",
        "Checks XML sitemap is updated and accessible",
        "Checks for redirect chains or loops",
        "Checks for soft 404s",
        "Provides prioritized recovery plan",
        "Mentions recovery timeline expectations (weeks)"
      ]
    },
    {
      "id": 4,
      "prompt": "Review the technical SEO of our e-commerce site. We have about 50,000 products and use faceted navigation.",
      "expected_output": "Site-wide audit applying the e-commerce playbook. Stratified sampling MUST include homepage, at least one category page, and at least one product page. Detects faceted-nav parameter URLs and crawl-budget risk.",
      "expectations": [
        "Applies stratified sampling: homepage + at least one category + one product page",
        "Detects faceted-nav parameters in sitemap (URLs containing '?') and flags crawl-budget risk",
        "Uses bdata scrape -f html to actually detect product schema (no detection-limitation caveat)",
        "Checks for thin category pages via word count",
        "Checks for duplicate product descriptions across sampled product pages",
        "Addresses out-of-stock product page handling",
        "Addresses pagination and infinite scroll",
        "Findings include Impact ratings and specific fixes",
        "Each finding has Issue, Impact, Evidence, Fix, and Priority"
      ]
    },
    {
      "id": 5,
      "prompt": "Can you check our blog posts for on-page SEO issues? We publish 4 posts per week but traffic has been flat for 6 months.",
      "expected_output": "Site-wide audit applying the content/blog playbook. Stratified sample includes at least 3 blog posts. Extracts publication/updated dates to identify outdated posts.",
      "expectations": [
        "Stratified sample includes at least 3 blog post pages from sitemap",
        "Extracts publication/updated dates via <time datetime> or Schema.org dateModified to identify outdated posts",
        "Checks for keyword cannibalization (only if target keywords are provided or inferable)",
        "Checks for topical clustering via internal-link map",
        "Checks for author pages reachable (no 404s on /author/<x>)",
        "Audits title tags, meta descriptions, heading structure per post",
        "Assesses E-E-A-T signals (author byline, dates, outbound citations)",
        "Provides prioritized action plan with refresh candidates and content audit recommendations"
      ]
    },
    {
      "id": 6,
      "prompt": "I run a local plumbing business with 3 locations. My website barely shows up when people search for 'plumber near me' in our areas. What's wrong?",
      "expected_output": "Local-business audit with NAP consistency check, LocalBusiness schema detection (now possible since we render JS), per-location SERP visibility check.",
      "expectations": [
        "Applies the Local Business playbook",
        "Stratified sample includes each /locations/* page found in sitemap",
        "Uses bdata scrape -f html to actually detect LocalBusiness schema (no caveat)",
        "Extracts NAP from each sampled page and flags inconsistencies (regex-based normalization)",
        "Runs bdata search 'plumber <city>' for each city to validate local SERP visibility",
        "Recommends individual location pages where missing",
        "Recommends local content strategy",
        "Checks standard technical SEO factors too",
        "Provides prioritized local SEO action plan"
      ]
    },
    {
      "id": 7,
      "prompt": "Our site loads really slowly, especially on mobile. Pages take 5-6 seconds to load. Is this hurting our SEO?",
      "expected_output": "CWV-focused audit using HTML-level proxies (page weight, render-blocking scripts, images without dimensions, lazy loading, resource hints). Always paired with explicit pointer to PageSpeed Insights for field measurement.",
      "expectations": [
        "Confirms page speed impacts SEO rankings",
        "Explains CWV thresholds (LCP < 2.5s, INP < 200ms, CLS < 0.1)",
        "Identifies 5-6s as well above acceptable",
        "Reports HTML-level CWV proxies from bdata scrape -f html: page weight, render-blocking script count, images-without-dimensions count, lazy-loading presence, resource hints",
        "Explicitly directs user to PageSpeed Insights at https://pagespeed.web.dev for field-data CWV measurement (in Out-of-Scope Notes)",
        "Provides prioritized speed fixes",
        "Addresses mobile-specific performance considerations"
      ]
    },
    {
      "id": 8,
      "prompt": "I want to add FAQ schema to my product pages. Can you help me set that up?",
      "expected_output": "Recognizes this as a schema markup implementation task, not an audit. Defers to the schema-markup skill.",
      "expectations": [
        "Recognizes this as a schema markup implementation task, not an SEO audit",
        "References or defers to the schema-markup skill",
        "Does not attempt a full SEO audit",
        "May briefly mention FAQ schema benefits but does not implement"
      ]
    },
    {
      "id": 9,
      "prompt": "Can you audit https://example-shop.com? It's a WooCommerce site with maybe 200 pages.",
      "expected_output": "Mode B site-wide audit with sitemap-stratified sampling. Demonstrates the architectural distinguisher: stratified sample across path-prefix groups, parallel page fetch, default 10-15 page budget.",
      "expectations": [
        "Fetches bdata scrape https://example-shop.com/sitemap.xml -f html before scraping any individual page",
        "Groups discovered URLs by path prefix (e.g., /product/, /category/, /blog/, /)",
        "Stratified sample includes homepage + per-group representatives, not 10 random URLs",
        "Default page budget is 10-15; documents how user can raise budget",
        "All bdata scrape calls for the sample are issued in parallel (single Bash message, multiple tool calls)",
        "Detects WooCommerce site type via R-15 cues",
        "Applies the e-commerce playbook (category page, product page, faceted-nav check)",
        "Output has the full Executive Summary + Findings + Out-of-Scope + Action Plan structure"
      ]
    },
    {
      "id": 10,
      "prompt": "Audit my site https://example.com — I really need to rank for 'CRM for small business'.",
      "expected_output": "Audit with signal-driven SERP triggered by the named target keyword. Demonstrates the architectural distinguisher: SERP fires only on signal, never gratuitously.",
      "expectations": [
        "Runs bdata search 'CRM for small business' --json because user named a target keyword",
        "Identifies user's current rank position (or absence) for that keyword",
        "Runs cannibalization check: bdata search 'CRM for small business site:example.com' --json",
        "Identifies top 3 ranking competitor URLs for content-depth comparison",
        "Also runs the always-on indexation proxy: bdata search 'site:example.com' --json (R-12)",
        "Performs the standard site-wide audit alongside the SERP work",
        "Each finding has Issue, Impact, Evidence, Fix, and Priority"
      ]
    }
  ]
}


path: references/audit-framework.md
content: # Audit Framework — SEO Audit

The auditor's brain. Five-tier priority order, every check, when to short-circuit. Recipes (concrete `bdata` commands) live in `bdata-recipes.md`. Site-type extras live in `site-type-playbooks.md`. Report shape lives in `output-templates.md`.

## Priority Order (Short-Circuiting)

The audit walks tiers top-to-bottom; lower-tier findings are partial signal until higher-tier blockers are fixed.

1. **Crawlability & Indexation** — can Google find and index the site at all?
2. **Technical Foundations** — is the site fast, secure, mobile-friendly?
3. **On-Page Optimization** — titles, meta, headings, internal links.
4. **Content Quality** — does each page deserve to rank?
5. **Authority & Links** — out of scope for direct measurement; HTML red flags only.

When a Tier-1 critical issue is found (e.g., `Disallow: /` in robots.txt), the rule is: report it as the top priority, explicitly tell the user that lower-tier findings may be misleading until the Tier-1 issue is fixed, but **still run lower tiers and report what you find** — caveat each downstream section so the user has a full picture without taking the findings as definitive. Lower-tier findings still require an Evidence block per the Hard Rule below; if a check cannot run because the Tier-1 blockage prevents fetching the page, omit it from the report rather than fabricate a finding.

## Tier 1 — Crawlability & Indexation (always run)

- **robots.txt** — fetch with R-01. Parse for `Disallow:` directives that match indexable paths (defined as: any path appearing in the sitemap, or the homepage, or any path matching common SEO-important patterns — `/products/`, `/category/`, `/categories/`, `/blog/`, `/posts/`, `/articles/`, `/locations/`, `/services/`, `/about`, `/`). Standard utility paths (`/wp-admin`, `/cgi-bin`, `/api`, `/admin`, `/cart`, `/checkout`, `/account`) being disallowed is normal and should not be flagged. Also confirm a `Sitemap:` line is present.
- **sitemap.xml** — fetch with R-02. Direct check: exists, parseable, contains canonical and indexable URLs, uses `<xhtml:link>` if multilingual.
- **Indexation proxy** — R-12: `bdata search "site:<domain>" --json`. Approximate indexed-URL count from the SERP sample. Flag as critical when the indexed sample is below 30% of the sitemap URL count for sitemaps under 100 URLs, or when the absolute gap exceeds 100 URLs for larger sitemaps (e.g., 12 indexed vs. 847 in sitemap is critical; 6 indexed vs. 10 is not because `site:` returns a sample, not exact counts). Always note in the finding that `site:` is a sample, not a precise index count, and direct the user to Google Search Console's Coverage report for authoritative numbers (Out-of-Scope Notes).
- **Per-page robots and canonical** — from rendered HTML: `<meta name="robots">`, `<link rel="canonical">`, self-referencing canonical. Flag `noindex` on indexable pages (using the same definition as the robots.txt rule above: pages that appear in the sitemap, the homepage, or pages matching common SEO-important path patterns). `noindex` on utility pages like `/cart` or `/account` is normal and should not be flagged.
- **Hreflang** (multilingual sites only) — R-08: parsed from rendered HTML head and sitemap. Self-referencing entry, reciprocal links, valid ISO codes, `x-default` present.

## Tier 2 — Technical Foundations

- **HTTPS** — final URL after redirects. If `bdata scrape http://<domain>` lands on `https://`, we're fine.
- **Mobile-friendliness** — viewport meta tag present, no fixed-width inline styles, responsive image markup (`srcset`, `<picture>`).
- **Core Web Vitals proxies** (HTML-level only — explicit "confirm with PageSpeed Insights" caveat in every CWV finding):
  - Page weight from total HTML bytes.
  - Render-blocking — count of synchronous `<script>` (no `async`/`defer`) and non-deferred `<link rel="stylesheet">` in `<head>`.
  - CLS risk — `<img>` tags missing explicit `width`/`height`.
  - Resource hints — `preconnect` / `preload` / `dns-prefetch` for critical origins.
  - Lazy loading — `loading="lazy"` on below-fold images.
- **URL structure** — readable, no parameters, hyphenated, lowercase, consistent trailing slash.

## Tier 3 — On-Page Optimization (per sampled page)

- **Title tag** — present, unique across sample, 50–60 chars, primary topic word in first half.
- **Meta description** — present, unique, 150–160 chars.
- **Heading structure** — exactly one H1, H1 contains primary topic, no skipped levels.
- **Schema markup** — R-07: `bdata scrape -f html` (CLI renders JS, so this is a direct check). Extract `script[type="application/ld+json"]` blocks and parse with `jq` for `@type` + basic shape validity. **Never claim "no schema found" without running R-07** — the inspiration skill got bitten by `web_fetch` falsely reporting no schema.
- **Image alt text** — count of `<img>` without `alt`; ratio across page.
- **Internal linking** — R-10: internal-link count per page, descriptive anchor text vs. generic ("click here", "read more"), orphan-page detection across the sample.

## Tier 4 — Content Quality

- **Word count** + paragraph count + heading-to-content ratio.
- **E-E-A-T signals** from HTML: author byline present, date published/updated visible, links to authoritative sources, contact/about reachable.
- **Keyword cannibalization** — only when SERP is triggered by a signal (user named a target keyword, asked "why am I not ranking", or asked about specific page performance). R-14: `bdata search "<keyword> site:<domain>" --json` and flag if multiple URLs from the domain rank.
- **AI-writing red flags** — em-dash overuse, hedge phrases ("delve", "in the realm of"). Soft signal only — never a hard finding.

## Tier 5 — Authority & Links

Out of scope for direct measurement. The report's `Out-of-Scope Notes` directs the user to Ahrefs / Semrush for backlink data. Flag only what HTML reveals — testimonials, customer logos, case studies — as "trust signals" in Tier 4.

## SERP Trigger Rules (mode-independent)

`bdata search` runs only when there is a clear signal — regardless of whether the audit is Mode A or Mode B:

- User mentions a target keyword.
- User asks "why am I not ranking for X" / "traffic dropped" / similar diagnostic prompts.
- User asks about a specific page's performance.

Generic "audit my site" prompts do **not** trigger SERP queries.

The one exception that always fires: R-12 (`bdata search "site:<domain>"`) for indexation proxy in Tier 1. This is one SERP call total per audit and is too cheap to skip.

## The Hard Rule

Any check we cannot perform via `bdata` produces a clearly-labeled "out-of-scope, use [tool]" entry in `Out-of-Scope Notes` rather than a fabricated finding. **Never claim a finding without an Evidence block citing a runnable command.**


path: references/bdata-recipes.md
content: # bdata Recipes — SEO Audit

Concrete `bdata` commands for each audit step. Each recipe has the same four parts: Purpose, Command(s), Parsing, Evidence-to-cite. Parse-only recipes that re-use rendered HTML from R-04 (rather than making a fresh `bdata` call) omit the Command section — their input is whichever R-04 page output the analysis is being run against.

## Per-Audit Data Flow (Mode B)

```
1. Discover     → R-01 (robots.txt) + R-02 (sitemap.xml) → URL list, group by path prefix
2. Sample       → R-03 stratified pick of 10–15 URLs (homepage + per-section samples)
3. Fetch        → R-04 parallel: bdata scrape <each-url> -f html
                  (single Bash message, multiple tool calls)
4a. SERP (always)→ R-12 site: indexation proxy (one call, always run)
4b. SERP (cond.) → R-13/R-14 (only when user named a target keyword or asked a diagnostic prompt)
5. Analyze      → parse rendered HTML per page (R-05..R-11), aggregate site-wide signals
6. Report       → format per output-templates.md
```

## Cost Discipline (read first)

- **Default page budget**: 10–15 pages for site-wide audits. The user can ask for a larger budget in natural language (e.g., "audit 30 pages") — there is no `bdata` CLI flag for this; it's an audit-level parameter the skill applies when sampling URLs in R-03.
- **Always parallelize** the sampled-page fetches: single Bash message, multiple `bdata scrape` tool calls. Do not loop sequentially.
- **Always pass `--json`** when piping to `jq`.
- **For arbitrary websites, `bdata scrape` is correct** — there is no `bdata pipelines` type for generic-website SEO. Pipelines exist for platforms (Amazon, LinkedIn, etc.); skip them here.
- **Never re-fetch** the same URL twice in one audit.

## Failure Handling

- If `bdata scrape <url>` returns empty/error, note it as a finding (5xx, blocked, etc.) and continue. Don't abort.
- If `sitemap.xml` is 404, fall back to homepage + `<a href>` extraction (R-10) for URL discovery — degraded but functional.
- If `bdata` is missing or unauthenticated, stop immediately and point the user at the prerequisites section in SKILL.md and the `brightdata-cli` skill.

---

## Recipes

### R-01: Fetch robots.txt

**Purpose:** Detect `Disallow: /` on important paths and presence of `Sitemap:` reference.

**Command:**
```bash
bdata scrape https://example.com/robots.txt -f html
```

**Parsing:**
```bash
# Disallow lines
grep -E '^Disallow:' robots.txt
# Sitemap reference
grep -iE '^Sitemap:' robots.txt | awk '{print $2}'
```

**Evidence to cite:** Command + `Disallow:` lines verbatim.

---

### R-02: Fetch sitemap.xml

**Purpose:** Pull the URL inventory for stratified sampling.

**Command:**
```bash
bdata scrape https://example.com/sitemap.xml -f html
```

**Parsing:**
```bash
# Extract <loc> URLs
grep -oE '<loc>[^<]+</loc>' sitemap.xml | sed -E 's|</?loc>||g'
# If sitemap-index, recurse: each <loc> points to a sub-sitemap
# Hreflang entries (multilingual)
grep -oE '<xhtml:link[^/]+/>' sitemap.xml
```

If sitemap is a sitemap-index (top-level `<sitemapindex>`), fetch each child sitemap with R-02.

**Evidence to cite:** Command + total URL count + first 5 URLs as a sample.

---

### R-03: Stratified URL sampling

**Purpose:** Pick 10–15 representative URLs from sitemap output, grouped by path prefix.

**Parsing (no bdata call — pure local processing of R-02 output):**
```bash
# Extract first path segment from each URL
sed -E 's|^https?://[^/]+/||' urls.txt | awk -F/ '{print $1}' | sort | uniq -c | sort -rn
```

Decision rule:
1. Always include homepage (`/`).
2. For each path-prefix group with ≥3 URLs, sample 1 URL (or 2 if it's the dominant group, e.g., `/products/` on e-commerce).
3. Cap total at 10 pages by default. Document budget override.
4. If `<budget>` is exceeded by the per-group caps, drop smallest groups first.

**Evidence to cite:** "Sampled 12 URLs from sitemap of 847; groups: `/blog/` (3), `/products/` (5), `/category/` (2), `/` (1), `/about` (1)".

---

### R-04: Parallel page fetch

**Purpose:** Fetch all sampled URLs as rendered HTML in parallel.

**Command (issue all `bdata scrape` calls in a SINGLE Bash message via multiple tool calls — Claude must use multiple Bash invocations in one assistant turn):**
```bash
bdata scrape https://example.com/ -f html -o /tmp/seo/_home.html
bdata scrape https://example.com/products/widget -f html -o /tmp/seo/_p1.html
# ...one call per sampled URL
```

**Parsing:** Each output is rendered HTML for downstream recipes (R-05..R-11).

**Evidence to cite:** The specific `bdata scrape <url>` command per finding.

---

### R-05: Title + meta description + canonical extraction

**Purpose:** Per-page on-page basics.

**Command:** Parses output of R-04 (no extra `bdata` call).

**Parsing:**
```bash
# Title
grep -oE '<title>[^<]+</title>' page.html | sed -E 's|</?title>||g'
# Meta description
grep -oE '<meta[^>]+name="description"[^>]*>' page.html | grep -oE 'content="[^"]*"' | sed -E 's|content="(.*)"|\1|'
# Canonical
grep -oE '<link[^>]+rel="canonical"[^>]*>' page.html | grep -oE 'href="[^"]*"' | sed -E 's|href="(.*)"|\1|'
```

**Evidence to cite:** Command + the extracted title/description/canonical line verbatim.

---

### R-06: Heading structure extraction

**Purpose:** Count H1s and detect heading-level skips.

**Parsing:**
```bash
# Count each heading level (use grep -oE | wc -l, not grep -c — minified HTML is one line)
for h in 1 2 3 4 5 6; do
  count=$(grep -oE "<h$h[ >]" page.html | wc -l)
  echo "h$h: $count"
done
# Extract H1 text
grep -oE '<h1[^>]*>[^<]+</h1>' page.html | sed -E 's|</?h1[^>]*>||g'
```

Flag: if H1 count ≠ 1, or if any H3 appears before any H2, etc.

**Evidence to cite:** Command + the per-level counts and H1 text.

---

### R-07: Schema markup detection (JS-injected)

**Purpose:** The recipe that distinguishes this skill. Detect JSON-LD schema, including markup injected by client-side JS (Yoast, RankMath, AIOSEO, Next.js, etc.).

**Why it works:** `bdata scrape -f html` runs the page through Bright Data's rendering layer, so JS-injected `<script type="application/ld+json">` blocks are present in the output — unlike `web_fetch` or `curl`.

**Command:** Parses output of R-04.

**Parsing:** JSON-LD blocks are multi-line and may contain literal `<` characters in string values, so a simple `grep` will silently fail on either count. Use Python for reliable extraction:
```bash
python3 - <<'PY' page.html
import re, sys, json
html = open(sys.argv[1]).read()
# Find every <script type="application/ld+json">...</script> block (DOTALL so newlines are matched)
blocks = re.findall(
    r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
    html, re.DOTALL | re.IGNORECASE,
)
for raw in blocks:
    try:
        obj = json.loads(raw.strip())
    except json.JSONDecodeError as e:
        print(f"[malformed JSON-LD] {e}")
        continue
    items = obj if isinstance(obj, list) else [obj]
    for item in items:
        t = item.get("@type", "(no @type)")
        print(t if isinstance(t, str) else ", ".join(t))
PY
```

Output is one `@type` per line (e.g., `Product`, `BreadcrumbList`, `Organization`). Empty output means no JSON-LD blocks were found in the rendered HTML.

**Evidence to cite:** Command + the extracted `@type` list (e.g., `["Product", "BreadcrumbList"]`).

---

### R-08: Hreflang cluster extraction

**Purpose:** Validate hreflang self-reference, reciprocity, and valid codes for multilingual sites.

**Parsing:**
```bash
# From each page's HTML head
grep -oE '<link[^>]+rel="alternate"[^>]+hreflang="[^"]*"[^>]*>' page.html
# Extract hreflang code + href into pairs:
# - hreflang="en-GB" href="https://example.com/en-gb/about"
```

Validation rules (apply across the cluster from all sampled pages):
- **Self-reference:** Each page must list itself with its own hreflang.
- **Reciprocity:** If page A points to page B with `hreflang=fr`, page B must point back to A.
- **Valid codes:** ISO 639-1 lang + optional ISO 3166-1 Alpha-2 region. `en-UK` is invalid (use `en-GB`).
- **x-default:** Must be declared somewhere in the cluster.

**Evidence to cite:** Command + the malformed entry, e.g., `<link rel="alternate" hreflang="en-UK" href="..."/>` plus the rule it violates.

---

### R-09: Image alt audit

**Purpose:** Count `<img>` without `alt`.

**Parsing:** (Use `grep -oE … | wc -l` instead of `grep -c` — `-c` counts matching lines, which underreports on minified single-line HTML.)
```bash
total=$(grep -oE '<img[^>]*>' page.html | wc -l)
with_alt=$(grep -oE '<img[^>]+alt=' page.html | wc -l)
without=$((total - with_alt))
echo "$without/$total images missing alt"
```

**Evidence to cite:** Command + the ratio (e.g., "14/22 images missing alt on /products/widget").

---

### R-10: Internal-link map

**Purpose:** Build internal-link graph across sampled pages; detect orphan candidates and generic anchor text.

**Parsing:**
```bash
# Extract hrefs
grep -oE '<a[^>]+href="[^"]*"' page.html | grep -oE 'href="[^"]*"' | sed -E 's|href="(.*)"|\1|'
# Extract anchor text per link
grep -oE '<a[^>]+href="[^"]*"[^>]*>[^<]+</a>' page.html
```

Internal vs. external classification: if hostname matches the audit domain (or no hostname, i.e., relative), it's internal.

Generic anchor list: `click here`, `read more`, `learn more`, `here`, `link`.

**Evidence to cite:** Command + count of internal links, generic-anchor count, and the orphan-candidate list (URLs in sitemap but not linked from any sampled page).

---

### R-11: CWV-proxy signals

**Purpose:** HTML-level proxies for Core Web Vitals — always paired with "confirm with PSI" caveat in the finding.

**Parsing:** (POSIX `grep -E` does not support negative lookahead, so render-blocking and CLS-risk are computed by subtraction. All counts use `grep -oE … | wc -l` instead of `grep -c` because `-c` counts matching *lines*, not occurrences — a single-line minified HTML file would otherwise return 1 for everything.)
```bash
# Page weight (rendered HTML bytes)
wc -c < page.html
# Render-blocking scripts (synchronous + in <head>) = (all script-with-src in <head>) − (those with async or defer)
head_html=$(sed -n '/<head>/,/<\/head>/p' page.html)
total_head_scripts=$(echo "$head_html" | grep -oE '<script[^>]+src=' | wc -l)
async_or_defer=$(echo "$head_html" | grep -oE '<script[^>]+(async|defer)[^>]*src=|<script[^>]+src=[^>]*(async|defer)' | wc -l)
echo $((total_head_scripts - async_or_defer))
# Render-blocking stylesheets (no media="print")
echo "$head_html" | grep -oE '<link[^>]+rel="stylesheet"[^>]*>' | grep -vE 'media="print"' | wc -l
# Images without dimensions (CLS risk) = total <img> − those with width or height
total_imgs=$(grep -oE '<img[^>]*>' page.html | wc -l)
imgs_with_dims=$(grep -oE '<img[^>]*(width|height)=[^>]*>' page.html | wc -l)
echo $((total_imgs - imgs_with_dims))
# Lazy-loading count
grep -oE '<img[^>]+loading="lazy"' page.html | wc -l
# Resource hints count
grep -oE '<link[^>]+rel="(preconnect|preload|dns-prefetch)"' page.html | wc -l
```

**Evidence to cite:** Command + the metric (e.g., "page weight 4.2MB", "8 sync scripts in <head>", "14 images without width/height").

**Always include in the finding:** "Confirm with PageSpeed Insights at https://pagespeed.web.dev/?url=<url> for field-data CWV measurements."

---

### R-12: Indexation proxy via SERP

**Purpose:** Approximate count of URLs Google has indexed.

**Command:**
```bash
bdata search "site:example.com" --json
```

**Parsing:**
```bash
bdata search "site:example.com" --json | jq '.organic | length'
# Or to get the actual URLs:
bdata search "site:example.com" --json | jq -r '.organic[].link'
```

**Note:** `site:` queries return a sample, not exact counts. Compare to sitemap URL count from R-02 — flag if "12 indexed vs. 847 in sitemap" levels of mismatch.

**Evidence to cite:** Command + the count and a few sample indexed URLs.

---

### R-13: Ranking position check (signal-driven)

**Purpose:** Find user's domain in SERP for a target keyword. Run only when the user named a keyword.

**Command:**
```bash
bdata search "project management software" --json
```

**Parsing:**
```bash
bdata search "project management software" --json \
  | jq '.organic | to_entries[] | select(.value.link | contains("example.com")) | {position: (.key + 1), url: .value.link}'
```

If no entry returned, the domain is not in the top results for that query.

**Evidence to cite:** Command + position (e.g., "ranks #18 with URL /pm-tool") or "not in top 100".

---

### R-14: Cannibalization check (signal-driven)

**Purpose:** Detect when multiple URLs from the user's domain rank for the same keyword.

**Command:**
```bash
bdata search "project management software site:example.com" --json
```

**Parsing:**
```bash
bdata search "project management software site:example.com" --json \
  | jq -r '.organic[].link'
```

If 2+ URLs returned, those are cannibalization candidates.

**Evidence to cite:** Command + the URL list.

---

### R-15: Site-type detection

**Purpose:** Classify the site so the right playbook from `site-type-playbooks.md` applies.

**Inputs:** Output of R-04 for the homepage + URL list from R-02.

**Heuristics:**
| Site type | Cue |
|---|---|
| SaaS / Product | `/pricing` URL exists; "free trial" / "sign up" CTAs in nav; description mentions software/platform/tool; JSON-LD `@type: SoftwareApplication` |
| E-commerce | `/products/` or `/shop/` paths in sitemap; `Schema.org Product` JSON-LD; "Add to cart" buttons; `?color=` / `?size=` parameter patterns |
| Content / Blog | `/blog/` or `/posts/` paths dominate sitemap; `<meta name="generator" content="WordPress">` (or similar); `<article>` with bylines |
| Local Business | NAP in homepage footer; `Schema.org LocalBusiness` JSON-LD; Google Maps embed; multiple `/locations/<city>` |
| Multilingual | 2+ `<link rel="alternate" hreflang>` in homepage `<head>`; locale prefixes in sitemap (`/en/`, `/de/`, `/ar/`); non-default `<html lang>` |

A site can match multiple types — apply all matching playbooks.

**Evidence to cite:** Per-type, the cue that matched (e.g., "SaaS — `/pricing` page exists, JSON-LD `@type: SoftwareApplication` detected on homepage").

---

### R-16: Faceted-nav parameter detection (e-commerce)

**Purpose:** Detect indexable faceted-navigation URLs that risk crawl-budget waste.

**Parsing:**
```bash
# From R-02 sitemap URL list
grep -E '\?' urls.txt | head -20
grep -cE '\?' urls.txt
```

If many `?` URLs exist in the sitemap, flag as crawl-budget risk.

**Evidence to cite:** Command + count + 3-5 example URLs.

---

### R-17: Out-of-stock product handling (e-commerce)

**Purpose:** Detect product pages that are out-of-stock but still return 200 with no canonical handling.

**Parsing (per sampled product page):**
```bash
# Look for "out of stock" / "sold out" / "unavailable" markers in rendered HTML
grep -iE 'out of stock|sold out|currently unavailable' page.html
# Check Schema.org availability (JS-rendered, parseable from R-04 output)
grep -oE '"availability"[^,}]*' page.html
# Check canonical destination — does it point to itself, or to a parent category?
grep -oE '<link[^>]+rel="canonical"[^>]+href="[^"]+"' page.html
```

Flag: page contains an out-of-stock signal, has `Schema.org availability: OutOfStock` (or no availability schema), and self-canonicals (rather than canonicaling to a parent category or returning 404). If we need to confirm the HTTP status code, use `curl -o /dev/null -s -w "%{http_code}\n" "<url>"` rather than relying on `bdata scrape -f json` shape — the JSON-mode output schema is not documented for this purpose.

**Evidence to cite:** Command + the out-of-stock text + missing canonical.

---

### R-18: Author-page reachability (blog)

**Purpose:** Find broken `/author/<x>` links from blog posts.

**Parsing:**
```bash
# From sampled blog posts
grep -oE '<a[^>]+href="[^"]*author[^"]*"' post.html | grep -oE 'href="[^"]*"' | sort -u
# For each unique author URL, fetch:
bdata scrape "https://example.com/author/jane" -f html
```

Flag if any author URL returns 404 or is blocked.

**Evidence to cite:** Command + the broken author URL.

---

### R-19: Publication-date / freshness extraction (blog)

**Purpose:** Identify outdated content (>18 months without update).

**Parsing:**
```bash
# datetime in <time> tags
grep -oE '<time[^>]+datetime="[^"]*"' post.html
# Modified date (Schema.org Article)
grep -oE '"dateModified"[^,}]*' post.html
# Updated date in visible text (fallback)
grep -iE 'last updated|updated on' post.html
```

Flag posts with `datetime` >18 months ago and no visible update marker.

**Evidence to cite:** Command + the date + how long ago it is.

---

### R-20: NAP extraction (local business)

**Purpose:** Extract Name / Address / Phone from each sampled page footer to detect inconsistency.

**Parsing:**
```bash
# Phone (US formats)
grep -oE '\(?\b[0-9]{3}\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}\b' page.html | sort -u
# Address (rough — match street suffixes)
grep -oE '[0-9]+ [A-Z][a-z]+( [A-Z][a-z]+)* (St|Ave|Blvd|Rd|Way|Dr|Ln)\b[^<]*' page.html | sort -u
# Or pull from LocalBusiness JSON-LD if present
echo "$JSONLD" | jq -r '.address, .telephone'
```

Normalize phone (digits only) before comparing across pages. Flag any mismatch as a finding.

**Evidence to cite:** Command + the conflicting values per page.

---

### R-21: Local SERP visibility (local business)

**Purpose:** Run `bdata search "<service> <city>"` per location to validate local SERP presence.

**Command:**
```bash
bdata search "plumber Austin" --country us --json
```

**Parsing:** Same as R-13 — find user's domain in `.organic[].link`.

**Evidence to cite:** Command + position per city.

---

### R-22: Outbound-link sampling (E-E-A-T)

**Purpose:** Quick check that content links to authoritative sources rather than orphan pages with no citations.

**Parsing:**
```bash
grep -oE '<a[^>]+href="https?://[^"]*"' page.html \
  | grep -oE 'href="[^"]*"' \
  | grep -vE 'href="https?://(www\.)?example\.com' \
  | sort -u | head
```

Flag: low outbound-link count + topic that demands citations (medical, legal, financial).

**Evidence to cite:** Command + outbound-link count.

---

### R-23: Author byline + bio detection (E-E-A-T)

**Purpose:** Detect missing or thin author bylines on blog content.

**Parsing:**
```bash
# Schema.org Article author
echo "$JSONLD" | jq -r '.author.name // .author[0].name'
# Visible byline (rough)
grep -iE 'by <[^>]+>|class="author"|rel="author"' post.html
```

Flag posts with no detectable author.

**Evidence to cite:** Command + presence/absence.

---

### R-24: Word-count + paragraph-count extraction

**Purpose:** Detect thin content.

**Parsing:**
```bash
# Strip HTML tags, count words
sed -E 's|<[^>]+>||g' page.html | wc -w
# Paragraph count
grep -oE '<p[ >]' page.html | wc -l
```

Thresholds (rough): <300 words is thin for a content page; <100 words is thin even for a product page.

**Evidence to cite:** Command + count.

---

### R-25: Generic anchor-text density

**Purpose:** Flag over-reliance on generic anchor text in internal links.

**Parsing:**
```bash
# From R-10 anchor extraction. Trim whitespace and use grep -oE | wc -l (not -c, to avoid line-counting on minified HTML).
generic=$(grep -oE '<a[^>]+href="[^"]*"[^>]*>[^<]+</a>' page.html \
  | sed -E 's|.*>([^<]+)</a>|\1|' \
  | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
  | grep -ciE '^(click here|read more|learn more|here|link|this|more)$')
total=$(grep -oE '<a[^>]+href="[^"]*"' page.html | wc -l)
echo "$generic/$total internal anchors are generic"
```

Flag if generic-anchor ratio is > 30% of internal-link count.

**Evidence to cite:** Command + ratio.


path: references/output-templates.md
content: # Output Templates — SEO Audit

This file defines the exact report structure every SEO audit must produce. The audit-framework.md and site-type-playbooks.md generate findings; this file specifies how to present them.

## Top-Level Report Structure

```markdown
# SEO Audit — <domain or URL>

## Executive Summary
- **Overall health**: <Critical | Poor | Fair | Good | Excellent>
- **Top priorities**:
  1. <Highest-impact issue, one sentence>
  2. <…>
  3. <…>
- **Quick wins**: <2–4 easy/immediate fixes>
- **Audit scope**: <N pages sampled from sitemap of M URLs | single-page audit of <url>>

## Technical SEO Findings
<one finding block per issue, ordered by Priority (1 first); if no findings, write "No issues found in this area.">

## On-Page SEO Findings
<one finding block per issue, ordered by Priority (1 first); if no findings, write "No issues found in this area.">

## Content Findings
<one finding block per issue, ordered by Priority (1 first); if no findings, write "No issues found in this area.">

## Out-of-Scope Notes
<things to check elsewhere — PSI, GSC, Ahrefs>

## Prioritized Action Plan
1. **Critical** (blocking indexation/ranking): …
2. **High-impact** improvements: …
3. **Quick wins** (easy, immediate benefit): …
4. **Long-term** recommendations: …
```

## Finding Block Shape

Every finding under `Technical SEO Findings`, `On-Page SEO Findings`, and `Content Findings` uses this exact shape:

````markdown
### <Short issue title>
- **Issue**: <What's wrong, 1–2 sentences>
- **Impact**: <High | Medium | Low> — <one sentence on why this matters for SEO>
- **Evidence**:
  - Command: `bdata scrape https://example.com -f html`
  - Output excerpt:
    ```
    <meta name="robots" content="noindex">
    ```
  - (or, for a SERP-based finding) Command: `bdata search "site:example.com" --json | jq '.organic | length'` → 12 (sitemap has 847 URLs)
  - (or, for a cannibalization finding) Command: `bdata search "<keyword> site:example.com" --json | jq -r '.organic[].link'` → returned 3 URLs
- **Fix**: <Specific, actionable recommendation>
- **Priority**: <1 (critical) | 2 | 3 | 4 | 5 (nice-to-have)>
````

`Impact` and `Priority` are orthogonal: Impact rates the SEO ranking effect (High/Medium/Low); Priority weights urgency factoring in fix effort. A finding can be Impact=High, Priority=2 (severe ranking effect, but moderate effort to fix) or Impact=Medium, Priority=1 (only moderate ranking effect, but trivially fixable and blocking other work).

This shape matches the inspiration skill's report contract verbatim — every finding is Issue / Impact / Evidence / Fix / Priority — so existing SEO-audit evals port without rewriting. The Evidence field is the one place we improve: every finding cites the exact `bdata` command that produced the evidence and an output excerpt the user can verify by re-running the command, which is uniquely possible because of the CLI's deterministic, scriptable output.

## Out-of-Scope Notes Section

Anything `bdata` cannot directly measure goes here, with explicit pointers:

- "Core Web Vitals field data → run PageSpeed Insights at https://pagespeed.web.dev/?url=<url>"
- "Index coverage detail → check Google Search Console > Pages report"
- "Backlink profile → use Ahrefs / Semrush / Moz"
- "JS-injected schema validation → already detected via `bdata scrape -f html`; cross-validate with Rich Results Test (https://search.google.com/test/rich-results)"

## Executive Summary Rubric

- **Top priorities**: 3–5 items, one sentence each, ordered by impact × ease-of-fix.
- **Quick wins**: findings rated Priority ≥ 3 (Medium severity or lower, since lower-severity items take less effort to ship) AND classified as Low effort.
- **Overall health**:
  - **Critical** — site uncrawlable or unindexed (Tier-1 failure).
  - **Poor** — multiple Tier-1 or Tier-2 issues.
  - **Fair** — Tier-3 issues dominate.
  - **Good** — only Tier-4 / quick-win issues remain.
  - **Excellent** — nothing material found.

## Mode-A vs. Mode-B Rendering

- **Mode A (single-page audit)**: same template, "Audit scope" line says "single-page audit of <url>"; Findings sections are typically shorter; site-type playbook may not apply.
- **Mode B (site-wide audit)**: full template; Findings include site-wide issues — cannibalization, duplicate titles across sample, hreflang clusters, internal-link orphans.

## Priority Numbering

| Priority | Meaning |
|---|---|
| 1 | Critical — blocks indexation or ranking. Fix immediately. |
| 2 | High — significant ranking impact within weeks. |
| 3 | Medium — meaningful improvement; schedule. |
| 4 | Low — minor polish. |
| 5 | Nice-to-have — may improve over time. |

### Mapping Priority levels to Action Plan buckets

When generating the Prioritized Action Plan section, group findings by Priority:

| Priority | Action Plan bucket |
|---|---|
| 1 | Critical (blocking indexation/ranking) |
| 2 | High-impact improvements |
| 3 | Quick wins (when also Low effort) — otherwise High-impact improvements |
| 4 | Long-term recommendations |
| 5 | Long-term recommendations |


path: references/site-type-playbooks.md
content: # Site-Type Playbooks — SEO Audit

Universal checks live in `audit-framework.md`. This file documents the *extra* checks per site type. Detection runs first via R-15.

A site can match multiple types (e.g., multilingual e-commerce → run both playbooks).

## Site-Type Detection Summary (from R-15)

| Type | Detection cue from `bdata scrape <homepage> -f html` |
|---|---|
| SaaS / Product | `/pricing` URL exists; "free trial" / "sign up" CTAs in nav; description mentions software/platform/tool; JSON-LD `@type: SoftwareApplication` |
| E-commerce | `/products/`, `/shop/`, `/cart/` paths in sitemap; `Schema.org Product` JSON-LD; "Add to cart"; `?color=`, `?size=` parameters |
| Content / Blog | `/blog/`, `/articles/`, `/posts/` dominate sitemap; `<meta name="generator" content="WordPress">`; `<article>` with bylines |
| Local Business | NAP in homepage footer; `Schema.org LocalBusiness` JSON-LD; Google Maps embed; `/locations/<city>` |
| Multilingual | 2+ `<link rel="alternate" hreflang>`; locale prefixes (`/en/`, `/de/`); non-default `<html lang>` |

## SaaS / Product Playbook

**Sampling:** stratified pick MUST include `/pricing` (if present) and 1–2 feature pages.

**Extra checks:**
- Product page content depth — at least 500 words on the main product page (R-24).
- Feature pages aren't thin — flag any below 300 words.
- Comparison/alternative pages exist — search sitemap for `/vs/` or `/alternatives/` paths.
- Glossary/educational content present.
- Blog interlinked with product pages — check via R-10 internal-link map.

**Common-issue templates:**
- "Feature page `<url>` is thin (<300 words) — see R-24."
- "No `/vs/<competitor>` or `/alternatives` pages found — common SaaS SEO gap."
- "Blog posts and product pages not interlinked — content marketing isn't supporting product pages."

## E-commerce Playbook

**Sampling:** stratified pick MUST include at least one category page and one product page. If sitemap is a sitemap-index, recurse via R-02.

**Extra checks:**
- Faceted-navigation parameters in URLs — R-16. Warn if many `?` URLs in sitemap (crawl-budget risk).
- Product schema present — R-07. Now directly detectable since we render JS.
- Thin category pages — R-24. Category page <100 words is a flag.
- Duplicate product descriptions across sample — pull description from each sampled product page (R-05) and diff.
- Out-of-stock handling — R-17.

**Common-issue templates:**
- "Faceted nav creating <N> indexable parameter URLs — robots.txt or `noindex` candidates."
- "Category page `<url>` has only <N> words — needs intro/buyer-guide content."
- "Product pages share identical description (3 of 3 sampled)."
- "Out-of-stock product `/products/<x>` returns 200 with no canonical to category."

## Content / Blog Playbook

**Sampling:** stratified pick MUST include at least 3 blog posts.

**Extra checks:**
- Outdated content — R-19. Flag posts >18 months old without update markers.
- Keyword cannibalization — R-14, only if user provided a target keyword.
- Topical clustering — internal links between related posts (R-10).
- Author pages reachable — R-18.
- Author byline present per post — R-23.
- Post-to-product internal linking for content-marketing sites.

**Common-issue templates:**
- "<N> posts last updated >2 years ago — refresh candidates."
- "Posts `/blog/a` and `/blog/b` both rank for keyword 'X' — cannibalization."
- "Author 'jane' bylined on <N> posts but `/author/jane` returns 404."
- "Post `<url>` has no detectable author byline — E-E-A-T signal missing."

## Local Business Playbook

**Sampling:** stratified pick MUST include each `/locations/*` page found in sitemap.

**Extra checks:**
- NAP consistency across all sampled pages — R-20. Compare normalized phone/address/business-name. Flag any mismatch.
- `LocalBusiness` schema on homepage — R-07. Now directly detectable.
- Location pages exist for each location mentioned — search sitemap for `/locations/` paths.
- Local SERP visibility — R-21 per location.

**Common-issue templates:**
- "Phone on `/about` (555-1234) differs from footer (555-1235) — NAP inconsistency."
- "No `LocalBusiness` schema on homepage."
- "Locations page lists 3 locations but only 2 have dedicated pages (`/locations/austin`, `/locations/dallas`)."
- "Search for 'plumber Austin' returns no result for example.com in top 100."

## Multilingual Playbook

**Sampling:** stratified pick MUST include each locale's homepage; ALSO sample at least one non-homepage page per locale where the budget allows.

**Extra checks (folds in inspiration's international-seo.md):**
- Hreflang self-reference — R-08. Each page must include itself in the cluster.
- Hreflang reciprocity — if A points to B, B must point back to A.
- Valid ISO codes — `en-UK` is invalid (use `en-GB`).
- `x-default` declared somewhere in cluster.
- All hreflang targets return 200 — sample-check via `bdata scrape`.
- Canonical URL appears in hreflang set — otherwise the cluster is silently dropped.
- No cross-locale canonical (e.g., `/fr/x` canonicaling to `/en/x` suppresses French version).
- Locale prefix consistency — `/en/`, `/de/`, etc., consistent across canonical, hreflang, sitemap.

**Common-issue templates:**
- "Hreflang missing self-reference on `/en/about` — entire cluster ignored by Google."
- "Page `/fr/contact` canonicals to `/en/contact` — suppresses French version."
- "Invalid hreflang code `en-UK` on `/en-gb/*` — use `en-GB`."
- "x-default not declared anywhere in the hreflang cluster."

## Default Playbook (unknown site type)

If R-15 detection is ambiguous or returns no match:
- Run only the universal checks from `audit-framework.md` (Tiers 1–4 baseline).
- Don't fabricate type-specific findings.
- Note the ambiguity in the audit's Executive Summary so the user knows the playbook decision.

## Detection-Time Guardrail

If site-type detection is ambiguous (e.g., a CMS that also sells products), explicitly state both playbooks were applied and let the report show findings from both. Never silently pick one. The Executive Summary's "Audit scope" line should mention which playbooks ran.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)