# SEO Audit

> End-to-end SEO + GEO (AI-search) improvement workflow for any website. Audits the codebase, plans per-page JSON-LD, generates metadata + llms.txt + sitemap + robots, and verifies with real crawl simulation. Use when the user says "audit seo", "improve seo", "geo audit", "ai search", "llms.txt", "per-page json-ld", "structured data", "is my site indexable", "why isn't this ranking in chatgpt/perplexity", "lighthouse", "core web vitals", or before launching a marketing/content surface. Use when this capability is needed.

- Skill: `tomevault-io/seo-audit-5` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/seo-audit-5`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/seo-audit-5/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/seo-audit-5

---


# SEO + GEO Improvement Skill

End-to-end workflow for making any website discoverable by both traditional search (Google, Bing) and AI search (ChatGPT, Perplexity, Claude, Google AI Overviews). Covers audit → plan → fix → verify in one skill.

Use this when the goal is **real traffic and AI citations**, not just a green Lighthouse score.

## Modes

Pick one before starting. Ask the user if unclear.

- **Quick mode** (≈10 min) — run Phase 1 only, single-threaded. Triage report with top 5 ship-blockers. Use when the user says "quick check", "is my site broken", "anything critical".
- **Deep mode** (≈25–40 min wall-clock with parallelism) — run all phases, **dispatching 4 parallel subagents via the Agent tool** for the heavy phases (see Orchestration). Use when the user says "improve seo", "full audit", "help me rank", "per-page json-ld", "end to end", or describes a traffic goal.
- **Team mode** — same as deep + live SERP competitor reverse-engineering subagent. Use when the user specifies target keywords or says "beat my competitor".

## Orchestration (Deep Mode + Team Mode)

In deep mode, **do not walk the phases sequentially.** After Preflight, dispatch parallel subagents via the `Agent` tool in a single message with multiple tool calls. The main thread stays in the orchestrator role: collect results, synthesize, produce the final report.

### Step 1 — Preflight on the main thread
Detect stack, classify surface type, confirm traffic goal. **Do not skip.** These feed every subagent prompt.

### Step 2 — Dispatch parallel subagents (ONE message, 4 Agent tool calls)

Use `subagent_type: "general-purpose"` for all. Every subagent prompt must include:
- Target directory (absolute path)
- Detected framework + rendering mode
- Surface type (marketing / auth app / hybrid)
- Traffic goal (Google / GEO / both / GEO-first)
- Scope (below)
- Required output format
- Word cap

#### Subagent A — Schema Coverage Mapper

```
You are mapping JSON-LD structured data coverage for an SEO improvement workflow.

Target: {{absolute path}}
Framework: {{framework}} ({{rendering mode}})
Surface type: {{type}}
Goal: {{traffic goal}}

Your job: per-route JSON-LD coverage matrix.

Steps:
1. Enumerate public routes. Next.js App Router: `fd -e tsx "page\.tsx$" src/app app`. Pages Router: `pages/`. Other: adapt.
2. Classify each route: Home, About, Service, Blog index, Blog post, Product, Tool/SoftwareApp, Case study, Contact, FAQ, Pricing, Landing, Legal, Other.
3. For each route, grep file + layout ancestors for `application/ld+json` / `<JsonLd` / `schema.org`. Record what's injected.
4. Grep for central schema utilities (e.g., `src/seo/schema.ts`) — list defined-but-unused exports (stale code).

Output (markdown, under 600 words):

## Route coverage matrix
| Route | Archetype | Required schemas | Currently injected | Gap |

## Stale schema utilities
- `file::symbol` — defined but never imported

## Top 5 schema priorities (ordered by traffic goal)
1. {{route}} — add {{schema}} — `file:line to inject`

Do NOT write schema code. Plan only.
```

#### Subagent B — Technical Sweep

```
You are auditing technical SEO plumbing: metadata, sitemap, robots, OG, CWV, crawler access.

Target: {{absolute path}}
Framework: {{framework}} ({{rendering mode}})
Surface type: {{type}}
Goal: {{traffic goal}}

Commands to run (adapt per framework):
- Metadata coverage: `rg -l "export const metadata|generateMetadata|<Head>|useSeoTags" src/app app pages 2>/dev/null`
- Inherited-title anti-pattern: check if ONLY the root layout has metadata
- Sitemap: `fd "sitemap\.(ts|xml)$" . -E node_modules` — verify env-driven URLs not hardcoded localhost
- Robots: `cat public/robots.txt 2>/dev/null; fd "robots\.ts$" src/app app` — AI crawler rules? auth route disallows?
- OG images: `fd "opengraph-image" src/app app` — per-route or only root?
- CWV anti-patterns:
  - `rg -n "<img " --type tsx | rg -v "width=|height="` (CLS)
  - `rg -n "unoptimized.*true" next.config.*` (AVIF/WebP killer)
  - `rg -n "next/script" --type tsx | rg -v "lazyOnload|afterInteractive"` (INP)
- noindex in prod: `rg -n "noindex|robots.*index.*false" src/ app/`
- Stale SEO deps: `rg "react-helmet|next-seo" package.json`
- llms.txt: `cat public/llms.txt 2>/dev/null | head -30`

Output (under 700 words, every finding has `file:line`):

## Technical findings

