agentleFS
vatsal2210vatsal2210/claude-skills/seo-audit/SKILL.md

seo-audit

Skill0 starsChanged 7 months agoMITView on GitHub

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.

  • Reads credentials

185 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use openseo-review-web-content, openseo-release-notes and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • JwuthriSkill

    openseo-review-web-content

    Write and review content for the OpenSEO website (web/) — blog posts, guides, feature pages, FAQs. Distills the philosophy for on-brand, useful, accurate content. Use whenever adding or editing user-facing prose in web/content or web/src.

    Also in 5 projects that use this

  • JwuthriSkill

    openseo-release-notes

    Cut an OpenSEO release — bump the version, draft user-facing release notes from commits since the last tag, run a review + subagent-verification pass, and open a "release: vX.X.X" PR. Use when the user asks to prepare a release, bump the version, or write release notes.

    Also in 6 projects that use this

  • nicepkgSkill

    community-led-growth

    Expert in community-led growth (CLG) - leveraging user communities to drive acquisition, retention, and expansion. Covers building developer communities, user groups, ambassador programs, and turning customers into advocates. Knows the difference between community as a feature vs community as a growth engine, and how to measure community ROI. Use when "community-led, CLG, ambassador program, champion program, user community, developer community, word of mouth, user group, " mentioned.

    Also in 5 projects that use this

  • nicepkgSkill

    ph-community-outreach

    Engage Reddit, Indie Hackers, Hacker News and other communities for Product Hunt launches. Use this skill to identify relevant communities, craft appropriate posts, and drive cross-platform engagement without appearing spammy.

    Also in 5 projects that use this

  • xfstudioSkill

    micro-saas-launcher

    Expert in launching small, focused SaaS products fast - the indie hacker approach to building profitable software. Covers idea validation, MVP development, pricing, launch strategies, and growing to sustainable revenue. Ship in weeks, not months. Use when: micro saas, indie hacker, small saas, side project, saas mvp.

    Also in 5 projects that use this

  • akii-technologies-ltdSkill

    keyword-clustering

    Cluster and organize keywords into topical groups for SEO. Use when the user asks to "cluster keywords", "group keywords", "organize keywords", "keyword mapping", "topic clusters", "keyword grouping", "build a content plan", "pillar pages", "topical authority", "semantic clustering", or pastes a list of keywords.

    Also in 12 projects that use this

  • +4 more from nicepkg
  • +2 more from Jwuthri

The file

Show its rules and text

We split this skill into the rules it gives an agent

