
Project Manager
- 54 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
AEO (Answer Engine Optimization) is a Claude skill that structures and audits content to be cited by LLMs such as ChatGPT, Claude, Perplexity and Gemini, and tracks those citations.
About
This listing is labeled project-manager, but its SKILL.md and files are an Answer Engine Optimization (AEO) skill named aeo. It optimizes content to be cited by LLMs like ChatGPT, Claude, Perplexity and Gemini, covering how models select sources, five citable content patterns, Q&A schema generation, a citability auditor and citation tracking. A developer or marketer uses it to make pages quotable by AI answer engines and to measure citation rate. Note: the catalog id does not match the underlying skill.
- Five content patterns that get cited by LLMs (definitions, tables, steps, stats, lists)
- Q&A schema generator plus a content citability auditor
- Citation tracking of brand mentions across ChatGPT, Claude, Perplexity, Gemini
Project Manager by the numbers
- 54 all-time installs (skills.sh)
- Ranked #1,304 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
project-manager capabilities & compatibility
- Capabilities
- seo audit · release notes · content strategy
- Use cases
- seo · marketing · research
- Pricing
- Free
What project-manager says it does
Answer Engine Optimization (AEO): optimize content to be cited by LLMs (ChatGPT, Claude, Perplexity, Gemini) in their answers.
**Citation is the new click.** When LLM cites your content, you don't always get a visit — but you get attribution.
After analysis of LLM citation behavior, five content patterns dominate:
npx skills add https://github.com/borghei/claude-skills --skill project-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Structure and audit content so LLMs cite it in their answers, and track which content gets cited by which model.
Who is it for?
Making content citable by AI answer engines and tracking brand citations across LLMs.
Skip if: Traditional Google-SERP SEO (the skill defers that to seo-specialist) or project management despite the catalog label.
When should I use this skill?
Designing content for LLM citation, auditing citability, or adding Q&A schema.
What you get
Content restructured into citable patterns with Q&A schema, plus a measured citation rate per LLM.
- citability audit
- q&a schema
- citation tracking report
By the numbers
- 5 content patterns that get cited
- ships 3 Python scripts (auditor, schema generator, citation extractor)
Files
Answer Engine Optimization (AEO)
End-to-end practice of optimizing content to be cited by LLMs when they generate answers. Covers the technical foundations (how LLMs select sources), content structuring patterns (Q&A schema, citation-worthy patterns), measurement (which content gets cited, by which LLM, how often), and the strategic positioning that differentiates AEO from traditional SEO and from AI-SEO.
This skill is provider-aware but provider-agnostic: works for content optimized for ChatGPT, Claude, Perplexity, Gemini, Copilot, and emerging AI surfaces.
---
When to use this skill
| Situation | Skill applies |
|---|---|
| Designing content strategy that targets LLM citation | Yes — start with AEO fundamentals |
| Auditing existing content for LLM citability | Yes — scripts/aeo_content_auditor.py |
| Adding Q&A schema to content | Yes — scripts/schema_qa_generator.py |
| Tracking which content gets cited by LLMs | Yes — scripts/citation_extractor.py |
| Choosing between AEO and traditional SEO investment | Yes — see AEO vs SEO vs AI-SEO |
| Ranking in Perplexity / Google AI Overviews | Use marketing/ai-seo |
| Traditional SEO (rank in Google search results) | Use marketing/seo-specialist |
---
AEO vs SEO vs AI-SEO
Three distinct (but overlapping) practices. Confusing them leads to wasted investment.
| Practice | Optimizes for | Surface | Success metric |
|---|---|---|---|
| Traditional SEO | Google / Bing rankings | SERPs (organic blue links) | Position, clicks |
| AI-SEO | AI search engines | Perplexity, Google AI Overviews, You.com | Position in AI search results, traffic from citations |
| AEO (this skill) | LLM citation in answers | ChatGPT, Claude, Gemini, Copilot answers | Citation rate, brand mention in LLM outputs |
Strategic positioning
For most B2B brands:
- Traditional SEO: still 50-70% of organic traffic. Don't abandon.
- AI-SEO: emerging 10-20% of search-driven engagement. Growing fast.
- AEO: 5-15% of LLM-mediated user discovery. Largest growth potential.
Optimize content for all three simultaneously; the techniques substantially overlap.
---
The AEO funnel
Users find brands through LLMs in a different funnel than search:
Traditional search: AEO funnel:
1. User types query 1. User asks LLM a question
2. SERPs show ~10 results 2. LLM generates answer
3. User clicks one 3. LLM cites N sources (1-10)
4. User reads page 4. User reads answer; may click cited source
5. User converts 5. User attributes answer to LLM (less so to cited brand)Key implications:
- Citation is the new click. When LLM cites your content, you don't always get a visit — but you get attribution.
- Brand-as-source becomes the goal. Even without click, being cited builds brand association.
- Quality > volume. LLMs cite a small number of sources; quality of citation matters more than ranking position.
- Trust signals matter more. LLMs avoid citing low-authority sources.
See references/aeo-fundamentals.md for the deep mechanics of how LLMs select sources, the citation models per provider, and the trust signals that drive selection.
---
The 5 content patterns that get cited
After analysis of LLM citation behavior, five content patterns dominate:
Pattern 1: Definitional content with clear claims
LLMs cite sources for definitions, facts, and short claims. Pages that answer "What is X?" with a clean 2-3 sentence definition followed by elaboration get cited often.
Structure:
[Term] is [crisp definition in 1-2 sentences].
[Elaboration with context and nuance — 1-3 paragraphs].
[Related concepts / scope / boundaries — optional].Pattern 2: Comparative tables
LLMs use tables to extract comparisons. Markdown tables in published content (or HTML equivalents) get cited when users ask "X vs Y."
| Feature | Product A | Product B |
|---------|-----------|-----------|
| Price | $X | $Y |
| Speed | Z ms | W ms |
| Support | 24/7 | Business hours |Pattern 3: Step-by-step procedural content
"How to [task]" content with explicit numbered steps. LLMs reproduce procedural steps; the cited source becomes the authoritative reference.
Pattern 4: Statistics + data with sources
LLMs cite content that provides numerical facts with attribution. "According to [your study], X% of [thing] does Y" is repeatable and citable.
Pattern 5: Lists with explanations
"Top N approaches to X" with each item explained gets cited when users ask comparative or enumeration questions.
See references/llm-content-structuring.md for deep patterns including FAQ schema, citation hooks, voice-search optimization, and LLM-readable structure markers.
---
Clarify First
Before generating, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Target queries — the actual questions customers ask LLMs about your category (drives which content to audit and restructure)
- [ ] Your brand name — exact wording to track in answers vs competitors (drives citation extraction)
- [ ] Target LLM surface — ChatGPT / Claude / Perplexity / Gemini (citation behavior and trust signals differ per provider)
- [ ] Canonical page/content — the high-value page to be the authoritative source (drives schema generation + pattern restructuring)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Quick start
1. Audit existing content: python3 scripts/aeo_content_auditor.py --path ./content 2. Add Q&A schema to high-value pages: python3 scripts/schema_qa_generator.py --content article.md 3. Track citations from competitors: python3 scripts/citation_extractor.py --query "What is X?" --brand "Your Brand" 4. Iterate: monthly content review with AEO scoring
---
End-to-end workflows
Workflow: AEO content strategy from scratch
1. Identify target queries — what questions do potential customers ask LLMs about your category? 2. Audit competitor citations — which brands get cited for those queries? scripts/citation_extractor.py 3. Audit your existing content — score current content for AEO patterns: scripts/aeo_content_auditor.py 4. Prioritize 10-20 high-value pages — those that should be the canonical source 5. Restructure per AEO patterns — definitional content, tables, step-by-step, statistics 6. Add structured data — scripts/schema_qa_generator.py generates FAQ schema 7. Build authority signals — backlinks, citations, mentions 8. Monitor monthly — track citation rate trend
Workflow: Audit individual content piece
1. Run scripts/aeo_content_auditor.py --path article.md --format markdown 2. Review per-pattern scoring (5 patterns above) 3. Identify gaps: missing definition, no table, no clear steps, no stats, no list 4. Restructure to add 2-3 missing patterns 5. Add FAQ schema with scripts/schema_qa_generator.py 6. Re-audit to confirm improvements
Workflow: Competitive citation analysis
1. Identify 10-20 key queries in your category 2. Query each LLM (ChatGPT, Claude, Perplexity, Gemini) with those questions 3. Record citations + brands mentioned 4. Analyze: which brands dominate? what content do they have? 5. Identify white-space queries (no clear dominant source yet) 6. Prioritize content creation for white-space queries
Workflow: Measure AEO performance
1. Citation rate: % of queries where your brand is cited (target: 30%+ for category leaders) 2. Brand mention rate: % of queries where your brand is mentioned (cited or not) 3. Source quality: are you cited as primary source or supporting? 4. Click-through from citations: traffic attributable to LLM citations (requires source tracking) 5. Voice tracking: how is your brand characterized (positive / neutral / negative attributes)
See references/citation-tracking-and-measurement.md for measurement methodologies, attribution challenges, and competitive benchmarking.
---
Common AEO failures
- Optimizing only for Google SERP: misses the LLM citation surface entirely
- Generic content without specific claims: LLMs prefer specific, factual content over generic explanation
- No structure markers (headings, lists, tables): LLMs can't extract specific information
- No FAQ schema: missed opportunity for Q&A surfacing in AI Overviews
- Stuffed keyword content: LLMs prefer natural language with clear meaning
- No authority signals: LLMs avoid citing low-trust sources
- Outdated content: LLMs prefer recent, current content
- Hidden behind paywalls: LLMs can't cite what they can't access
- No structured data: missed opportunity for richer extraction
- Brand-first content: LLMs prefer informational content over promotional
---
LLM-by-LLM citation behavior
Different LLMs have different citation behaviors:
| LLM | Citation style | What gets cited |
|---|---|---|
| ChatGPT | Inline citations (when web-enabled); fewer otherwise | Recent, authoritative sources |
| Claude | Citations when grounding enabled (tools); generally avoids unsupported claims | High-quality sources, evidence-based |
| Perplexity | Always cites sources prominently | Recent + authoritative sources |
| Google Gemini / AI Overviews | Cites in AI Overviews + Gemini responses | High-ranking pages + structured data |
| Copilot (Microsoft) | Cites sources prominently | Sources varied |
| Meta AI | Lighter citation | Limited transparency |
Optimize content with structure markers (headings, lists, tables) and authority signals (links, citations, expert attribution) — works across all of these.
---
Tooling
| Script | Purpose |
|---|---|
scripts/aeo_content_auditor.py | Score content for AEO patterns (definition, table, steps, stats, list, structure markers) |
scripts/citation_extractor.py | Parse LLM responses (saved transcripts) for brand citations + competitive analysis |
scripts/schema_qa_generator.py | Generate JSON-LD FAQ schema from content (FAQPage / QAPage / HowTo) |
---
References
- aeo-fundamentals.md — how LLMs select sources; citation mechanisms per provider; trust signals
- llm-content-structuring.md — content patterns; Q&A schema; voice-search; structure markers
- citation-tracking-and-measurement.md — measurement methodologies; attribution; benchmarking
---
Related skills
marketing/ai-seo— AI search engine ranking (Perplexity, Google AI Overviews); complementary to AEOmarketing/seo-specialist— traditional SEO (Google rankings); foundational; still 50-70% of organicmarketing/seo-audit— technical SEO auditmarketing/programmatic-seo— scaled content production with SEO patternsc-level-advisor/cs-cmo-advisor— strategic AEO investment decisions
AEO Fundamentals
Deep reference on how LLMs select sources to cite, citation mechanisms per provider, the trust signals that drive selection, and the strategic positioning of AEO relative to traditional SEO and AI-SEO.
---
How LLMs select sources
Modern LLMs use one of three citation mechanisms:
1. Training-time citation (implicit)
Sources mentioned in training data become part of the model's parametric knowledge. The model can reproduce facts without explicitly citing source. Brand mentions accumulated over years of training-data presence shape what the model knows.
Implications:
- Long-tail brand presence in high-quality sources (Wikipedia, major publications, academic) matters even without per-query citation
- New brands have a cold-start disadvantage
- Content authored in 2024+ may not appear until next training cycle (typically 6-18 month lag)
2. Retrieval-time citation (RAG / web-enabled)
When the LLM has web access (ChatGPT browsing, Claude with web search, Perplexity always, Gemini with grounding), it retrieves current sources and cites them in the answer.
Selection signals (varies by provider):
- Search ranking (uses upstream search engine: Google / Bing typically)
- Source authority (domain reputation)
- Content recency
- Content relevance to the specific query
- Structured data presence (helps extraction)
- Citation quality from prior interactions
Implications:
- Traditional SEO matters — LLMs use search results as input
- Content structure matters — extractable content gets cited more
- Authority matters — established domains preferred over new
3. Tool-mediated citation
When LLMs use specific tools (Anthropic's Computer Use, OpenAI function calls, etc.), citations come from tool outputs. Less common for typical user queries.
---
What makes content citable
Beyond search ranking, specific content properties increase citation rate:
Property 1: Specificity over generality
LLMs cite specific claims. "The recommended ratio is 1:7" is more citable than "the ratio depends on context."
Property 2: Extractable structure
LLMs can extract:
- Numbered lists
- Tables with clear headers
- Q&A pairs
- Definitions with clear subject-verb-object structure
- Step-by-step procedures
LLMs cannot reliably extract:
- Narrative prose with implicit structure
- Information embedded in images without alt text
- Content in PDFs (most LLMs can't read PDFs natively)
- Tweet / social-media-style fragments
Property 3: Citation-friendly URL structure
LLMs cite URLs. Good URL structure:
- Permanent / stable
- Human-readable (slug includes topic)
- Accessible (no login wall; no JS-only rendering)
- Indexed (in Google's index = likely in LLM's reach)
Property 4: Author + publication date visible
LLMs prefer cite-able sources with author attribution + date. Avoid:
- Anonymous content
- Hidden dates
- "Last updated" without specific date
Property 5: Authority signals
- Domain authority (Ahrefs DR, Moz DA — proxies)
- Inbound links from other authoritative domains
- Wikipedia presence
- Citations in academic / news sources
- Industry recognition (analyst reports referencing brand)
---
Trust signals (what LLMs avoid)
LLMs (their training and grounding pipelines) systematically de-prioritize:
- Spammy domains
- Aggressive ads
- Thin content (low word count, low information density)
- AI-generated content without human review (yes, ironic)
- Content with high engagement-bait patterns
- Sources flagged for misinformation
- Hate speech / extreme content
- Affiliate-link farms
- Outdated content for time-sensitive topics
- Content behind paywalls (mostly; some LLMs handle some paywalls)
---
Citation mechanisms per provider
ChatGPT (OpenAI)
Without browsing: Cites primarily from training-time knowledge. Citations are implicit (brand mentions appear in answers, but no source links).
With browsing/search: Cites with inline links when answer requires recent / specific information. Browsing typically engaged for time-sensitive or specific factual queries.
SearchGPT (when available): More explicit citation, similar to Perplexity model.
Claude (Anthropic)
Native: Tends to avoid citing without evidence; less likely to provide specific URL citations than ChatGPT.
With tools / grounding: Cites sources when web search / retrieval is part of the response.
Quality bias: Strong preference for high-quality, evidence-based sources. Lower tolerance for low-authority citations.
Perplexity
Citation-first: Every answer includes source citations prominently displayed. Multiple sources per answer (typically 4-10).
Search-grounded: All answers run through search before answering. Heavy use of recent web content.
Implications for AEO: Perplexity is the most-amenable to AEO investment. If you rank in their search, you get cited.
Google Gemini / AI Overviews
AI Overviews (in Google Search): Inline citations to source pages. Selected for queries where AI Overview is shown (mostly informational queries).
Gemini conversational: Cites sources when relevant; web-grounded by default in many use cases.
Implications: Google AI Overviews are heavily SEO-driven. If you rank top 10 in Google, you have a shot at AI Overview citation.
Microsoft Copilot
Bing-grounded: Most answers grounded in Bing search results. Citations prominently displayed.
SharePoint / enterprise: Internal sources cited for enterprise Copilot.
Meta AI
Limited transparency: Citations less consistent. Brand presence in training data more important than retrieval-time.
---
Citation distribution patterns
Observed patterns in LLM citations:
| Pattern | Description |
|---|---|
| Winner-take-most | For specific factual queries, 1-3 sources dominate (often Wikipedia + 1-2 authoritative sites) |
| Diverse for opinion queries | "Best X for Y" queries cite 5-10 sources, more democratic |
| Recency bias | News / current-event queries cite recent sources heavily |
| Authority bias | Health / legal / financial queries cite high-authority sources (.gov, .edu, major publications) |
| Brand bias | When asking about specific brand, the brand's own content is cited (often #1) |
---
Strategic AEO positioning
When AEO matters most
| Industry / context | AEO importance |
|---|---|
| B2B SaaS / Tech | High; technical buyers ask LLMs |
| Professional services (legal, accounting, consulting) | High; high-trust info-seeking |
| Healthcare | High but regulated; trust signals critical |
| Finance | High but regulated |
| Consumer brands | Medium; consumer LLM use growing |
| Local services | Medium; LLM doesn't replace local search well |
| E-commerce | Variable; product-specific queries vs research queries |
When NOT to invest in AEO
- Pre-PMF brands without informational content: AEO requires content investment that may not return
- Hyperlocal businesses: Google Maps still dominates local queries
- Pure transactional commerce: "Buy X" queries less LLM-mediated than "Research X" queries
- Highly regulated content that can't make specific claims: AEO benefits from specificity
AEO investment levels
| Investment level | Activities | Quarterly time |
|---|---|---|
| Minimal | FAQ schema on existing pages; monitor citation | 4-8 hours |
| Standard | Top-20-pages restructuring per AEO patterns; monthly tracking | 40-60 hours |
| Comprehensive | Content strategy + production + monitoring + competitive analysis | 100-200 hours |
| Strategic | Above + brand-positioning campaigns (PR / partnerships) | 300+ hours |
---
How LLMs evolve (and what to track)
LLM citation behavior changes:
- New training cycles → updated parametric knowledge
- New search providers → different retrieval-time citation
- New product features → AI Overviews, Perplexity Pro, etc.
Track:
- LLM market share (which to optimize for)
- New citation surfaces (AI Overviews, Perplexity Spaces, etc.)
- Provider policy changes (training data opt-outs, paywall handling)
- Industry citation patterns in your category
---
Common AEO misconceptions
Misconception 1: "AEO = AI-generated content"
False. AI-generated content is often de-prioritized. AEO is about content STRUCTURE and AUTHORITY, not authorship.
Misconception 2: "AEO is just SEO with new keywords"
False. AEO requires structure (tables, lists, Q&A), specificity (claims, statistics), and authority (trust signals) beyond keyword optimization.
Misconception 3: "If we rank #1 in Google we'll be cited by LLMs"
Mostly true for Perplexity + Google AI Overviews. Less true for ChatGPT / Claude where training-time mentions matter.
Misconception 4: "AEO replaces SEO"
False. SEO is still 50-70% of traffic. AEO adds another channel.
Misconception 5: "We can measure AEO precisely like SEO"
False. AEO measurement is harder. Citations don't always link back; brand mentions in LLM answers are hard to detect; provider attribution varies.
Misconception 6: "Just add Q&A schema and we're optimized"
Partly. Q&A schema helps extraction; doesn't substitute for content structure + authority.
---
AEO and brand voice
AEO content has a tension with brand voice:
- AEO content prefers: factual, structured, specific
- Brand voice often: personal, narrative, distinctive
Balance:
- Brand pages (homepage, about) stay brand-voice
- Knowledge content (blogs, guides, docs) goes AEO-structured
- Product pages mix (top: brand; bottom: structured specs / FAQ)
---
Cheat sheet
| Question | Answer |
|---|---|
| Where to start? | Audit existing content for AEO patterns; restructure top 10 pages |
| Which LLM to optimize for? | All, simultaneously — techniques overlap |
| AEO vs SEO investment split? | 60-70% SEO foundation; 20-30% AEO restructuring + measurement |
| Time to see results? | 3-6 months for content updates to be indexed + reflected in LLM citations |
| Most-impactful single tactic? | FAQ schema on top 20 pages |
| How to measure? | Citation extraction from saved LLM queries; brand mention rate |
| When does AEO replace SEO? | It doesn't; it's a parallel channel |
| Does AEO help conversions? | Indirectly — brand mention in LLM answer drives brand awareness; conversion comes later |
Citation Tracking and Measurement
Reference for measuring AEO performance: citation rate, brand mention rate, sentiment, attribution challenges, competitive benchmarking, dashboard design.
---
What to measure
Primary AEO metrics
| Metric | Definition | Target |
|---|---|---|
| Citation rate | % of target queries where your brand is cited | 30%+ for category leaders; 10%+ for emerging |
| Brand mention rate | % of target queries where your brand is mentioned (cited or not) | 50%+ for category leaders |
| Source quality | Are you cited as primary source or supporting? | Aim for primary on category-defining queries |
| Citation sentiment | Positive / neutral / negative characterization in cited context | > 80% positive/neutral |
| Share of voice (SOV) in citations | Your citations / total citations across competitors | Aim for top-3 in category |
| Click-through from citations | Traffic attributable to LLM citations (if measurable) | Variable; growing |
Secondary metrics
| Metric | Definition |
|---|---|
| Citation freshness | Average age of content cited |
| Citation distribution by surface | Which LLM cites you most (ChatGPT vs Claude vs Perplexity etc.) |
| Query coverage | % of in-category queries where you appear |
| Long-tail vs head citation | Citations for high-volume vs niche queries |
| Schema markup rate | % of your content with FAQ / QAPage / HowTo schema |
---
Citation extraction methodology
Manual method (small scale, qualitative)
1. Identify target queries — list of 20-50 queries in your category 2. Query each LLM — submit each query to ChatGPT, Claude, Perplexity, Gemini, Copilot 3. Capture full response — text + citations (where shown) 4. Tag for analysis — brand mentions, source URLs, sentiment
Pros: Captures full context; sentiment analysis possible Cons: Doesn't scale; sample-based; LLM responses vary across queries
Semi-automated method (medium scale)
1. API-based querying — use LLM APIs to programmatically query 2. Parse responses — extract citations + brand mentions 3. Store in DB — query / LLM / response / citations 4. Dashboard — citation rate over time
Pros: Scales to hundreds of queries; consistent capture Cons: API costs; doesn't capture consumer-LLM behavior (no API access for ChatGPT-app, only API responses)
Third-party tracker tools
Emerging category of tools that track brand mentions in LLMs:
- Profound, Otterly, Athena, others
- Subscribe-based; track your brand vs competitors
- Provide dashboards + alerts
Best for: ongoing tracking without DIY infrastructure.
---
Attribution challenges
AEO attribution is harder than SEO attribution:
Challenge 1: No referrer
When a user clicks a citation in an LLM response, the referrer is often the LLM domain (chat.openai.com, claude.ai, perplexity.ai) — not preserving the originating query.
Workaround:
- UTM parameters on cited URLs (if you control them)
- Cohort analysis: traffic from LLM domains
- Survey users ("How did you hear about us?" → "AI assistant / ChatGPT")
Challenge 2: No-click citations
Most LLM citations don't generate a click. Brand mention exists; web traffic doesn't change.
Workaround:
- Measure brand-mention rate (not just citation-driven traffic)
- Brand-awareness lift survey (annual)
- Direct-traffic increase as proxy
Challenge 3: LLM response variability
Same query, multiple responses, different citations. Statistical significance requires multiple samples per query.
Workaround:
- Query each LLM 3-5x per target query
- Use median citation rate, not single-instance
Challenge 4: Training-time vs retrieval-time
When LLM cites without web grounding, the brand mention comes from training data. Hard to attribute to specific content investment.
Workaround:
- Track brand mentions in long-term training-data-proxy sources (Wikipedia, major publications)
- Distinguish "with browsing" vs "without browsing" citations
Challenge 5: Sentiment / characterization variability
LLM may cite you but characterize you neutrally or negatively. Citation alone insufficient.
Workaround:
- Capture full context, not just URL
- Sentiment classification per citation
- Track narrative shifts over time
---
Sampling methodology
Query selection
Pick queries representative of your category:
- Definitional queries: "What is X?"
- Comparison queries: "X vs Y"
- Best-of queries: "Best X for Y"
- How-to queries: "How to X"
- Recommendation queries: "Should I use X?"
- Brand-specific queries: "Is [brand] worth it?", "[brand] review"
Aim for 50-200 queries per category.
Sampling frequency
- Daily: for time-sensitive industries (news, finance)
- Weekly: standard
- Monthly: for slow-moving categories
LLM sampling
Sample across LLMs in proportion to your audience's usage:
| LLM | Approximate market share (2026) | Recommended sampling weight |
|---|---|---|
| ChatGPT | 60-65% | 60% of queries |
| Gemini | 15-20% | 20% of queries |
| Claude | 8-12% | 10% of queries |
| Perplexity | 5-8% | 5% of queries |
| Copilot | 3-5% | 3% of queries |
| Other | 1-3% | 2% of queries |
Adjust per actual audience usage (verify via survey).
---
Competitive benchmarking
Share of voice (SOV) in citations
SOV (your brand) = your citations / total citations across all brands × 100%For each target query:
- List all brands cited
- Calculate share for each
- Aggregate across queries
Healthy SOV by stage:
- Category leader: 25-40% SOV
- Top 3: 10-25%
- Growing: 5-15%
- Emerging: < 5%
Competitor citation analysis
For competitors that out-cite you:
- Which queries do they win?
- What content do they have for those queries?
- What's the structure / format?
- What's their authority profile?
Compete by:
- Building equivalent content (better structured)
- Targeting white-space queries (no clear leader yet)
- Building authority through earned media
Share of citations by content type
Track which content types drive your citations:
- Blog posts (informational)
- Documentation (technical)
- Product pages (transactional)
- Research / reports (data-driven)
- Customer stories (social proof)
Helps allocate content investment.
---
Sentiment analysis
For each citation, classify sentiment:
| Sentiment | Definition | Example |
|---|---|---|
| Positive | Brand favorably characterized | "[Your brand] is widely considered the leading X" |
| Neutral | Mentioned as one of several options | "Among the options are [your brand]..." |
| Negative | Brand unfavorably characterized | "[Your brand] is known for [negative trait]" |
Detection methods
- Manual: human reviewers tag citations (small samples)
- LLM-assisted: use LLM to classify sentiment of citations (medium scale)
- Automated: NLP-based sentiment analysis (large scale; less accurate for nuance)
Target: > 80% positive/neutral; investigate negative citations (often based on outdated info or specific incidents).
---
Dashboard design
Daily / weekly view
- Citation rate trend (line chart, last 90 days)
- Top 10 queries cited for (table)
- New negative citations (alert)
- LLM-by-LLM distribution (bar chart)
Monthly view
- Citation rate by category
- Share of voice trend
- Content type contribution
- Sentiment distribution
- Competitive comparison
Quarterly view
- AEO ROI estimate
- Content investment vs citation lift
- Strategic recommendations
---
Tooling stack (recommended)
| Tool category | Examples | Use |
|---|---|---|
| LLM APIs | OpenAI, Anthropic, Google AI Studio, Perplexity | Query LLMs programmatically |
| Citation extraction | Custom scripts (our citation_extractor.py) | Parse responses |
| Storage | Postgres / BigQuery / Snowflake | Store query + response + citations |
| Dashboarding | Looker / Tableau / Metabase | Visualize trends |
| Specialized tools | Profound / Otterly / Athena | Ongoing competitive tracking |
| Sentiment | LLM-as-judge or NLP libraries | Classify sentiment |
---
Common measurement failures
Failure 1: Measuring only what's easy
E.g., only counting Perplexity citations (most explicit) and ignoring ChatGPT / Claude (less explicit).
Failure 2: Single-LLM bias
Optimizing for one LLM (often ChatGPT) and missing where audience actually queries.
Failure 3: Conflating mention with citation
Brand mentioned in LLM response = mention. Brand-source URL referenced = citation. Different metrics.
Failure 4: Ignoring sentiment
Citation count up, but negative? Worse than no citation. Always measure sentiment.
Failure 5: Not segmenting by query intent
Branded queries (asking about your brand) ≠ category queries (asking about your space). Citations from each have different value.
Failure 6: Over-relying on direct-traffic proxy
Direct traffic increase has many causes; LLM attribution requires more rigorous methodology.
---
ROI estimation
Estimate AEO ROI:
AEO ROI = (Estimated brand-awareness lift × Brand value per impression × Citation count)
- AEO investment costApproximations:
- Brand-awareness lift per citation: typically equivalent to a low-quality social media impression (~$0.01-$0.10 brand value)
- Or: citation in high-trust context (LLM-cited as authoritative) ~ $0.50-$2.00 brand value per impression
- Aggregate across thousands of citations per month
Specifics vary; require survey-based validation.
Budget allocation guide
Quarterly AEO investment by stage:
| Stage | Investment | Activities |
|---|---|---|
| Pre-PMF | $0 | Focus on PMF; AEO can wait |
| Early growth | $5k-$15k/quarter | Audit + restructure top 10 pages; FAQ schema; basic tracking |
| Growth | $20k-$50k/quarter | Content production + tracking + competitive analysis |
| Scale | $50k-$200k/quarter | Strategic AEO program + brand-positioning campaigns |
| Category leader | $200k+ | Continuous + dominate citation in category |
---
Cheat sheet
| Question | Answer |
|---|---|
| How often to measure? | Weekly for trend; monthly for strategy |
| How many queries to sample? | 50-200 in your category |
| Which LLMs to track? | All major ones; weight per audience usage |
| Citation count alone meaningful? | Partially; pair with sentiment + share of voice |
| Attribution to specific content? | Hard for ChatGPT/Claude; clearer for Perplexity |
| Manual vs automated tracking? | Automated for scale; manual for sentiment / nuance |
| When to invest in 3rd-party tool? | When sampling > 200 queries per month |
| Most-important single metric? | Citation rate trend over time |
| Biggest measurement risk? | Conflating mention with citation; ignoring sentiment |
LLM Content Structuring
Reference for structuring content to maximize LLM citation: the 5 citable content patterns in depth, FAQ / QAPage / HowTo schema, citation hooks, voice-search optimization, structure markers.
---
The 5 citable content patterns (deep)
Pattern 1: Definitional content
LLMs respond to "What is X?" queries with definitions, then cite the source for the definition. To be the cited source:
Format:
[Term] is [crisp definition in 1-2 sentences].
[Elaboration with 1-3 paragraphs: history, context, who uses it, why it matters].
## How [Term] works
[Mechanism / process explanation]
## When to use [Term]
[Use cases]
## When NOT to use [Term]
[Anti-patterns / limitations]
## [Term] vs [Related concept]
[Comparative content]LLM extraction patterns:
- First 1-2 sentences become the definition LLM reproduces
- Subsequent sections answer follow-up questions ("how does X work")
- "When NOT to use" content helps LLM nuance the answer
Anti-patterns:
- Burying the definition behind narrative intro
- Generic definitions ("X is a method...")
- Definitions that require reading 3 paragraphs
Pattern 2: Comparative tables
LLMs love tables. When users ask "X vs Y" or "best X," tables get cited.
Format:
| Feature | Product A | Product B | Product C |
|---------|-----------|-----------|-----------|
| Price | $X/mo | $Y/mo | $Z/mo |
| Speed | 50ms | 75ms | 30ms |
| Best for | Small teams | Enterprise | Solo |
| Free tier | Yes (limited) | No | Yes |
| Support | Email | 24/7 chat | Self-serve |
| Integration count | 50+ | 200+ | 20 |LLM extraction patterns:
- Reproduces table verbatim
- Cites your page as the comparison source
- Sometimes generates "Source: [your site]" attribution
Anti-patterns:
- Tables in images (not extractable)
- Tables with inconsistent columns
- Tables with marketing-claim columns ("amazing", "best in class")
- Single-column tables (not actually comparisons)
Pattern 3: Step-by-step procedural content
"How to [task]" content. LLMs reproduce steps; cited as authoritative.
Format:
# How to [task]
[1-sentence intro: what this accomplishes, prerequisites]
## Step 1: [Action]
[Detail; 1-3 sentences explaining what + why]
## Step 2: [Action]
[Detail]
## Step 3: [Action]
[Detail]
[Continue for all steps]
## What to do if [common error]
[Troubleshooting]LLM extraction patterns:
- Reproduces numbered steps
- Cites your page as procedure source
- Sometimes condenses if very long
Anti-patterns:
- Steps with implicit ordering ("first you'll need to..." in prose)
- Missing step numbers
- Steps that combine multiple actions
- Procedures behind login walls
Pattern 4: Statistics + data with attribution
"According to [your study], X% of Y..." LLMs cite sources for specific statistics.
Format:
[Specific statistic with context]. Source: [your research / study / data analysis], conducted [date], based on [methodology].
For example:
"54% of B2B buyers research vendors via LLMs in 2026, up from 12% in 2024. (Source: [study name] by [your brand], published Feb 2026, based on survey of 1,200 B2B buyers across 12 industries.)"LLM extraction patterns:
- Statistic reproduced verbatim
- Source attribution included if explicit
- Higher citation rate for data with credible methodology
Anti-patterns:
- Statistics without source ("studies show...")
- Methodology not specified
- Old data (LLMs prefer recent)
- Statistics in images / charts (not extractable without alt text)
Pattern 5: Lists with explanations
"Top N [things]" content. LLMs reproduce lists when users ask comparative or enumeration questions.
Format:
# Top [N] [things] for [purpose]
[Intro: 1-2 paragraphs about the category + selection criteria]
## 1. [Item Name]
[2-3 sentences: what it is, why it ranks #1, who should use]
## 2. [Item Name]
[Same structure]
[Continue for N items]
## How we evaluated
[Methodology — adds credibility]LLM extraction patterns:
- Reproduces top 3-5 items
- Cites your page as source of ranking
- Sometimes filters / re-orders based on user-specific context
Anti-patterns:
- Lists without explanations (just names)
- Inconsistent rationale per item
- Lists without selection criteria
---
FAQ / QAPage / HowTo schema (JSON-LD)
Structured data helps LLMs (and search engines) extract Q&A content from your pages. Three relevant schemas:
FAQPage schema
For pages with a list of questions and answers (e.g., FAQ page, knowledge base article with FAQ section).
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is X?",
"acceptedAnswer": {
"@type": "Answer",
"text": "X is..."
}
},
{
"@type": "Question",
"name": "How does X work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "X works by..."
}
}
]
}QAPage schema
For pages that are primarily one Q&A (e.g., Stack Overflow-style question pages, individual help articles).
{
"@context": "https://schema.org",
"@type": "QAPage",
"mainEntity": {
"@type": "Question",
"name": "How do I [task]?",
"acceptedAnswer": {
"@type": "Answer",
"text": "To accomplish this, ..."
}
}
}HowTo schema
For procedural content with steps.
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to [task]",
"description": "Procedure for [task].",
"step": [
{"@type": "HowToStep", "name": "Step 1", "text": "First, ..."},
{"@type": "HowToStep", "name": "Step 2", "text": "Then, ..."}
]
}Where to place schema
- In
<head>as<script type="application/ld+json"> - One schema per page (combining is allowed but cleaner separate)
- Don't duplicate the visible content meaning in schema; describe the structure
Schema verification
Use Google's Rich Results Test (or Schema.org validator) to verify JSON-LD parses correctly. Errors are silent failures (LLMs / search ignore broken schema).
---
Citation hooks (in-content patterns)
Specific in-content patterns that increase citation likelihood:
Hook 1: Explicit "according to [source]" pattern
According to [your study], 73% of users prefer X.When LLM cites this content, the "according to..." pattern transfers as attribution.
Hook 2: Bold key terms / claims
The recommended approach is **X over Y because of Z**.Bold makes claims extraction-friendly.
Hook 3: Pull-quotes / callouts
> Key insight: 80% of [outcome] depends on [factor].LLMs often extract callouts directly.
Hook 4: Inline citations to authoritative sources
This pattern is documented in [authoritative source link].When LLM cites you, the citation chain helps establish your authority.
Hook 5: Update dates visible
**Last updated: 2026-05-01**Recency increases citation rate for time-sensitive topics.
---
Voice-search and conversational query optimization
LLM queries are often conversational ("How do I...", "What's the difference between..."). Optimize content to match conversational query patterns:
Match conversational query templates
| Query template | Content structure |
|---|---|
| "What is X?" | Definitional content |
| "How does X work?" | Mechanism / process explanation |
| "How do I X?" | Step-by-step procedure |
| "Why does X happen?" | Causal / explanatory content |
| "When should I use X?" | Use-case content |
| "X vs Y" | Comparative table + explanation |
| "Best X for Y" | List with criteria |
| "Top N X" | Ranked list |
Title pages / sections matching these patterns directly.
Long-tail conversational queries
Traditional SEO: target high-volume keywords. AEO: also target long-tail conversational queries (3-7 word patterns).
Example:
- SEO target: "data quality" (high volume)
- AEO target: "how do I improve data quality in my data warehouse" (specific, conversational)
Long-tail conversational has lower volume but higher LLM-citation rate.
---
Structure markers LLMs use
LLMs use specific markers to extract content. Use them consistently:
Headings (H1, H2, H3, ...)
- One H1 per page (the main title)
- H2 for major sections
- H3 for sub-sections
- Don't skip levels (H1 → H3 without H2)
LLMs use heading hierarchy to understand content structure.
Ordered vs unordered lists
- Ordered (numbered): use for sequential steps, ranked items
- Unordered (bullets): use for parallel items without order
LLMs preserve the distinction.
Tables
- Use Markdown table syntax (or HTML
<table>) — extractable - First row = headers
- Consistent columns per row
- One table per concept (avoid sprawling tables)
Code blocks
- Use fenced code blocks with language hints (```python)
- LLMs reproduce code from properly-fenced blocks
- Inline code with backticks for short references
Definition lists (Markdown via : or HTML <dl>)
Underused but powerful for definitional content:
<dl>
<dt>Term</dt>
<dd>Definition</dd>
</dl>LLMs extract dt/dd pairs efficiently.
---
Content length
| Content type | Optimal length |
|---|---|
| Definitional content | 500-1500 words |
| How-to guide | 800-2500 words |
| Comparative analysis | 1500-3500 words |
| Statistics report | 1000-3000 words |
| Listicle (top N) | 1500-3500 words |
Too short = thin content; too long = LLM may truncate or miss key sections.
---
Multimedia and LLM extraction
LLMs (as of 2026) extract from text well; multimedia partially:
| Media | LLM extraction | Recommendation |
|---|---|---|
| Plain text | Excellent | Use as primary content |
| Markdown / HTML tables | Excellent | Use freely |
| Bullet / numbered lists | Excellent | Use freely |
| Headings | Excellent | Use for structure |
| Code blocks | Excellent (with language hints) | Use freely |
| Images | Limited (depends on alt text + nearby caption) | Always include alt text |
| Charts / data visualizations | Limited | Pair with data table + alt text |
| Videos | Limited (some LLMs can transcribe; not for citation) | Pair with transcript |
| Embeds (Twitter, YouTube) | Variable | Don't rely on embed content for citation |
| PDF content | Limited (some LLMs read PDFs; many can't) | Publish key content in HTML, not PDF-only |
| Tables in images | Cannot extract | Always render tables in HTML / Markdown |
---
Updating content for AEO
Existing content audit checklist:
- [ ] Does the page answer a specific question?
- [ ] Is there a 1-2 sentence definition / direct answer at the top?
- [ ] Are there headings (H2 / H3) for sub-topics?
- [ ] Are key claims bolded?
- [ ] Are there extractable tables, lists, or step-by-steps?
- [ ] Are statistics attributed?
- [ ] Is there an FAQ section?
- [ ] Is FAQ schema added?
- [ ] Is the last-updated date visible?
- [ ] Is author attribution visible?
- [ ] Are images alt-texted?
- [ ] Are PDFs paired with HTML equivalents?
- [ ] Does the URL include relevant keywords?
Run through this for each priority page.
---
Cheat sheet
| Question | Answer |
|---|---|
| Most impactful single change? | Add 1-2 sentence definition / direct answer at top of page |
| Add FAQ schema everywhere? | On pages with Q&A content; not on all pages |
| Length sweet spot? | 1500-2500 words for most content types |
| Bullet vs numbered? | Numbered for sequential; bullet for parallel |
| Tables in images? | Never. Always HTML / Markdown |
| PDF-only content? | Don't. HTML or HTML+PDF |
| Update dates visible? | Yes; especially for time-sensitive content |
| Inline keywords still matter? | Less than 5 years ago, but still relevant |
| Long-tail or head terms? | Both; long-tail favored for AEO |
| Match conversational query templates? | Yes; title pages / sections directly |
#!/usr/bin/env python3
"""
aeo_content_auditor.py — Score content for Answer Engine Optimization (AEO) patterns.
Scans markdown/HTML content; emits per-pattern score (definition, table, steps,
stats, list, structure markers) + overall AEO score + remediation recommendations.
Stdlib only. Markdown or JSON.
Usage:
python3 aeo_content_auditor.py --path ./content
python3 aeo_content_auditor.py --path article.md --format json
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any
# Detection patterns for AEO content properties
RE_HEADING = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
RE_MARKDOWN_TABLE = re.compile(r"^\|.+\|\s*$\n^\|[\s\-:|]+\|\s*$", re.MULTILINE)
RE_HTML_TABLE = re.compile(r"<table\b", re.IGNORECASE)
RE_NUMBERED_LIST = re.compile(r"^\s*\d+\.\s+\S", re.MULTILINE)
RE_BULLET_LIST = re.compile(r"^\s*[-*+]\s+\S", re.MULTILINE)
RE_DEFINITION_TERM = re.compile(r"^([A-Z][\w\s-]{2,40})\s+is\s+", re.MULTILINE)
RE_STATISTIC = re.compile(r"\b\d+(?:\.\d+)?%\s|\b\$\d+\b|\b\d+(?:,\d{3})+\b")
RE_SOURCE_ATTRIBUTION = re.compile(r"\b(?:according to|source:|cited from|per the|\[.*?\]\(.+?\))", re.IGNORECASE)
RE_BOLD = re.compile(r"\*\*[^*]+\*\*|__[^_]+__")
RE_FAQ_HEADING = re.compile(r"^#{1,3}\s+(?:FAQ|Frequently Asked Questions|Q&A|Questions)\s*$", re.IGNORECASE | re.MULTILINE)
RE_QA_PATTERN = re.compile(r"^(?:Q:|Question:)\s+.+\n+(?:A:|Answer:)\s+", re.IGNORECASE | re.MULTILINE)
RE_QA_SCHEMA = re.compile(r'"@type"\s*:\s*"(?:FAQPage|QAPage|Question|HowTo)"')
RE_HOWTO_STEP = re.compile(r"^#{1,4}\s+Step\s+\d+", re.IGNORECASE | re.MULTILINE)
RE_LAST_UPDATED = re.compile(r"\b(?:last updated|updated on|published on|published)\s*[:]\s*\d{4}-\d{2}-\d{2}", re.IGNORECASE)
RE_PDF_LINK = re.compile(r"\.pdf\b", re.IGNORECASE)
RE_IMAGE = re.compile(r"!\[([^\]]*)\]\([^\)]+\)|<img\s[^>]*>", re.IGNORECASE)
@dataclass
class PatternScore:
pattern: str
score: int
max_score: int
finding: str
@dataclass
class FileScore:
path: str
word_count: int
heading_count: int
pattern_scores: list[PatternScore]
overall_score: int
recommendations: list[str]
def count_words(text: str) -> int:
# Strip code blocks + HTML tags then count
stripped = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
stripped = re.sub(r"<[^>]+>", "", stripped)
return len(stripped.split())
def detect_definitional_content(text: str, headings: list[str]) -> PatternScore:
"""Score: heading with 'What is' or definitional sentence near top."""
score = 0
finding = ""
has_what_is = any(re.search(r"what is\s+\S", h.lower()) for h in headings[:3])
has_def_sentence = bool(RE_DEFINITION_TERM.search(text[:500]))
if has_what_is:
score += 50
finding += "✓ 'What is' heading near top. "
if has_def_sentence:
score += 50
finding += "✓ Definitional sentence near top. "
if not finding:
finding = "✗ No definitional structure detected."
return PatternScore("Definitional content", score, 100, finding.strip())
def detect_tables(text: str) -> PatternScore:
md_tables = len(RE_MARKDOWN_TABLE.findall(text))
html_tables = len(RE_HTML_TABLE.findall(text))
total = md_tables + html_tables
if total == 0:
return PatternScore("Comparative tables", 0, 100, "✗ No tables found")
score = min(100, total * 30)
return PatternScore("Comparative tables", score, 100,
f"✓ {total} table(s) found ({md_tables} markdown, {html_tables} HTML)")
def detect_step_by_step(text: str) -> PatternScore:
numbered = len(RE_NUMBERED_LIST.findall(text))
step_headings = len(RE_HOWTO_STEP.findall(text))
score = min(100, numbered * 8 + step_headings * 20)
if score == 0:
return PatternScore("Step-by-step procedural", 0, 100, "✗ No numbered steps or step headings")
return PatternScore("Step-by-step procedural", score, 100,
f"✓ {numbered} numbered list items + {step_headings} step headings")
def detect_statistics(text: str) -> PatternScore:
stats = len(RE_STATISTIC.findall(text))
sources = len(RE_SOURCE_ATTRIBUTION.findall(text))
score = min(100, stats * 5 + sources * 10)
if stats == 0:
return PatternScore("Statistics + attribution", 0, 100, "✗ No statistical claims detected")
if stats > 0 and sources == 0:
return PatternScore("Statistics + attribution", min(40, score), 100,
f"⚠️ {stats} statistics but no source attributions")
return PatternScore("Statistics + attribution", score, 100,
f"✓ {stats} statistics + {sources} source attributions")
def detect_lists(text: str) -> PatternScore:
bullets = len(RE_BULLET_LIST.findall(text))
numbered = len(RE_NUMBERED_LIST.findall(text))
total = bullets + numbered
if total < 3:
return PatternScore("Lists with structure", 0, 100, "✗ Too few list items")
score = min(100, total * 4)
return PatternScore("Lists with structure", score, 100,
f"✓ {bullets} bullet + {numbered} numbered items")
def detect_structure_markers(text: str, headings: list[str]) -> PatternScore:
"""Headings, bold, FAQ patterns, schema."""
score = 0
findings = []
if len(headings) >= 3:
score += 25
findings.append(f"{len(headings)} headings")
if RE_BOLD.search(text):
score += 20
findings.append("bold text used")
if RE_FAQ_HEADING.search(text) or RE_QA_PATTERN.search(text):
score += 25
findings.append("FAQ section present")
if RE_QA_SCHEMA.search(text):
score += 30
findings.append("✓ FAQ/QAPage/HowTo schema found")
if score == 0:
return PatternScore("Structure markers", 0, 100, "✗ Weak structural markers")
return PatternScore("Structure markers", score, 100, "✓ " + ", ".join(findings))
def detect_freshness(text: str) -> PatternScore:
if RE_LAST_UPDATED.search(text):
return PatternScore("Freshness indicator", 100, 100, "✓ Last-updated date visible")
return PatternScore("Freshness indicator", 0, 100, "✗ No last-updated date visible")
def detect_extractability_issues(text: str) -> list[str]:
issues = []
if RE_PDF_LINK.search(text):
issues.append("PDF links present; ensure HTML equivalent exists for LLM extraction")
img_matches = RE_IMAGE.findall(text)
if img_matches:
no_alt = sum(1 for m in img_matches if not m or m == "")
if no_alt > 0:
issues.append(f"{no_alt} images may lack alt text (LLM can't extract content)")
return issues
def score_file(path: Path, text: str) -> FileScore:
word_count = count_words(text)
heading_matches = RE_HEADING.findall(text)
headings = [m[1] for m in heading_matches]
patterns = [
detect_definitional_content(text, headings),
detect_tables(text),
detect_step_by_step(text),
detect_statistics(text),
detect_lists(text),
detect_structure_markers(text, headings),
detect_freshness(text),
]
overall = sum(p.score for p in patterns) // len(patterns)
recommendations = []
if word_count < 300:
recommendations.append("Content very short (<300 words); LLMs prefer 1500-2500 words")
elif word_count > 5000:
recommendations.append("Content very long (>5000 words); LLMs may miss key sections; consider splitting")
for p in patterns:
if p.score < 50:
recommendations.append(f"Improve {p.pattern}: {p.finding}")
issues = detect_extractability_issues(text)
recommendations.extend(issues)
return FileScore(
path=str(path),
word_count=word_count,
heading_count=len(headings),
pattern_scores=patterns,
overall_score=overall,
recommendations=recommendations,
)
def scan_path(root: Path) -> list[FileScore]:
results: list[FileScore] = []
if root.is_file():
if root.suffix in (".md", ".html", ".htm"):
text = root.read_text(encoding="utf-8", errors="ignore")
results.append(score_file(root, text))
return results
for ext in (".md", ".html", ".htm"):
for p in root.rglob(f"*{ext}"):
if any(part.startswith(".") for part in p.parts):
continue
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
results.append(score_file(p, text))
return results
def render_markdown(results: list[FileScore]) -> str:
out = ["# AEO Content Audit", ""]
out.append(f"_Files analyzed: {len(results)}_")
out.append("")
if results:
avg = sum(r.overall_score for r in results) // len(results)
out.append(f"**Average AEO score**: {avg}/100")
out.append("")
out.append("## Summary by File")
out.append("")
out.append("| File | Words | Overall | Definition | Tables | Steps | Stats | Lists | Structure | Fresh |")
out.append("|------|-------|---------|------------|--------|-------|-------|-------|-----------|-------|")
for r in sorted(results, key=lambda x: x.overall_score):
scores = {p.pattern: p.score for p in r.pattern_scores}
out.append(
f"| {r.path} | {r.word_count} | **{r.overall_score}** | "
f"{scores.get('Definitional content', 0)} | {scores.get('Comparative tables', 0)} | "
f"{scores.get('Step-by-step procedural', 0)} | {scores.get('Statistics + attribution', 0)} | "
f"{scores.get('Lists with structure', 0)} | {scores.get('Structure markers', 0)} | "
f"{scores.get('Freshness indicator', 0)} |"
)
out.append("")
out.append("## Per-File Recommendations")
out.append("")
for r in sorted(results, key=lambda x: x.overall_score):
if r.recommendations:
out.append(f"### {r.path} (score {r.overall_score}/100)")
for rec in r.recommendations:
out.append(f"- {rec}")
out.append("")
return "\n".join(out)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Audit content for AEO (Answer Engine Optimization) patterns",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--path", required=True, help="File or directory to audit")
p.add_argument("--format", choices=["markdown", "json"], default="markdown")
p.add_argument("--output", help="Output file path")
return p.parse_args()
def main() -> int:
args = parse_args()
root = Path(args.path).resolve()
if not root.exists():
print(f"error: path not found: {root}", file=sys.stderr)
return 2
results = scan_path(root)
if args.format == "json":
out = json.dumps([asdict(r) for r in results], indent=2, default=str)
else:
out = render_markdown(results)
if args.output:
Path(args.output).write_text(out)
print(f"wrote {args.output}", file=sys.stderr)
else:
print(out)
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
citation_extractor.py — Extract brand citations + URLs from LLM response transcripts.
Reads a JSON or text file containing LLM responses; identifies:
- URL citations (cited sources)
- Brand mentions (from a provided brand list)
- Sentiment context (positive/neutral/negative around each mention)
- Share of voice across brands
Stdlib only. Markdown or JSON.
Input format (JSON):
[
{
"query": "What is the best CRM?",
"llm": "ChatGPT",
"response": "The most popular CRM tools include Salesforce, HubSpot, ...",
"citations": ["https://example.com/source1"]
},
...
]
Or plain-text file with one response per double-newline-separated block.
Usage:
python3 citation_extractor.py --responses responses.json --brands "Your Brand,Competitor A,Competitor B"
python3 citation_extractor.py --responses transcripts.txt --brands "Acme,Globex" --format json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import defaultdict
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any
URL_RE = re.compile(r"https?://[^\s\)\]\}\>\"]+")
# Sentiment indicator words (basic heuristic; not a replacement for proper sentiment analysis)
POSITIVE_WORDS = {
"best", "leading", "top", "excellent", "recommended", "favorite", "preferred",
"outstanding", "superior", "winning", "first", "trusted", "popular", "premier",
"proven", "powerful", "robust", "reliable", "successful", "innovative",
}
NEGATIVE_WORDS = {
"worst", "poor", "bad", "weak", "outdated", "deprecated", "lagging",
"inferior", "failing", "questionable", "unreliable", "abandoned", "discontinued",
"criticized", "controversial", "lawsuit", "scandal",
}
@dataclass
class Mention:
query: str
llm: str
brand: str
context_snippet: str
sentiment: str # positive / neutral / negative
cited_with_url: bool
@dataclass
class Report:
total_responses: int
brand_mentions_count: int
per_brand_mentions: dict[str, int]
per_brand_sentiment: dict[str, dict[str, int]]
per_llm_mentions: dict[str, int]
share_of_voice_pct: dict[str, float]
mentions: list[Mention]
cited_urls: list[str]
def classify_sentiment(context: str, brand: str) -> str:
"""Naive sentiment based on nearby positive/negative words."""
context_lower = context.lower()
pos = sum(1 for w in POSITIVE_WORDS if w in context_lower)
neg = sum(1 for w in NEGATIVE_WORDS if w in context_lower)
if pos > neg:
return "positive"
if neg > pos:
return "negative"
return "neutral"
def extract_mentions(query: str, llm: str, response: str, brands: list[str], citations: list[str]) -> list[Mention]:
"""Find brand mentions in response with context."""
mentions: list[Mention] = []
for brand in brands:
pattern = re.compile(r"\b" + re.escape(brand) + r"\b", re.IGNORECASE)
for match in pattern.finditer(response):
# Get context around mention (50 chars before + 100 chars after)
start = max(0, match.start() - 50)
end = min(len(response), match.end() + 100)
context = response[start:end].strip()
sentiment = classify_sentiment(context, brand)
# Check if any citation URL contains brand domain (rough proxy)
cited = any(brand.lower().replace(" ", "") in c.lower() for c in citations)
mentions.append(Mention(
query=query,
llm=llm,
brand=brand,
context_snippet=context[:200],
sentiment=sentiment,
cited_with_url=cited,
))
return mentions
def parse_responses(path: Path) -> list[dict[str, Any]]:
text = path.read_text(encoding="utf-8", errors="ignore")
suffix = path.suffix.lower()
if suffix == ".json":
data = json.loads(text)
if isinstance(data, list):
return data
return [data]
# Plain text: split by double-newline; each block is one response
blocks = [b.strip() for b in text.split("\n\n\n") if b.strip()]
return [{"query": "<unknown>", "llm": "<unknown>", "response": b, "citations": []} for b in blocks]
def analyze(responses: list[dict[str, Any]], brands: list[str]) -> Report:
all_mentions: list[Mention] = []
per_brand: dict[str, int] = defaultdict(int)
per_brand_sentiment: dict[str, dict[str, int]] = defaultdict(lambda: {"positive": 0, "neutral": 0, "negative": 0})
per_llm: dict[str, int] = defaultdict(int)
cited_urls_set: set[str] = set()
for r in responses:
query = r.get("query", "<unknown>")
llm = r.get("llm", "<unknown>")
response_text = r.get("response", "")
citations = r.get("citations", []) or []
# Extract URLs from response text too
extracted_urls = URL_RE.findall(response_text)
all_citations = list(set(citations + extracted_urls))
cited_urls_set.update(all_citations)
mentions = extract_mentions(query, llm, response_text, brands, all_citations)
all_mentions.extend(mentions)
for m in mentions:
per_brand[m.brand] += 1
per_brand_sentiment[m.brand][m.sentiment] += 1
per_llm[m.llm] += 1
total_mentions = sum(per_brand.values())
sov: dict[str, float] = {}
if total_mentions > 0:
for brand, count in per_brand.items():
sov[brand] = round(100 * count / total_mentions, 1)
return Report(
total_responses=len(responses),
brand_mentions_count=total_mentions,
per_brand_mentions=dict(per_brand),
per_brand_sentiment={k: dict(v) for k, v in per_brand_sentiment.items()},
per_llm_mentions=dict(per_llm),
share_of_voice_pct=sov,
mentions=all_mentions,
cited_urls=sorted(cited_urls_set),
)
def render_markdown(r: Report) -> str:
out = ["# LLM Citation Extraction Report", ""]
out.append(f"_Responses analyzed: {r.total_responses}_")
out.append(f"_Total brand mentions: {r.brand_mentions_count}_")
out.append("")
out.append("## Share of Voice")
out.append("")
out.append("| Brand | Mentions | SOV % |")
out.append("|-------|----------|-------|")
for brand, count in sorted(r.per_brand_mentions.items(), key=lambda x: -x[1]):
sov = r.share_of_voice_pct.get(brand, 0)
out.append(f"| {brand} | {count} | {sov}% |")
out.append("")
out.append("## Sentiment by Brand")
out.append("")
out.append("| Brand | Positive | Neutral | Negative |")
out.append("|-------|----------|---------|----------|")
for brand, sentiments in r.per_brand_sentiment.items():
out.append(f"| {brand} | {sentiments['positive']} | {sentiments['neutral']} | {sentiments['negative']} |")
out.append("")
out.append("## Mentions by LLM")
out.append("")
out.append("| LLM | Mentions |")
out.append("|-----|----------|")
for llm, count in sorted(r.per_llm_mentions.items(), key=lambda x: -x[1]):
out.append(f"| {llm} | {count} |")
out.append("")
if r.cited_urls:
out.append(f"## Cited URLs ({len(r.cited_urls)})")
out.append("")
for url in r.cited_urls[:50]:
out.append(f"- {url}")
if len(r.cited_urls) > 50:
out.append(f"... and {len(r.cited_urls) - 50} more")
out.append("")
# Sample negative mentions (worth investigation)
negative = [m for m in r.mentions if m.sentiment == "negative"]
if negative:
out.append(f"## ⚠️ Negative Mentions ({len(negative)}) — investigate")
out.append("")
for m in negative[:20]:
out.append(f"- **{m.brand}** ({m.llm}): \"{m.context_snippet}\"")
out.append("")
return "\n".join(out)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Extract brand citations + mentions from LLM responses",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--responses", required=True, help="JSON or text file with LLM responses")
p.add_argument("--brands", required=True, help="Comma-separated brand names to track")
p.add_argument("--format", choices=["markdown", "json"], default="markdown")
p.add_argument("--output", help="Output file path")
return p.parse_args()
def main() -> int:
args = parse_args()
try:
responses = parse_responses(Path(args.responses))
except (OSError, json.JSONDecodeError) as e:
print(f"error loading responses: {e}", file=sys.stderr)
return 2
brands = [b.strip() for b in args.brands.split(",") if b.strip()]
r = analyze(responses, brands)
if args.format == "json":
out = json.dumps(asdict(r), indent=2, default=str)
else:
out = render_markdown(r)
if args.output:
Path(args.output).write_text(out)
print(f"wrote {args.output}", file=sys.stderr)
else:
print(out)
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
schema_qa_generator.py — Generate JSON-LD FAQ / QAPage / HowTo schema from content.
Reads a markdown or HTML file; extracts Q&A pairs (or how-to steps); emits
JSON-LD schema ready to embed in <head>.
Detection patterns:
- FAQ: Q&A patterns (Q: ... A: ...) or heading-followed-by-paragraph
- QAPage: single Q&A
- HowTo: numbered steps under # How to ... heading
Stdlib only. JSON-LD output or JSON wrapper.
Usage:
python3 schema_qa_generator.py --content article.md
python3 schema_qa_generator.py --content article.md --schema-type FAQPage
python3 schema_qa_generator.py --content faq.md --schema-type FAQPage --output schema.json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any
# Q&A heading patterns: H2/H3 questions followed by content
RE_HEADING_QA = re.compile(
r"^#{2,3}\s+(.+\?)\s*$\n+([^\n#].+?)(?=\n#{1,3}\s+|\Z)",
re.MULTILINE | re.DOTALL,
)
# Q: / A: pattern
RE_QA_PATTERN = re.compile(
r"(?:^|\n)(?:\*\*)?Q(?:uestion)?:?\s*[:\-]?\s*(.+?)(?:\*\*)?\n+(?:\*\*)?A(?:nswer)?:?\s*[:\-]?\s*(.+?)(?=\n+(?:Q(?:uestion)?:|\n#|\Z))",
re.IGNORECASE | re.DOTALL,
)
# How-to step heading
RE_HOWTO_STEP = re.compile(
r"^#{2,4}\s+Step\s+\d+:?\s*(.+?)\s*$\n+(.+?)(?=\n#{1,4}\s+|\Z)",
re.IGNORECASE | re.MULTILINE | re.DOTALL,
)
# How-to overall heading
RE_HOWTO_HEADING = re.compile(r"^#\s+How\s+to\s+(.+?)\s*$", re.IGNORECASE | re.MULTILINE)
@dataclass
class QAPair:
question: str
answer: str
@dataclass
class HowToStep:
name: str
text: str
def clean_text(text: str) -> str:
"""Strip markdown formatting; trim whitespace."""
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
text = re.sub(r"\*([^*]+)\*", r"\1", text) # italic
text = re.sub(r"`([^`]+)`", r"\1", text) # inline code
text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text) # links
text = re.sub(r"\s+", " ", text) # collapse whitespace
return text.strip()
def extract_qa_pairs(text: str) -> list[QAPair]:
pairs: list[QAPair] = []
# Pattern 1: H2/H3 questions
for m in RE_HEADING_QA.finditer(text):
question = clean_text(m.group(1))
answer = clean_text(m.group(2)[:500]) # cap answer at 500 chars
if question and answer and len(question) < 200:
pairs.append(QAPair(question=question, answer=answer))
# Pattern 2: Q: / A:
for m in RE_QA_PATTERN.finditer(text):
question = clean_text(m.group(1).rstrip("?") + "?")
answer = clean_text(m.group(2)[:500])
if question and answer and len(question) < 200:
# Avoid duplicates
if not any(p.question.lower() == question.lower() for p in pairs):
pairs.append(QAPair(question=question, answer=answer))
return pairs
def extract_howto_steps(text: str) -> tuple[str, list[HowToStep]]:
"""Returns (howto_title, steps)."""
howto_match = RE_HOWTO_HEADING.search(text)
if not howto_match:
return "", []
title = clean_text(howto_match.group(1))
steps: list[HowToStep] = []
for m in RE_HOWTO_STEP.finditer(text):
name = clean_text(m.group(1))
step_text = clean_text(m.group(2)[:400])
steps.append(HowToStep(name=name, text=step_text))
return title, steps
def build_faq_schema(pairs: list[QAPair]) -> dict[str, Any]:
return {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": p.question,
"acceptedAnswer": {
"@type": "Answer",
"text": p.answer,
},
}
for p in pairs
],
}
def build_qapage_schema(pair: QAPair) -> dict[str, Any]:
return {
"@context": "https://schema.org",
"@type": "QAPage",
"mainEntity": {
"@type": "Question",
"name": pair.question,
"acceptedAnswer": {
"@type": "Answer",
"text": pair.answer,
},
},
}
def build_howto_schema(title: str, steps: list[HowToStep]) -> dict[str, Any]:
return {
"@context": "https://schema.org",
"@type": "HowTo",
"name": f"How to {title}",
"step": [
{
"@type": "HowToStep",
"name": s.name,
"text": s.text,
}
for s in steps
],
}
def auto_detect_schema_type(pairs: list[QAPair], howto_steps: list[HowToStep]) -> str:
if len(howto_steps) >= 3:
return "HowTo"
if len(pairs) >= 3:
return "FAQPage"
if len(pairs) == 1:
return "QAPage"
if len(pairs) == 2:
return "FAQPage"
return "none"
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Generate JSON-LD schema from content",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--content", required=True, help="Markdown or HTML content file")
p.add_argument("--schema-type", choices=["FAQPage", "QAPage", "HowTo", "auto"], default="auto")
p.add_argument("--format", choices=["json-ld", "html", "wrapped-json"], default="json-ld",
help="json-ld: just the schema; html: wrapped in <script> tag; wrapped-json: with metadata")
p.add_argument("--output", help="Output file path")
return p.parse_args()
def main() -> int:
args = parse_args()
try:
text = Path(args.content).read_text(encoding="utf-8", errors="ignore")
except OSError as e:
print(f"error: {e}", file=sys.stderr)
return 2
pairs = extract_qa_pairs(text)
howto_title, howto_steps = extract_howto_steps(text)
schema_type = args.schema_type
if schema_type == "auto":
schema_type = auto_detect_schema_type(pairs, howto_steps)
if schema_type == "none":
print("error: no Q&A pairs or how-to steps detected in content", file=sys.stderr)
return 1
if schema_type == "FAQPage":
if not pairs:
print("error: no Q&A pairs detected for FAQPage schema", file=sys.stderr)
return 1
schema = build_faq_schema(pairs)
elif schema_type == "QAPage":
if not pairs:
print("error: no Q&A pair detected for QAPage schema", file=sys.stderr)
return 1
schema = build_qapage_schema(pairs[0])
elif schema_type == "HowTo":
if not howto_steps:
print("error: no how-to steps detected", file=sys.stderr)
return 1
schema = build_howto_schema(howto_title, howto_steps)
else:
print(f"error: unknown schema type: {schema_type}", file=sys.stderr)
return 2
if args.format == "html":
out = f'<script type="application/ld+json">\n{json.dumps(schema, indent=2)}\n</script>'
elif args.format == "wrapped-json":
out = json.dumps({
"schema_type": schema_type,
"extracted_qa_count": len(pairs),
"extracted_howto_step_count": len(howto_steps),
"schema": schema,
}, indent=2)
else:
out = json.dumps(schema, indent=2)
if args.output:
Path(args.output).write_text(out)
print(f"wrote {args.output}", file=sys.stderr)
else:
print(out)
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
FAQ
What is the difference between AEO, SEO and AI-SEO?
Traditional SEO optimizes Google/Bing rankings, AI-SEO optimizes AI search engines like Perplexity and AI Overviews, and AEO optimizes for LLM citation in ChatGPT, Claude, Gemini and Copilot answers.
Which content patterns get cited by LLMs?
Five: definitional content, comparative tables, step-by-step procedures, statistics with sources, and explained lists.