### 🐛 Bugs (ship-blockers)
- {{issue}} — `file:line` — {{fix}}

### ❌ Missing (high impact)

### ⚠️ Partial (quick wins)

### ✅ Strengths

## Top 5 technical priorities (ordered by traffic goal)
1. ...
```

#### Subagent C — Content Shape + GEO Readiness

```
You are auditing content structure for GEO — making the site citable by ChatGPT, Claude, Perplexity, Google AI Overviews.

Target: {{absolute path}}
Framework: {{framework}}
Surface type: {{type}}
Goal: {{traffic goal}}

Your job: audit the top 5 public content pages (home, top services, top blog posts, top tools). NOT JSON-LD (another agent handles that). Content shape + entity signals only.

For each page, open component + any MDX/content source and check:
- H2-as-questions: are section headings framed as questions AI tools would be asked?
- Lead sentence test: short declarative sentence per section that stands alone when extracted?
- Lists & tables for comparable data (AI cites these more than prose)
- Entity names in plain text (not only in logos/images)
- Semantic landmarks: `<article>`, `<section>`, `<nav>`, `<main>`, `<time datetime="...">`, `<figure>`/`<figcaption>`
- Visible dates: `dateModified` shown to users, not only schema
- Author byline linked to dedicated author page
- Internal citations to authoritative primary sources

Also check:
- `public/llms.txt` — exists? up to date? covers top pages?
- Entity signals on home/about: Person/Organization with rich `sameAs` (LinkedIn, GitHub, Wikipedia, Scholar)?

Output (under 700 words):

## Content shape findings (per page)
### /{{route}}
- H2-as-questions: ✅/⚠️/❌ — {{notes}}
- Lead sentences: ...
- ...

## llms.txt status

## Entity signals status

## Top 5 content priorities (ordered by traffic goal)
```

#### Subagent D — Crawler Simulation (requires live URL)

Only dispatch if user provides a deployed URL. If pre-launch, skip and note it.

```
You are running live crawler simulation against a deployed site to verify what AI search engines actually see. Catches CSR SPAs invisible to non-JS crawlers.

Target URL: {{url}}
Surface type: {{type}}

Run:

URL="{{url}}"
for UA in \
  "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
  "GPTBot/1.2 (+https://openai.com/gptbot)" \
  "OAI-SearchBot/1.0 (+https://openai.com/searchbot)" \
  "ChatGPT-User/1.0 (+https://openai.com/bot)" \
  "ClaudeBot/1.0 (+claudebot@anthropic.com)" \
  "PerplexityBot/1.0 (+https://perplexity.ai/perplexitybot)" \
  "Google-Extended" \
  "Applebot-Extended" \
  "CCBot/2.0"; do
  echo "=== $UA ==="
  RESP=$(curl -sL -A "$UA" -w "\nSTATUS:%{http_code}" "$URL")
  echo "$RESP" | tail -1
  echo "  h1:      $(echo "$RESP" | grep -c '<h1')"
  echo "  h2:      $(echo "$RESP" | grep -c '<h2')"
  echo "  JSON-LD: $(echo "$RESP" | grep -c 'application/ld\+json')"
  echo "  og:title: $(echo "$RESP" | grep -c 'og:title')"
  echo "  title:   $(echo "$RESP" | grep -oE '<title[^>]*>[^<]*' | head -1)"
  echo "  bytes:   $(echo "$RESP" | wc -c)"
done

curl -sL "$URL/robots.txt" | head -30
curl -sI "$URL/sitemap.xml"
curl -sI "$URL/llms.txt"

Output (under 500 words):

## Per-crawler visibility matrix
| UA | Status | h1 | h2 | JSON-LD | og:title | Bytes | Title |

## Verdict
One sentence per crawler class.

## 🐛 Bugs
Any crawler seeing 0 content / 0 json-ld / 0 h1 is a bug.
```

#### Subagent E — SERP Competitor Reverse-Engineer (Team mode only)

Only dispatch if user provides target keywords. Uses WebFetch.

```
You are reverse-engineering the top 10 Google SERP results for target keyword(s), extracting patterns the user's site can adopt.

Target keyword(s): {{keywords}}
User's site: {{url}}

Steps:
1. WebFetch `https://www.google.com/search?q={{url-encoded keyword}}` — if Google blocks, ask user for a raw SERP dump
2. Extract top 10 organic (skip ads, knowledge panels unless relevant)
3. For each competitor URL, WebFetch and extract:
   - Title tag length + pattern
   - Meta description
   - H1 text
   - H2 count + framing (question vs statement)
   - Rough word count
   - Schema types (grep `@type":` in HTML)
   - Internal/external link counts
   - Image count + alt quality
4. Identify patterns: common schema, heading framings, length norms, SERP feature targets.

Output (under 700 words):

## SERP top 10 — {{keyword}}
| Rank | Domain | Title | Words | H2s | Schemas |

