Angeo AEO Audit
September 16, 2026 Β· View on GitHub
One CLI command that tells you exactly why ChatGPT, Gemini, Claude, and Perplexity aren't recommending your store β and how to fix it.
- π Project home: angeo.dev
- π¦ Source: github.com/angeo-dev/module-aeo-audit
- π Issues: github.com/angeo-dev/module-aeo-audit/issues
- π Full guide: Magento 2 AEO Guide 2026
Compatibility
| Component | Version |
|---|---|
| Magento Open Source | 2.4.6, 2.4.7, 2.4.8 |
| Adobe Commerce | 2.4.6, 2.4.7, 2.4.8 |
| Adobe Commerce Cloud | All current versions |
| PHP | 8.1 β 8.5 |
| Themes | Luma, HyvΓ€ |
Tested with: Magento Open Source 2.4.7-p3 + PHP 8.3 + HyvΓ€ 1.3.
What's new in v4.0.0
The evidence layer. Signals 1β15 audit configuration; v4 adds two signals
that audit reality: waf_reality (does the edge actually let the bots in that
robots.txt invites?) and ai_crawler_activity (which AI crawlers demonstrably
visited, from GDPR-safe evidence sources). Plus: purpose-classified bot
grading (blocking GPTBot is a licensing choice, blocking OAI-SearchBot is an
AEO failure), GitHub Actions CI with a Mage-OS installability job, i18n
dictionary, configurable cron schedule, vendor-neutral & switchable fix
hints, and a security fix in the CrUX checker (TLS verification re-enabled,
API key moved from URL to header, encrypted key now actually decrypted).
Full details in CHANGELOG.md.
What's new in v3.0.0
Major release β see CHANGELOG.md for the breaking-change migration guide if you have custom checkers.
20 signals (up from 9), reflecting the actual AEO landscape of 2026: AI shopping integrations, merchant policies, agentic commerce, and structured-data quality.
6 new checkers:
merchant_policiesβMerchantReturnPolicy+OfferShippingDetailsβ required by Google AI Mode and ChatGPT Shopping since Jan 2026organization_schemaβ brand entity in AI knowledge graphsucp_profileβ Universal Commerce Protocol (/.well-known/ucp), with built-in security check that detects leaked JWK private keysjsonld_qualityβ three-page schema breadth audit (homepage / category / product),WebSite+SearchAction,BreadcrumbList,ItemListwell_knownβ discovery matrix for/.well-known/{ucp,ai-plugin.json,security.txt,mcp}core_web_vitalsβ LCP / INP / CLS via Google CrUX API (free, opt-in with API key)
Refactored architecture (this is the BC-break):
- Shared
Service\HttpCacheβ eliminates duplicate fetches across checkers (hundreds of redundant HTTP requests on multi-store audits before, dozens now) Service\StoreUrlSamplerβ single source of truth for product / category / CMS URL sampling- New
--categoryand--fail-on-severityCLI flags for CI workflows - Per-checker exception isolation β slow or failing checkers no longer halt the audit run
Note on access-log monitoring: an
ai_bot_trafficchecker was prototyped during v3 development and excluded from the release after security review β it encouraged broad read access on/var/log/nginx/, didn't work on Cloud/containerised hosting, and was dominated by false positives behind edge caches. AI-bot traffic is better measured at the edge (Fastly/Cloudflare Analytics) or via APM (New Relic, Datadog) than inside a PHP module. See CHANGELOG.md "Considered and rejected" for the full rationale. Thelive_signalcategory remains inCheckerInterfacefor third-party modules with secure live-signal sources β notablyangeo/module-aeo-brand-visibility.
What it checks β 18 signals
Two layers since v4.0.0: configuration signals verify the store is set up for AI engines; the evidence layer verifies AI engines actually reach it.
| # | Signal | Code | Weight | Category | What it validates |
|---|---|---|---|---|---|
| 1 | robots.txt β AI bots | robots_txt | 1.0 | technical | 12 AI bots, syntax errors, versioned UAs, conflicting rules |
| 2 | llms.txt β content map | llms_txt | 1.0 | technical | llmstxt.org v2 structure, store-locale + currency match, cross-host links, empty blockquote, headings before the first H2, HTML links where mirrors exist |
| 3 | llms.jsonl β catalog | llms_jsonl | 0.75 | technical | JSON Lines validity, required fields, eCommerce fields |
| 4 | sitemap.xml | sitemap | 0.8 | technical | XML, lastmod, .gz, catalog disproportion |
| 5 | Product schema | product_schema | 1.0 | technical | JSON-LD on real product, offers, HyvΓ€ detection |
| 6 | Merchant policies β NEW | merchant_policies | 0.9 | technical | hasMerchantReturnPolicy, OfferShippingDetails, priceValidUntil, itemCondition |
| 7 | Organization schema β NEW | organization_schema | 0.8 | technical | Organization / OnlineStore on homepage, sameAs, logo |
| 8 | UCP profile β NEW | ucp_profile | 0.9 | technical | /.well-known/ucp, signing keys, leaked-private-key detection |
| 9 | AI product feed | ai_product_feed | 1.0 | feed | Feed file, /.well-known/ai-plugin.json, REST endpoint |
| 10 | JSON-LD quality β NEW | jsonld_quality | 0.7 | technical | Breadcrumb, ItemList, WebSite+SearchAction, duplicate schemas |
| 11 | Canonical + hreflang | canonical | 0.7 | technical | Canonical agrees with og:url + JSON-LD url; hreflang on multi-store |
| 12 | Open Graph | open_graph | 0.7 | technical | All 5 OG tags, description length |
| 13 | FAQ schema | faq_schema | 0.5 | technical | FAQPage JSON-LD on homepage or sampled CMS page |
| 14 | Well-known matrix β NEW | well_known | 0.5 | technical | ucp / ai-plugin.json / security.txt / mcp inventory |
| 15 | Core Web Vitals β | core_web_vitals | 0.5 | external_api | LCP / INP / CLS via Google CrUX (API key required) |
| 16 | WAF reality check β β | waf_reality | 0.9 | technical | Probes the edge with real AI crawler UAs β flags bots robots.txt allows but the WAF/CDN blocks (challenge pages detected even behind HTTP 200) |
| 17 | AI crawler activity β β | ai_crawler_activity | 0.5 | live_signal | Which AI crawlers actually visited, by class (search / training / fetch), from GDPR-safe evidence sources |
| 18 | A2A Agent Card β β β | agent_card | 0.6 | technical | /.well-known/agent-card.json β required when the UCP profile declares an a2a transport, informational otherwise; flags cards stranded at the pre-0.3 /.well-known/agent.json path |
| 19 | llms.txt v2 link relations β NEW | link_relations | 0.7 | technical | rel="alternate" type="text/markdown" + rel="describedby" in <head> or Link: header; follows both and fails on a declared-but-broken target; Link: header on the mirror; both page.html.md and page.md forms |
| 20 | agents.md β NEW | agents_md | 0.7 | technical | Delivery / returns / privacy actually reachable, linked policies, cross-reference to llms.txt, text/html shadowing, /sitemap_agentic_discovery.xml |
β = added in v3.0.0 Β· β β = added in v4.0.0 Β· β β β = added in v4.1.0.
The agent card is conditional on purpose
Almost no Magento store has opted into A2A, and scoring them all down for a
protocol they never adopted would make the number less useful, not more. So
agent_card asks the UCP profile first. If the store advertises an a2a
transport, the card is part of the contract and its absence is a FAIL. If
it does not, the signal passes with applicable: false in the details.
One case is flagged regardless: a card served only at /.well-known/agent.json,
the path A2A used before 0.3. A spec-compliant 1.0.0 client never looks there,
so the operator believes they have published a card that, to the protocol, does
not exist.
v4 grading philosophy: bots are judged by purpose
The bot catalog (BotRegistry) classifies every
AI agent as training (collects pages to train models), search
(indexes for answer engines β this traffic earns citations), or fetcher
(real-time page access for a user asking right now):
- Blocking a search crawler (OAI-SearchBot, PerplexityBot, Claude-SearchBot) fails the audit β the store disappears from AI answers.
- Blocking a training crawler (GPTBot, ClaudeBot, CCBot, Bytespider) is a respected licensing choice β reported, never punished. Opting out of model training does not remove a store from AI search.
- Opt-out tokens (Google-Extended, Applebot-Extended) never crawl anything themselves and are reported as conscious-choice notices.
The evidence layer
waf_reality and ai_crawler_activity corroborate each other:
| waf_reality | ai_crawler_activity | Diagnosis |
|---|---|---|
| edge blocks probe | zero hits | Real block β allow-list AI crawlers in your WAF/CDN bot management |
| edge blocks probe | healthy hits | Verified-bot program correctly rejecting UA spoofers β no action needed |
| consistent | zero hits | Store likely not yet discovered β work on feeds, llms.txt, citations |
| consistent | search hits | Fully AI-visible β |
Evidence sources for ai_crawler_activity (pluggable via
Angeo\AeoAudit\Api\BotHitSourceInterface):
-
Built-in instrumentation (default, zero-config): a frontend plugin counts AI bot requests at the PHP layer on every hosting model. Limitation reported honestly: requests served entirely from full-page cache never reach PHP and are not counted.
-
Webserver access log (opt-in): set an absolute path under Stores β Configuration β Angeo AEO β Live Signal. Only the trailing 8 MB is read; combined and JSON-lines formats are auto-detected. Never
chmodthe live log β grant targeted read access instead:# point-grant read to the PHP user (survives permission audits): setfacl -m u:www-data:r /var/log/nginx/access.log # β¦or copy bot-filtered lines on rotation (logrotate postrotate): grep -Ei 'GPTBot|OAI-SearchBot|ClaudeBot|Claude-SearchBot|PerplexityBot|Amazonbot|Applebot|meta-externalagent|CCBot|Bytespider' \ /var/log/nginx/access.log.1 > /var/log/ai-bots/access.logBehind a CDN the origin log under-counts (cache hits never reach the origin) β the report says so explicitly.
-
Your own adapter (CDN analytics APIs are the highest-fidelity source β they see cache hits too). Implement
BotHitSourceInterface, register via di.xml on the checker'ssourcesargument.
Privacy: only aggregates β bot code, class, store, date, count β are ever
persisted (angeo_aeo_bot_hit). No IPs, no URLs, no raw user agents, no log
lines. Retention (default 90 days) is enforced by cron. Absence of any
source is an informational note, never a score penalty.
Installation
composer require angeo/module-aeo-audit
bin/magento setup:upgrade
bin/magento cache:flush
For full coverage, install the companion modules:
composer require \
angeo/module-llms-txt \
angeo/module-rich-data \
angeo/module-openai-product-feed \
angeo/module-openai-product-feed-api \
angeo/module-ucp \
angeo/module-aeo-brand-visibility
CLI usage
# Audit all stores
bin/magento angeo:aeo:audit
# Specific store
bin/magento angeo:aeo:audit --store=en_us
# JSON output (for dashboards / CI)
bin/magento angeo:aeo:audit --format=json
# Markdown report to file
bin/magento angeo:aeo:audit --format=markdown --output=/var/www/html/aeo-report.md
# Fast technical-only checks (skip external APIs)
bin/magento angeo:aeo:audit --category=technical
# Run only external-API checks (Core Web Vitals + any third-party live signals)
bin/magento angeo:aeo:audit --category=external_api,live_signal
# Fail build if score below threshold
bin/magento angeo:aeo:audit --fail-on=80
# Fail build if any critical-severity check fails
bin/magento angeo:aeo:audit --fail-on-severity=critical
# Run without saving to DB (CI / read-only environments)
bin/magento angeo:aeo:audit --no-save
Sample output:
AEO Score: [ββββββββββββββββββββ] 81% β Good
β Pass: 12 β Warn: 3 β Fail: 1
Critical fixes needed:
β Install angeo/module-openai-product-feed and register at chatgpt.com/merchants
π‘ Fix with angeo modules:
composer require angeo/module-openai-product-feed angeo/module-openai-product-feed-api
composer require angeo/module-ucp
Configuration
Some checkers need configuration. All are accessed via: Stores β Configuration β Angeo AEO.
| Setting | Purpose |
|---|---|
| CrUX API Key | Required by core_web_vitals checker. Free key from console.cloud.google.com β enable the Chrome UX Report API. Stored encrypted. |
Admin UI
- Marketing β Angeo AEO β AEO Audit Results β full history grid
- Marketing β Angeo AEO β Score Trend β line chart of AEO score over time
- βΆ Run Audit Now button (on the Audit Results grid, Score Trend, and result
view pages) β triggers an on-demand audit. Since 3.1.0 this is a POST action
protected by the admin form key and the dedicated
Angeo_AeoAudit::run_auditACL permission; the old GET menu entry was removed.
Score interpretation
| Score | Label | Typical situation |
|---|---|---|
| 0β25% | Critical | Default Magento install. AI crawlers blocked. No schema. |
| 26β50% | Needs Improvement | Some fixes applied. Feed or merchant policies missing. |
| 51β75% | Needs Improvement | Core signals in place. UCP, ai-plugin.json, or hreflang missing. |
| 76β90% | Good | Strong foundation. Minor gaps in well-known or CWV. |
| 91β100% | Excellent | Full 2026 AEO compliance. |
Cron
Scheduled audit β default every Monday at 03:00 server time, configurable under Stores β Configuration β Angeo AEO β Scheduled Audit since v4.0.0. Results saved to DB (last 50 per store); expired bot-hit counters pruned per the retention setting.
bin/magento cron:run --group=default
For fast daily checks (without external APIs or log scans), schedule an
additional cron job calling the audit with --category=technical.
Extending with custom checks
Implement Angeo\AeoAudit\Api\CheckerInterface (or extend
Angeo\AeoAudit\Model\Checker\AbstractChecker, which provides HTTP cache,
URL sampling and JSON-LD parsing), and register via di.xml:
<type name="Angeo\AeoAudit\Model\AuditRunner">
<arguments>
<argument name="checkers" xsi:type="array">
<item name="my_check" xsi:type="object">Vendor\Module\Model\Checker\MyChecker</item>
</argument>
</arguments>
</type>
v3 interface:
public function getName(): string; // "My Custom Check"
public function getCode(): string; // "my_check"
public function getWeight(): float; // 0.0β1.0
public function getCategory(): string; // CheckerInterface::CATEGORY_*
public function getSeverity(): string; // CheckerInterface::SEVERITY_*
public function getFixCommand(): string; // "composer require vendor/fix-module" or ""
public function check(\Magento\Store\Api\Data\StoreInterface $store): CheckResult;
Migrating from v2? See CHANGELOG.md for the migration guide.
Running tests
vendor/bin/phpunit -c app/code/Angeo/AeoAudit/phpunit.xml
v3 ships with unit tests covering all 15 checkers, both services
(HttpCache, StoreUrlSampler), the AuditRunner, and the report value
objects.
Code quality
# Magento Coding Standard
vendor/bin/phpcs --standard=Magento2 \
--extensions=php,phtml --severity=10 \
app/code/Angeo/AeoAudit/
# PHPStan static analysis
vendor/bin/phpstan analyse -l 5 app/code/Angeo/AeoAudit/
The Angeo AI Visibility Suite
| Module | Signal | Purpose |
|---|---|---|
angeo/module-aeo-audit | β | This module β audit all 18 signals |
angeo/module-robots-txt-aeo | #1 | Inject AI bot rules into robots.txt |
angeo/module-llms-txt | #2, #3 | Generate llms.txt and llms.jsonl |
angeo/module-rich-data | #5, #6, #7, #13 | Product, Organization, FAQ JSON-LD + merchant policies |
angeo/module-openai-product-feed | #9 | ACP product feed for ChatGPT Shopping |
angeo/module-openai-product-feed-api | #9 | REST API β 6 ACP endpoints |
angeo/module-openai-instant-checkout | β | Agentic Commerce Protocol β instant checkout from ChatGPT |
angeo/module-ucp | #8 | Universal Commerce Protocol β /.well-known/ucp |
angeo/module-aeo-brand-visibility | (extends) | Live AI visibility across ChatGPT, Claude, Perplexity, Gemini, Groq |
Contributing
Issues and PRs welcome at github.com/angeo-dev/module-aeo-audit.
Before opening a PR:
- Run
vendor/bin/phpunit -c phpunit.xmlβ all tests must pass - Run
vendor/bin/phpcs --standard=Magento2β no MCS violations - Add tests for any new checker
License
MIT β see LICENSE
Made with care by Ievgenii Gryshkun β open-source contributions to the Magento + AI commerce ecosystem.