32 rules. 0 are common, 25 are distinctive, and 7 are very specific to this project.

  • 0

    Common

    Most of the 167 edited versions we read keep these.

  • 25

    Distinctive

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

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

    line 10
  2. Very specific:

    Use subagent_type: "general-purpose" for all.

    line 29
  3. Distinctive:

    Only dispatch if user provides a deployed URL.

    line 154
  4. Distinctive:

    Only dispatch if user provides target keywords.

    line 204
  5. Distinctive:

    After the report, ask which findings to implement.

    line 247
  6. Distinctive:

    Before any phase, establish three things.

    line 255
  7. Very specific:

    Read package.json, then whichever config file exists: next.config.*, vite.config.*, astro.config.*, svelte.config.*, nuxt.config.*, …

    line 258
  8. Distinctive:

    Ask if unclear. Three archetypes:

    line 265
  9. Distinctive:

    Ask the user which outcome they're optimizing for.

    line 272
  10. Distinctive:

    Before starting, run these commands to get real data:

    line 285
  11. Very specific:

    All templates below go in <script type="application/ld+json"> in <head> (or via framework-native method).

    line 386
  12. Very specific:

    Only include aggregateRating if the ratings are real and visible on the page.

    line 514
  13. Distinctive:

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

    line 562
  14. Distinctive:

    Every public site should have explicit rules for AI crawlers.

    line 572
  15. Very specific:

    Place at public/llms.txt. Follows llmstxt.org convention.

    line 628
  16. Distinctive:

    Honest framing for the user: no primary source (OpenAI, Anthropic, Google, Perplexity) confirms llms.txt is a ranking signal.

    line 659
  17. Distinctive:

    Explicit entity names in plain text: Don't put product names only in logos/images — LLMs can't read them.

    line 668
  18. Distinctive:

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

    line 674
  19. Distinctive:

    Never include auth-gated routes.

    line 728
  20. Very specific:

    Static (simpler): place opengraph-image.png (1200×630) next to each page.tsx.

    line 736
  21. Distinctive:

    After all fixes, grep for anything left behind:

    line 764
  22. Distinctive:

    Delete, don't comment out.

    line 770
  23. Distinctive:

    Never declare done without running these.

    line 776
  24. Distinctive:

    If Google Search Console is verified, capture the pre-fix baseline (so post-fix improvements are provable):

    line 828
  25. Distinctive:

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

    line 834
  26. Distinctive:

    Ask ChatGPT, Claude, and Perplexity directly:

    line 838
  27. Distinctive:

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

    line 852
  28. Distinctive:

    Match the audit to the surface type: Do not recommend sitemap/OG work for an auth-only app.

    line 926
  29. Distinctive:

    No generic SEO preambles: Every output line must be specific to the codebase in front of you.

    line 930
  30. Very specific:

    Respect post-refactor cleanup: After recommending fixes, list any stale SEO code that would be orphaned (unwired JSON-LD utilities, dead …

    line 931
  31. Distinctive:

    Do not silently start editing code: After producing the report, ask the user which findings to implement.

    line 933
  32. Distinctive:

    Every anti-pattern below has an exact grep signature.

    line 938

“Common” is measured: kept by at least 6 in 10 of the 167 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

# 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.

RouteArchetypeRequired schemasCurrent state
/HomeWebSite + SearchAction, Organization OR Person❌ missing
/aboutAboutAboutPage, Person (with sameAs)❌
/servicesServices indexItemList of Service, optional FAQPage❌
/services/[slug]ServiceService, BreadcrumbList, FAQPage if applicable❌
/blogBlog indexBlog, BreadcrumbList❌
/blog/[slug]Blog postBlogPosting, BreadcrumbList, Person (author)❌
/blog/[slug] (tutorial)Blog post + how-toBlogPosting + HowTo, BreadcrumbList❌
/toolsTools indexItemList of SoftwareApplication❌
/tools/[slug]Tool/productSoftwareApplication OR Product, BreadcrumbList, AggregateRating only if reviews are real and visible❌
/projects/[slug]Case studyCreativeWork OR Article, BreadcrumbList❌
/testimonialsTestimonialsCollectionPage + Review array❌
/teamTeamCollectionPage of Person❌
/contactContactContactPage, optional Organization with contactPoint❌
/faqFAQFAQPage, BreadcrumbList❌
/pricingPricingProduct + 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 before shipping.

WebSite + SearchAction (home page only):

{
  "@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):

{
  "@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):

{
  "@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):

{
  "@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):

{
  "@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):

{
  "@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):

{
  "@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):

{
  "@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:

# 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 convention. Template:

# {{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):

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:
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 + 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:

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.

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

# 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):

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 "useEffect.*document\.(title|head)" --type tsx
  • Client-only layout crashes SSR prerender — root layout pulls Zustand/Context/hooks, throws at build. Fix: mount-guard wrapper on the layout, not dynamic = 'force-dynamic' escape hatch.
  • Sitemap hardcoded to localhost / preview URL — shipped to prod unchanged. Always env-driven.
    • Signature: rg -n 'localhost|vercel\.app|\.preview\.' src/app/sitemap*
  • noindex in prod due to NODE_ENV gating instead of platform env (VERCEL_ENV !== 'production').
  • FAQ JSON-LD with questions not visible on the page — Google penalizes mismatches. Every Question.name + acceptedAnswer.text must appear verbatim in rendered content.
  • Missing BreadcrumbList on blog posts — required for rich-results eligibility, not nice-to-have.
  • Missing <meta name="viewport"> — frameworks usually inject, but verify in built HTML.

Reference Commands (cheat sheet)

# Find all routes
fd -e tsx "page\.tsx$" src/app app 2>/dev/null

# Check current metadata coverage
rg -l "export const metadata|generateMetadata" src/app app 2>/dev/null

# Check current JSON-LD coverage
rg -l "application/ld\+json" src/ 2>/dev/null

# Crawl simulation (replace URL + UA)
curl -sL -A "PerplexityBot/1.0" "$URL" | grep -c "<h1"

# Raw HTML inspection
curl -sL -A "GPTBot/1.2" "$URL" | head -200

# Lighthouse
npx lighthouse "$URL" --only-categories=seo,performance --view

# Rich Results
open "https://search.google.com/test/rich-results?url=$URL"

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.