## Patterns to adopt
- Most ranking pages use {{pattern}}
- Schema cluster: top 5 all inject {{types}}
- Average content length: {{n}} words (user's: {{m}})
- Common H2 framings: {{examples}}

## What the winners do that the user doesn't
1. ...
```

### Step 3 — Synthesize on the main thread

Collect all subagent outputs. **Do not forward raw to the user.** Produce a unified report using the Output Format below, with findings re-ordered by the traffic goal chosen in Preflight. Rule 10 is load-bearing.

### Step 4 — Ask before touching code

After the report, ask which findings to implement. No silent edits. Architectural findings (SPA → SSR) get 2–3 options with tradeoffs. Rule 9.

### Step 5 — Handoff to `seo-measure`

Tell the user: "To compound this into 20-year-expert-level knowledge for YOUR specific site, run `seo-measure` weekly. Audit is a snapshot; measure is the learning system." Then optionally invoke `seo-measure` to capture the baseline.

## Preflight — Always Run First

Before any phase, establish three things. Do not skip.

### 1. Stack detection
Read `package.json`, then whichever config file exists: `next.config.*`, `vite.config.*`, `astro.config.*`, `svelte.config.*`, `nuxt.config.*`, `gatsby-config.*`, `remix.config.*`.

Record: framework, major version, rendering mode (SSR / SSG / ISR / CSR / hybrid), styling (affects CLS checks), i18n library if any.

**Rendering mode determines what's possible.** A CSR-only SPA cannot be made AI-discoverable by a checklist — it needs a framework discussion. Surface this as Finding #1 if detected, not buried in a category.

### 2. Surface-type classification
Ask if unclear. Three archetypes:

- **Marketing site** — public-facing, traffic is the goal. Full treatment.
- **Authenticated app** — login-gated. Goal is *blocking* crawlers from auth/dashboard/onboarding, not courting them. Skip OG/sitemap work except for public routes (landing, pricing, invite-accept).
- **Hybrid** — marketing pages + app routes in same codebase. Audit the two surfaces separately. Most Next.js SaaS products are this shape.

### 3. Traffic goal
Ask the user which outcome they're optimizing for. Shapes prioritization:

- (i) **Google organic** — classic SEO. Prioritize indexability, Core Web Vitals, keyword coverage, schema eligible for rich results.
- (ii) **AI citations** — GEO. Prioritize server-rendered HTML, entity markup, semantic structure, llms.txt, content shape (H2-as-questions, short lead sentences, lists/tables).
- (iii) **Both equally** — default.
- (iv) **Both, GEO-first** — for sites already strong on Google but invisible to AI.

---

## Phase 1 — Audit (always run)

Work through all 10 categories. For every item: **✅ / ⚠️ / ❌ / 🐛** with `file:line` evidence. Never mark present without proof.

Before starting, run these commands to get real data:

```bash
# Inventory every route in the codebase (Next.js App Router example)
# Prefer `find` over `fd` — `fd` is not always installed; Glob can silently
# return empty on some repos, so fall back to find.
find src/app app -name "page.tsx" -o -name "page.ts" 2>/dev/null

# Find all metadata exports
rg -n "export const metadata|generateMetadata|<Head>|useSeoTags|react-helmet" --type ts --type tsx

# Find all JSON-LD injections
rg -n "application/ld\+json|<JsonLd|jsonLd|schema\.org" --type ts --type tsx

# CRITICAL ANTI-PATTERN CHECK — JSON-LD rendered as <meta> instead of <script>.
# In Next.js App Router, putting JSON-LD in metadata.other serializes it as
# <meta name="application/ld+json" content="...">, NOT as a script tag.
# Google and all AI crawlers ignore it. The #1 real-world Next.js SEO bug.
rg -nC2 '"application/ld\+json"' --type ts --type tsx | rg -B2 -A2 "other:|other ="

# Find the public SEO files — sitemap may be at ANY of these paths
ls public/robots.txt public/sitemap.xml public/llms.txt public/manifest.json 2>/dev/null
find src/app app -name "robots.ts" -o -name "robots.tsx" 2>/dev/null
find src/app app -name "sitemap.ts" -o -name "sitemap.tsx" 2>/dev/null
find src/app app -path "*sitemap.xml/route.ts" 2>/dev/null  # folder-as-route pattern

# Grep for noindex (must not appear in prod build)
rg -n "noindex|robots.*index.*false|X-Robots-Tag" --type ts --type tsx

# CWV anti-patterns
rg -n "<img " --type tsx | rg -v "width=|height="
rg -n "unoptimized.*true" --type ts
rg -n "dangerouslyAllowSVG.*true" --type ts   # supply-chain risk

# Third-party <Script> without strategy="lazyOnload" — afterInteractive
# (the Next.js default) still blocks INP on weak devices.
rg -nC1 '<Script' --type tsx | rg -v 'lazyOnload'

# Fake AggregateRating — Google spam-policy landmine
rg -n '"AggregateRating"|aggregateRating' --type ts --type tsx
# For each hit, verify the rating is backed by real visible review data.
```

### Categories

1. **Framework fit & rendering** — SSR/SSG for public routes? Prerender for SPAs? No global static export on web?
2. **Per-route metadata** — unique title (50–60 chars) + description (140–160 chars) per public route? Canonical per route? `<html lang="...">` set? No inherited-root-title anti-pattern?
3. **robots.txt + sitemap.xml** — exist, env-driven URLs (no hardcoded localhost), auth routes disallowed, AI crawler rules explicit, sitemap referenced from robots.
4. **Structured data (JSON-LD)** — see Phase 2 for the coverage matrix. Here just note: does *any* JSON-LD exist? Is it in `<head>`? Are required fields populated?
5. **Open Graph + Twitter** — minimum set present, per-route OG images, 1200×630 PNG/JPG (not AVIF), absolute URLs.
6. **Core Web Vitals** — LCP causes (images without priority, fonts without preload, render-blocking JS), INP causes (heavy handlers, third-party scripts without lazyOnload), CLS causes (images without width/height, fonts without size-adjust).
7. **GEO readiness** — `llms.txt`? AI crawler rules in robots.txt? Server-rendered HTML? Semantic HTML landmarks? Entity markup on author/org?
8. **Accessibility-as-SEO** — one h1 per page, heading order, alt text, descriptive link text, form labels, landmarks.
9. **Indexability bugs** — any `noindex` in prod output? Blocked `/_next/static/`? Redirect chains? Soft 404s? Infinite URL spaces?
10. **Monitoring baseline** — GSC verified? Bing Webmaster? Lighthouse CI? Vercel Speed Insights / RUM?

### Stale-code cleanup (cross-cut, always check)
- JSON-LD utility defined in code but never imported by any page? Flag as stale.
- `react-helmet` + framework-native metadata both present? Pick one.
- Old `next-seo` usage alongside App Router `metadata`? Migrate.
- `useEffect`-injected meta tags? Invisible to non-JS crawlers — flag.
- **Before recommending removal of any dependency**, grep `src/` to confirm it's actually unused. A stale `package.json` entry and a live import look the same from `package.json` alone — removing a live dep breaks the build.

---

## Phase 2 — Per-Page JSON-LD Planning

The single highest-leverage GEO move. Most sites have zero JSON-LD or a sitewide `Organization` and nothing else. Per-page schema is what drives rich results *and* AI citations.

### Step 1: Build the coverage matrix

Map every public route to the schema type(s) it should have. Skip auth/app routes.

| Route | Archetype | Required schemas | Current state |
|-------|-----------|------------------|---------------|
| `/` | Home | `WebSite` + `SearchAction`, `Organization` OR `Person` | ❌ missing |
| `/about` | About | `AboutPage`, `Person` (with `sameAs`) | ❌ |
| `/services` | Services index | `ItemList` of `Service`, optional `FAQPage` | ❌ |
| `/services/[slug]` | Service | `Service`, `BreadcrumbList`, `FAQPage` if applicable | ❌ |
| `/blog` | Blog index | `Blog`, `BreadcrumbList` | ❌ |
| `/blog/[slug]` | Blog post | `BlogPosting`, `BreadcrumbList`, `Person` (author) | ❌ |
| `/blog/[slug]` (tutorial) | Blog post + how-to | `BlogPosting` + `HowTo`, `BreadcrumbList` | ❌ |
| `/tools` | Tools index | `ItemList` of `SoftwareApplication` | ❌ |
| `/tools/[slug]` | Tool/product | `SoftwareApplication` OR `Product`, `BreadcrumbList`, `AggregateRating` *only if reviews are real and visible* | ❌ |
| `/projects/[slug]` | Case study | `CreativeWork` OR `Article`, `BreadcrumbList` | ❌ |
| `/testimonials` | Testimonials | `CollectionPage` + `Review` array | ❌ |
| `/team` | Team | `CollectionPage` of `Person` | ❌ |
| `/contact` | Contact | `ContactPage`, optional `Organization` with `contactPoint` | ❌ |
| `/faq` | FAQ | `FAQPage`, `BreadcrumbList` | ❌ |
| `/pricing` | Pricing | `Product` + `Offer` array (for rich results), OR `Service` + `PriceSpecification` | ❌ |

Present this matrix to the user before writing any schemas. Get confirmation on priorities.

**Common mistakes filling this matrix:**
- Forgetting `BreadcrumbList` on every non-home route — required for rich-results eligibility.
- Marking a route ✅ based on grep match alone without verifying the schema is injected as a `<script>` tag (see the `metadata.other` anti-pattern).
- Recommending `AggregateRating` without real review data.
- Missing `HowTo` opportunity on tutorial-style blog posts.

### Step 2: Templates (copy-paste, fill in, inject)

All templates below go in `<script type="application/ld+json">` in `<head>` (or via framework-native method). Use absolute URLs. Validate at [Google Rich Results Test](https://search.google.com/test/rich-results) before shipping.

**`WebSite` + `SearchAction` (home page only):**
```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "{{site name}}",
  "url": "{{site url}}",
  "potentialAction": {
    "@type": "SearchAction",
    "target": { "@type": "EntryPoint", "urlTemplate": "{{site url}}/search?q={search_term_string}" },
    "query-input": "required name=search_term_string"
  }
}
```

**`Person` (home / about / blog author — the entity signal that matters most for GEO):**
```json
{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "{{full name}}",
  "url": "{{canonical profile url}}",
  "image": "{{absolute headshot url}}",
  "jobTitle": "{{role}}",
  "worksFor": { "@type": "Organization", "name": "{{org}}", "url": "{{org url}}" },
  "sameAs": [
    "https://linkedin.com/in/{{handle}}",
    "https://github.com/{{handle}}",
    "https://x.com/{{handle}}",
    "https://scholar.google.com/citations?user={{id}}"
  ],
  "knowsAbout": ["{{topic1}}", "{{topic2}}", "{{topic3}}"],
  "alumniOf": { "@type": "Organization", "name": "{{school}}" }
}
```
`sameAs` is the critical field — it lets AI search engines resolve "who is this person" across the web. Include LinkedIn, GitHub, X, Wikipedia/Wikidata if it exists, Google Scholar, personal site.

**`Organization` (home page, if not a personal site):**
```json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "{{org name}}",
  "url": "{{site url}}",
  "logo": "{{absolute logo url, min 112x112}}",
  "sameAs": ["{{linkedin}}", "{{x}}", "{{github}}", "{{youtube}}"],
  "contactPoint": {
    "@type": "ContactPoint",
    "email": "{{contact email}}",
    "contactType": "customer support"
  }
}
```

**`BlogPosting` (every blog post):**
```json
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "{{post title, ≤110 chars}}",
  "description": "{{post excerpt}}",
  "image": ["{{absolute hero image url, 1200x630}}"],
  "datePublished": "{{ISO 8601}}",
  "dateModified": "{{ISO 8601}}",
  "author": {
    "@type": "Person",
    "name": "{{author name}}",
    "url": "{{author page url}}"
  },
  "publisher": {
    "@type": "Organization",
    "name": "{{site name}}",
    "logo": { "@type": "ImageObject", "url": "{{absolute logo url}}" }
  },
  "mainEntityOfPage": { "@type": "WebPage", "@id": "{{absolute post url}}" },
  "keywords": ["{{kw1}}", "{{kw2}}"],
  "articleSection": "{{category}}",
  "wordCount": {{int}}
}
```
**Required-field gotchas:** `image` must be an array, `author` must be an object not a string, `datePublished`/`dateModified` must be ISO 8601, `headline` must not exceed 110 chars or Google truncates it.

**`Service` (consulting / agency service pages):**
```json
{
  "@context": "https://schema.org",
  "@type": "Service",
  "name": "{{service name}}",
  "description": "{{short desc}}",
  "provider": { "@type": "Person or Organization", "name": "{{name}}", "url": "{{url}}" },
  "serviceType": "{{category}}",
  "areaServed": "{{Worldwide or country/city}}",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "USD",
    "price": "{{amount}}",
    "availability": "https://schema.org/InStock"
  },
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "{{catalog name}}",
    "itemListElement": [
      { "@type": "Offer", "itemOffered": { "@type": "Service", "name": "{{sub-service}}" } }
    ]
  }
}
```

**`SoftwareApplication` (tools, SaaS, calculators):**
```json
{
  "@context": "https://schema.org",
  "@type": "SoftwareApplication",
  "name": "{{app name}}",
  "description": "{{short desc}}",
  "applicationCategory": "{{BusinessApplication | DeveloperApplication | etc}}",
  "operatingSystem": "Web",
  "url": "{{app url}}",
  "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "{{4.8}}",
    "reviewCount": "{{int}}"
  }
}
```
Only include `aggregateRating` if the ratings are real and visible on the page. Google penalizes fake review markup.

**`FAQPage` (any page with a real FAQ section visible to users):**
```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "{{question text, matches h2/h3 on page}}",
      "acceptedAnswer": { "@type": "Answer", "text": "{{answer text}}" }
    }
  ]
}
```
**Critical:** the questions/answers in JSON-LD must exactly match what's rendered on the page. Google penalizes mismatches.

**`BreadcrumbList` (every non-home page):**
```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "{{site url}}" },
    { "@type": "ListItem", "position": 2, "name": "{{section}}", "item": "{{section url}}" },
    { "@type": "ListItem", "position": 3, "name": "{{page title}}", "item": "{{page url}}" }
  ]
}
```

### Step 3: Injection strategy per framework

- **Next.js App Router** → helper function in `src/seo/schema.ts` that returns the JSON object, then `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />` inside the page or layout component. Do NOT use `JSON.stringify` inside `generateMetadata` — metadata API doesn't handle script tags.
- **Next.js Pages Router** → same, inside `<Head>` from `next/head`.
- **Vite/React SPA** → React 19 native `<script>` in component, or `react-helmet-async` for React 18. If SSR isn't enabled, the JSON-LD will only reach JS-executing crawlers.
- **Astro** → slot into `<Fragment slot="head">`.
- **SvelteKit** → `<svelte:head>` in `+page.svelte`.

### Step 4: Validate

For each schema added:
```bash
# Local validation — paste URL into the form
open "https://search.google.com/test/rich-results?url={{url}}"
open "https://validator.schema.org/?url={{url}}"
```

Run the Rich Results test on at least one page per archetype before merging.

---

## Phase 3 — GEO Structural Improvements

GEO is mostly **content shape + entity signals + server-rendered HTML**. JSON-LD is necessary but not sufficient.

### 3a. Crawler access (robots.txt)

Every public site should have explicit rules for AI crawlers. Default template:

```
# robots.txt
User-agent: *
Allow: /

