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
- nisarth/claude-skills-storetheir own version, first seen Nov 8, 2026
- alpit-patel/seo-aeo-geo-kittheir own version, first seen Nov 8, 2026
- foverse/seoversetheir own version, first seen Nov 8, 2026
- insight68/Skillstheir own version, first seen Nov 8, 2026
- figueroaignacio/ignaciofigueroa.devtheir own version, first seen Nov 8, 2026
- ftzi/livespectheir own version, first seen Nov 8, 2026
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.
JwuthriSkillopenseo-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
JwuthriSkillopenseo-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
nicepkgSkillcommunity-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
nicepkgSkillph-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
xfstudioSkillmicro-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-ltdSkillkeyword-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
Other versions185 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 185
- coreyhaines31/marketingskills★ 59ksame name
- anthropics/knowledge-work-plugins★ 35ksame name
- every-app/open-seo★ 27ksame name
- AgriciDaniel/claude-seo★ 20ksame name
- trycompai/crm★ 12ksame name
- Ryze-AI-Adgent/open-seo-mcp-skills★ 8.4ksame name
- TheCraigHewitt/seomachine★ 7.5ksame name
- legions-developer/evilcharts★ 3.2ksame name
- MaxMiksa/Auto-Company★ 3.2ksame name
- MaxMiksa/Autonomous-Company★ 3.2ksame name
175 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository7 other files it gives its agents.
- llms.txtllms.txt
- gap-checkSkill
- plan-reviewSkill
- pre-shipSkill
- pr-triageSkill
- seo-measureSkill
- simplifySkill
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
- Distinctive: line 10
Use this when the goal is real traffic and AI citations, not just a green Lighthouse score.
- Very specific: line 29
Use subagent_type: "general-purpose" for all.
- Distinctive: line 154
Only dispatch if user provides a deployed URL.
- Distinctive: line 204
Only dispatch if user provides target keywords.
- Distinctive: line 247
After the report, ask which findings to implement.
- Distinctive: line 255
Before any phase, establish three things.
- Very specific: line 258
Read package.json, then whichever config file exists: next.config.*, vite.config.*, astro.config.*, svelte.config.*, nuxt.config.*, …
- Distinctive: line 265
Ask if unclear. Three archetypes:
- Distinctive: line 272
Ask the user which outcome they're optimizing for.
- Distinctive: line 285
Before starting, run these commands to get real data:
- Very specific: line 386
All templates below go in <script type="application/ld+json"> in <head> (or via framework-native method).
- Very specific: line 514
Only include aggregateRating if the ratings are real and visible on the page.
- Distinctive: line 562
Run the Rich Results test on at least one page per archetype before merging.
- Distinctive: line 572
Every public site should have explicit rules for AI crawlers.
- Very specific: line 628
Place at public/llms.txt. Follows llmstxt.org convention.
- Distinctive: line 659
Honest framing for the user: no primary source (OpenAI, Anthropic, Google, Perplexity) confirms llms.txt is a ranking signal.
- Distinctive: line 668
Explicit entity names in plain text: Don't put product names only in logos/images — LLMs can't read them.
- Distinctive: line 674
When auditing content pages, flag each of these as ✅/❌ per page.
- Distinctive: line 728
Never include auth-gated routes.
- Very specific: line 736
Static (simpler): place opengraph-image.png (1200×630) next to each page.tsx.
- Distinctive: line 764
After all fixes, grep for anything left behind:
- Distinctive: line 770
Delete, don't comment out.
- Distinctive: line 776
Never declare done without running these.
- Distinctive: line 828
If Google Search Console is verified, capture the pre-fix baseline (so post-fix improvements are provable):
- Distinctive: line 834
If not verified, add it to Phase 6 as a follow-up.
- Distinctive: line 838
Ask ChatGPT, Claude, and Perplexity directly:
- Distinctive: line 852
Leave-behinds that let the user measure without running the skill again:
- Distinctive: line 926
Match the audit to the surface type: Do not recommend sitemap/OG work for an auth-only app.
- Distinctive: line 930
No generic SEO preambles: Every output line must be specific to the codebase in front of you.
- Very specific: line 931
Respect post-refactor cleanup: After recommending fixes, list any stale SEO code that would be orphaned (unwired JSON-LD utilities, dead …
- Distinctive: line 933
Do not silently start editing code: After producing the report, ask the user which findings to implement.
- Distinctive: line 938
Every anti-pattern below has an exact grep signature.
“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.
Files
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
- Framework fit & rendering — SSR/SSG for public routes? Prerender for SPAs? No global static export on web?
- 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? - robots.txt + sitemap.xml — exist, env-driven URLs (no hardcoded localhost), auth routes disallowed, AI crawler rules explicit, sitemap referenced from robots.
- 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? - Open Graph + Twitter — minimum set present, per-route OG images, 1200×630 PNG/JPG (not AVIF), absolute URLs.
- 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).
- GEO readiness —
llms.txt? AI crawler rules in robots.txt? Server-rendered HTML? Semantic HTML landmarks? Entity markup on author/org? - Accessibility-as-SEO — one h1 per page, heading order, alt text, descriptive link text, form labels, landmarks.
- Indexability bugs — any
noindexin prod output? Blocked/_next/static/? Redirect chains? Soft 404s? Infinite URL spaces? - 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-seousage alongside App Routermetadata? 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 stalepackage.jsonentry and a live import look the same frompackage.jsonalone — 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
BreadcrumbListon 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 themetadata.otheranti-pattern). - Recommending
AggregateRatingwithout real review data. - Missing
HowToopportunity 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.tsthat returns the JSON object, then<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />inside the page or layout component. Do NOT useJSON.stringifyinsidegenerateMetadata— metadata API doesn't handle script tags. - Next.js Pages Router → same, inside
<Head>fromnext/head. - Vite/React SPA → React 19 native
<script>in component, orreact-helmet-asyncfor 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
GPTBotremoves your content from OpenAI training, not from ChatGPT search. Search usesOAI-SearchBot+ChatGPT-User. Google-Extendedcontrols Gemini;Googlebotcontrols 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
dateModifiedas 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
PersonorOrganizationJSON-LD on home page with richsameAs(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,generateMetadatafor dynamic. Always setalternates.canonicalto an absolute URL. - React 19 SPA:
<title>,<meta>,<link>in component JSX — React hoists to<head>. - React 18 SPA:
react-helmet-async(notreact-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_URLenv-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.
lastModifiedmust 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 eachpage.tsx. Next.js auto-exports as metadata. - Dynamic (blog/products):
opengraph-image.tsxusingnext/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:
- LCP image → add
priorityprop onnext/image, setfetchpriority="high", add explicitwidth/height. - Fonts →
next/fontorfont-display: swap+ preload only above-fold weights. - Third-party scripts →
next/script strategy="lazyOnload"for analytics,afterInteractivefor auth. - CLS from images → grep every
<img>without dimensions, add them. - 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 goneDelete, 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 -lExpected: 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.jsonBudgets 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
- Never mark "Present" without
file:lineevidence. - Match the audit to the surface type. Do not recommend sitemap/OG work for an auth-only app.
- Options, not verdicts, on tradeoffs. Present 2–3 options with concrete tradeoffs for architectural choices. Recommend A explicitly.
- GEO is hygiene, not magic.
llms.txtis not a proven ranking signal. Install it as low-cost discoverability, never overclaim. - Flag framework mismatches loudly. A CSR SPA that needs AI discoverability requires a framework discussion, not a checklist item. Surface as Finding #1.
- No generic SEO preambles. Every output line must be specific to the codebase in front of you.
- Respect post-refactor cleanup. After recommending fixes, list any stale SEO code that would be orphaned (unwired JSON-LD utilities, dead
react-helmetimports, commented-outmetatags). - 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.
- Do not silently start editing code. After producing the report, ask the user which findings to implement. Never touch code without explicit go-ahead.
- 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 serializesotheras<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 ="
- Signature:
- 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.
- Signature:
- Global
images: { unoptimized: true }innext.config.*— kills AVIF/WebP + responsive sizing. Only acceptable gated on a Capacitor/mobile build env var.- Signature:
rg -n "unoptimized.*true" next.config.*
- Signature:
dangerouslyAllowSVG: truein next.config image block — supply-chain risk for remote SVGs even with CSP sandbox.- Signature:
rg -n "dangerouslyAllowSVG.*true" next.config.*
- Signature:
- Third-party
<Script>withoutstrategy="lazyOnload"— evenafterInteractive(the Next.js default) blocks INP on weak devices. Analytics, widgets, chat launchers should always belazyOnload.- Signature:
rg -nC1 '<Script' --type tsx | rg -v 'lazyOnload'
- Signature:
- Fake
AggregateRatingon 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.
- Signature:
WebSite+SearchActionwith stringtarget— Google's spec requires{ "@type": "EntryPoint", "urlTemplate": "..." }object form. Plain strings fail Sitelinks Search Box validation.- Signature:
rg -nB2 -A4 'SearchAction' --type ts— checktargetfield shape.
- Signature:
- 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*Schemainsrc/seo/schema.ts, verify at least one import exists elsewhere insrc/.
- Signature: for each
- Client-only home page (
"use client"onsrc/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.
- Signature:
useEffect-injecteddocument.title/ meta tags — invisible to non-JS crawlers.- Signature:
rg -n "useEffect.*document\.(title|head)" --type tsx
- Signature:
- 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*
- Signature:
noindexin prod due toNODE_ENVgating instead of platform env (VERCEL_ENV !== 'production').- FAQ JSON-LD with questions not visible on the page — Google penalizes mismatches. Every
Question.name+acceptedAnswer.textmust appear verbatim in rendered content. - Missing
BreadcrumbListon 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"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
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.