# Traditional search
Sitemap: https://{{site}}/sitemap.xml

# AI search engines — ALLOW (change to Disallow if you want out of AI answers)
User-agent: GPTBot           # OpenAI training
Allow: /

User-agent: OAI-SearchBot    # ChatGPT Search indexer
Allow: /

User-agent: ChatGPT-User     # User-triggered browse in ChatGPT
Allow: /

User-agent: ClaudeBot        # Anthropic crawler
Allow: /

User-agent: anthropic-ai     # Legacy Anthropic UA
Allow: /

User-agent: PerplexityBot    # Perplexity indexer
Allow: /

User-agent: Perplexity-User  # User-triggered Perplexity fetch
Allow: /

User-agent: Google-Extended  # Gemini training (separate from Googlebot)
Allow: /

User-agent: Applebot-Extended # Apple Intelligence training
Allow: /

User-agent: CCBot            # Common Crawl (feeds many AI training sets)
Allow: /

# Block if authenticated app routes exist
Disallow: /dashboard/
Disallow: /auth/
Disallow: /onboarding/
Disallow: /api/
Disallow: /admin/
```

**Key distinctions (cite to the user if they push back):**
- Blocking `GPTBot` removes your content from OpenAI *training*, not from ChatGPT *search*. Search uses `OAI-SearchBot` + `ChatGPT-User`.
- `Google-Extended` controls Gemini; `Googlebot` controls Google Search indexing. They're separate — you can opt out of AI training while staying in search.
- `Applebot` = Siri/Spotlight/Safari search. `Applebot-Extended` = Apple Intelligence training. Again separate.

### 3b. llms.txt (low-cost hygiene, not a ranking signal)

Place at `public/llms.txt`. Follows [llmstxt.org](https://llmstxt.org) convention. Template:

```markdown
# {{Site Name}}

> {{One-sentence site description — this is what AI tools see first.}}

{{Longer paragraph: who you are, what the site is for, key expertise areas.}}

## Key pages

- [Home]({{site url}}): {{one-line summary}}
- [About]({{site url}}/about): {{one-line summary}}
- [Services]({{site url}}/services): {{one-line summary}}
- [Blog]({{site url}}/blog): {{one-line summary}}

## Services

- [{{service}}]({{url}}): {{one-line}}

## Recent articles

- [{{post title}}]({{url}}): {{one-line}}

## Contact

- Email: {{email}}
- LinkedIn: {{url}}
- GitHub: {{url}}
```

**Honest framing for the user:** no primary source (OpenAI, Anthropic, Google, Perplexity) confirms llms.txt is a ranking signal. It's a discoverability convenience. Install it because it's cheap, not because it'll move the needle alone.

### 3c. Content shape (the real GEO lever)

AI search extracts and cites. What gets extracted:

- **H2s as questions.** "How does X work?" outperforms "Overview of X". LLMs are trained on Q&A data.
- **Short declarative lead sentence per section.** First sentence is extracted disproportionately. Make it stand alone.
- **Lists and tables for comparable data.** Cited more often than prose. Use for pricing tiers, feature matrices, step-by-step.
- **Explicit entity names in plain text.** Don't put product names only in logos/images — LLMs can't read them.
- **Semantic HTML landmarks.** `<article>`, `<section>`, `<nav>`, `<main>`, `<time datetime="...">`, `<figure>` + `<figcaption>`. AI extractors use these.
- **Citations to primary sources.** LLMs prefer citing content that itself cites. Link out to authoritative sources in body copy.
- **Dates visible in content.** Recency matters — AI answers prefer fresh content. Show `dateModified` as a visible line, not only in schema.
- **Author byline with link.** Entity resolution depends on the author having their own URL + schema.

When auditing content pages, flag each of these as ✅/❌ per page.

### 3d. Entity signals

- `Person` or `Organization` JSON-LD on home page with rich `sameAs` (see Phase 2 templates).
- External presence: Wikipedia/Wikidata entry if possible (biggest entity-resolution boost), LinkedIn Company or Personal profile, GitHub org, Crunchbase for companies.
- Internal linking to a canonical "about" page from every article byline.

---

## Phase 4 — Technical Fixes

Order matters — do these top-down.

### 4a. Per-route metadata (if Phase 1 found inheritance anti-pattern)

For every public route, ensure unique title + description + canonical. Framework-specific:

- **Next.js App Router:** `export const metadata: Metadata = { ... }` for static, `generateMetadata` for dynamic. Always set `alternates.canonical` to an absolute URL.
- **React 19 SPA:** `<title>`, `<meta>`, `<link>` in component JSX — React hoists to `<head>`.
- **React 18 SPA:** `react-helmet-async` (not `react-helmet` — unmaintained).

### 4b. Sitemap.xml

Template for Next.js App Router (`src/app/sitemap.ts`):

```ts
import type { MetadataRoute } from 'next';

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const staticRoutes = ['', '/about', '/services', '/blog', '/contact'].map(path => ({
    url: `${SITE_URL}${path}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: path === '' ? 1.0 : 0.8,
  }));

  // Dynamic routes — pull from CMS/filesystem/db
  const posts = await getAllPosts();
  const postRoutes = posts.map(p => ({
    url: `${SITE_URL}/blog/${p.slug}`,
    lastModified: new Date(p.updatedAt),
    changeFrequency: 'monthly' as const,
    priority: 0.6,
  }));

  return [...staticRoutes, ...postRoutes];
}
```

**Rules:**
- `SITE_URL` env-driven, never hardcoded. Validate it doesn't point to localhost or a preview URL.
- Never include auth-gated routes.
- Under 50k URLs per file; use sitemap index if exceeded.
- `lastModified` must be real — stale dates hurt more than help.

### 4c. OG images

Per-route OG images are table stakes. Two approaches:

- **Static (simpler):** place `opengraph-image.png` (1200×630) next to each `page.tsx`. Next.js auto-exports as metadata.
- **Dynamic (blog/products):** `opengraph-image.tsx` using `next/og`:
```tsx
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OG({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return new ImageResponse(
    <div style={{ display: 'flex', fontSize: 64, background: '#000', color: '#fff', width: '100%', height: '100%', padding: 80 }}>
      {post.title}
    </div>
  );
}
```
Validate with [opengraph.xyz](https://www.opengraph.xyz) + LinkedIn Post Inspector (aggressive cache — may need force-refresh).

### 4d. Core Web Vitals fixes

Top five fixes, in order of typical impact:
1. LCP image → add `priority` prop on `next/image`, set `fetchpriority="high"`, add explicit `width`/`height`.
2. Fonts → `next/font` or `font-display: swap` + preload only above-fold weights.
3. Third-party scripts → `next/script strategy="lazyOnload"` for analytics, `afterInteractive` for auth.
4. CLS from images → grep every `<img>` without dimensions, add them.
5. INP from heavy effects → wrap state updates in `useTransition`, defer non-critical work.

### 4e. Stale code cleanup

After all fixes, grep for anything left behind:
```bash
rg -n "react-helmet" --type tsx    # should be gone if migrated
rg -n "next-seo" --type ts         # should be gone if App Router metadata is used
rg -n "useEffect.*document\.title" # imperative title hack — should be gone
```
Delete, don't comment out.

---

## Phase 5 — Verification (real, not theoretical)

Never declare done without running these.

### 5a. Crawl simulation — the fastest GEO check

This catches CSR SPAs that are invisible to AI crawlers. If `h1` count is 0 when user-agent is `PerplexityBot`, the site is not discoverable by Perplexity regardless of JSON-LD.

```bash
URL="{{target url}}"

# Googlebot — should work even on SPAs (Google renders JS)
curl -sL -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" "$URL" | grep -c "<h1"

# PerplexityBot — will NOT execute JS
curl -sL -A "PerplexityBot/1.0" "$URL" | grep -c "<h1"

# ClaudeBot
curl -sL -A "ClaudeBot/1.0 (+claudebot@anthropic.com)" "$URL" | grep -c "<h1"

# GPTBot
curl -sL -A "GPTBot/1.2 (+https://openai.com/gptbot)" "$URL" | grep -c "<h1"

# Check JSON-LD is present in raw HTML (not injected post-hydration)
curl -sL -A "GPTBot/1.2" "$URL" | grep -o 'application/ld+json' | wc -l
```

Expected: every UA returns ≥1 `<h1>` and ≥1 JSON-LD script. If Googlebot returns content but PerplexityBot doesn't, the site is CSR-only — server rendering is required.

### 5b. Lighthouse

```bash
# One-off
npx lighthouse "$URL" --only-categories=seo,performance,accessibility,best-practices --view

# CI — budgets file
npx @lhci/cli autorun --config=.lighthouserc.json
```

Budgets to assert:
- Performance ≥ 90, SEO = 100, Accessibility ≥ 95, Best Practices ≥ 95
- LCP ≤ 2500ms, INP ≤ 200ms, CLS ≤ 0.1

### 5c. Rich Results + schema validation

For each archetype (home, blog post, service, tool, FAQ):
```bash
open "https://search.google.com/test/rich-results?url={{url}}"
open "https://validator.schema.org/?url={{url}}"
```
Fix any errors before shipping. Warnings are optional but worth reading.

### 5d. Search Console baseline

If Google Search Console is verified, capture the pre-fix baseline (so post-fix improvements are provable):
- Coverage: # indexed, # excluded, error types
- Performance: impressions, clicks, CTR, average position — last 28 days
- Core Web Vitals: URLs in Good / Needs improvement / Poor (field data)
- Enhancements: which schema types Google recognizes

If not verified, add it to Phase 6 as a follow-up.

### 5e. AI citation check (manual, 5 min)

Ask ChatGPT, Claude, and Perplexity directly:
```
Prompts:
1. "What is {{site name}}?"
2. "Who is {{author/org name}}?"
3. "What services does {{site/org}} offer?"
4. "Find articles about {{topic}} by {{author}}"
```
Record whether the site is cited. Zero citations = not discoverable. This is the only metric that matters for GEO.

---

## Phase 6 — Monitoring Setup (if not already in place)

Leave-behinds that let the user measure without running the skill again:

- [ ] Google Search Console verified on apex + www, sitemap submitted
- [ ] Bing Webmaster Tools imported from GSC (feeds ChatGPT Search via Bing)
- [ ] Lighthouse CI with budgets in the repo (`.lighthouserc.json`)
- [ ] Vercel Speed Insights or equivalent RUM for field data
- [ ] Monthly calendar reminder: grep server logs for AI crawler hits (`GPTBot`, `ClaudeBot`, `PerplexityBot`). Zero hits = still invisible.

---

## Output Format

```
# SEO + GEO Report — {{site}}

**Stack:** {{framework + rendering}}
**Surface type:** {{marketing / app / hybrid}}
**Traffic goal:** {{google / geo / both}}
**Mode:** {{quick / deep}}
**Date:** {{YYYY-MM-DD}}

## Phase 1 — Audit

| # | Category | Status | Notes |
|---|----------|--------|-------|
...

## Findings (by severity)

### 🐛 Bugs (ship-blockers)
...

### ❌ Missing (high impact)
...

### ⚠️ Partial (quick wins)
...

### ✅ Strengths (keep)
...

## Phase 2 — JSON-LD Coverage Matrix
[route → archetype → required schemas → current state table]

## Phase 3 — GEO Readiness
- Crawler access: ...
- llms.txt: ...
- Content shape: ... (per priority page)
- Entity signals: ...

## Phase 4 — Technical Fix List (ordered)
1. ...

## Phase 5 — Verification Results
- Crawl simulation: ...
- Lighthouse: ...
- Rich Results test: ...
- AI citation check: ...

## Top 5 This-Week Actions
1. ...

## Top 5 This-Month Actions
1. ...

## Unresolved / Needs User Input
- ...
```

---

## Rules

1. **Never mark "Present" without `file:line` evidence.**
2. **Match the audit to the surface type.** Do not recommend sitemap/OG work for an auth-only app.
3. **Options, not verdicts, on tradeoffs.** Present 2–3 options with concrete tradeoffs for architectural choices. Recommend A explicitly.
4. **GEO is hygiene, not magic.** `llms.txt` is not a proven ranking signal. Install it as low-cost discoverability, never overclaim.
5. **Flag framework mismatches loudly.** A CSR SPA that needs AI discoverability requires a framework discussion, not a checklist item. Surface as Finding #1.
6. **No generic SEO preambles.** Every output line must be specific to the codebase in front of you.
7. **Respect post-refactor cleanup.** After recommending fixes, list any stale SEO code that would be orphaned (unwired JSON-LD utilities, dead `react-helmet` imports, commented-out `meta` tags).
8. **Verify before declaring done.** Phase 5 is not optional in deep mode. If tooling isn't available (no curl, no lighthouse, no GSC access), state that explicitly.
9. **Do not silently start editing code.** After producing the report, ask the user which findings to implement. Never touch code without explicit go-ahead.
10. **Honor the traffic goal.** A user optimizing for AI citations should not get 20 Core Web Vitals fixes before JSON-LD and content shape. Re-order findings by goal.

## Known Anti-Patterns

Every anti-pattern below has an exact grep signature. Run them all in Phase 1 — these catch more real bugs than any general checklist ever will.

- **JSON-LD rendered as `<meta>` instead of `<script>`** — **the #1 real-world Next.js App Router SEO bug.** `metadata.other["application/ld+json"]: JSON.stringify(schema)` looks right but Next.js serializes `other` as `<meta name="application/ld+json" content="...">`, invisible to Google and all AI crawlers. Inject via `<script type="application/ld+json" dangerouslySetInnerHTML={{__html: JSON.stringify(schema)}} />` in the layout/page body, not via metadata.
  - Signature: `rg -nC2 '"application/ld\+json"' --type ts --type tsx | rg -B2 -A2 "other:|other ="`

- **Inherited root title across all routes** — every page sharing one title tag. Common in SPA → Next.js migrations.
  - Signature: `rg -l "export const metadata|generateMetadata" src/app app` — if only the root layout matches, flag.

- **Global `images: { unoptimized: true }` in `next.config.*`** — kills AVIF/WebP + responsive sizing. Only acceptable gated on a Capacitor/mobile build env var.
  - Signature: `rg -n "unoptimized.*true" next.config.*`

- **`dangerouslyAllowSVG: true`** in next.config image block — supply-chain risk for remote SVGs even with CSP sandbox.
  - Signature: `rg -n "dangerouslyAllowSVG.*true" next.config.*`

- **Third-party `<Script>` without `strategy="lazyOnload"`** — even `afterInteractive` (the Next.js default) blocks INP on weak devices. Analytics, widgets, chat launchers should always be `lazyOnload`.
  - Signature: `rg -nC1 '<Script' --type tsx | rg -v 'lazyOnload'`

- **Fake `AggregateRating` on free tools / calculators** — `ratingValue: "5.0", reviewCount: "1"` hardcoded = Google spam policy. Manual-action territory.
  - Signature: `rg -n '"AggregateRating"|aggregateRating' --type ts --type tsx` — verify every hit is backed by real visible review data.

- **`WebSite` + `SearchAction` with string `target`** — Google's spec requires `{ "@type": "EntryPoint", "urlTemplate": "..." }` object form. Plain strings fail Sitelinks Search Box validation.
  - Signature: `rg -nB2 -A4 'SearchAction' --type ts` — check `target` field shape.

- **JSON-LD utility defined but never imported** — schemas live in code, no page injects them. Either wire up or delete.
  - Signature: for each `export function generate*Schema` in `src/seo/schema.ts`, verify at least one import exists elsewhere in `src/`.

- **Client-only home page** (`"use client"` on `src/app/page.tsx`) — the highest-GEO-value route should be server-rendered so async data + server-only schema generation work.
  - Signature: `head -1 src/app/page.tsx 2>/dev/null | grep "use client"` — flag if matched.

- **`useEffect`-injected `document.title` / meta tags** — invisible to non-JS crawlers.
  - Signature: `rg -n "useE

…(truncated)
