aeo-score
v1.0.3
Published
AEO Audit scoring engine — 14 product checks across 5 groups
Readme
@avada/aeo-score
Answer Engine Optimization scoring engine for Shopify stores. Evaluate how AI-ready your product pages are — 14 checks across 5 groups, decorator-driven, fully typed.
Why
Agentic commerce is here. Merchants don't just need SEO anymore — their content has to be parseable by ChatGPT, Perplexity, Google AI Overviews, and Shopify storefront MCP. The same product can rank well on Google yet be invisible to AI shopping agents because it lacks JSON-LD, answer-first intros, FAQ schema, or structured media.
@avada/aeo-score is the audit engine behind the Avada AEO Audit app. It runs 14 deterministic checks against a product's raw Shopify data and returns a weighted 0–100 score with actionable details.
Install
yarn add @avada/aeo-score
# or
npm install @avada/aeo-scoreRequires TypeScript ≥ 4.9 if you consume the types directly.
Quick start
import {AeoScoreService} from '@avada/aeo-score';
const service = new AeoScoreService();
const auditor = service.getAuditor('products');
const {score, checks} = auditor.auditItem({
id: 'gid://shopify/Product/1',
title: '7 Chakra Bracelet',
vendor: 'Company 123',
productType: 'Bracelet',
bodyHtml: '<p>Handmade bracelet, ideal for everyday wear…</p>',
variants: [{price: '43', sku: 'SKU-001'}],
images: [{url: '...', altText: 'Chakra bracelet front view'}],
hasProductSchema: true,
faqs: {questions: []}
});
console.log(score); // 65
console.log(checks[0]);
// { id: 'product_schema', passed: true, severity: 'critical',
// group: 'structured_data', action: {type: 'contact'}, ... }The 14 product checks
| # | ID | Severity | Weight | Group |
| - | -- | -------- | ------ | ----- |
| 1 | product_schema | critical | 12 | Structured data |
| 2 | product_brand | critical | 12 | Structured data |
| 3 | product_type | medium | 5 | Structured data |
| 4 | product_description | critical | 12 | Content quality |
| 5 | product_answer_first | high | 8 | Content quality |
| 6 | product_conversational | medium | 5 | Content quality |
| 7 | product_faq | high | 8 | FAQs & UGC |
| 8 | product_faq_schema | high | 8 | FAQs & UGC |
| 9 | product_reviews | low | 3 | FAQs & UGC |
| 10 | product_price | high | 8 | Commerce signals |
| 11 | product_variants | high | 8 | Commerce signals |
| 12 | product_sku | medium | 5 | Commerce signals |
| 13 | product_images | high | 8 | Media |
| 14 | product_alt_text | medium | 5 | Media |
Score formula: round(Σ passed.weight / Σ all.weight × 100), skipped checks excluded.
Architecture
┌──────────────────────────────────────────┐
│ AeoScoreService │
│ ├── auditors: Record<type, Auditor> │
│ ├── getAuditor(type) │
│ ├── calculateStats(items) │
│ └── getScoreLabel(score) │
└────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ ContentTypeAuditor<ProductData> │
│ ├── auditItem(data, ctx) │
│ ├── auditAll(items, ctx) │
│ └── categoryScore(scoredItems) │
└────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ BaseIssue<TData> (abstract) │
│ ├── config: IssueConfig (from decorator)│
│ ├── check(data, ctx): CheckResult │
│ └── toResult(data, ctx): CheckOutput │
└────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ @Issue(PRODUCT.X) │
│ class XxxIssue extends BaseIssue { … } │
└──────────────────────────────────────────┘Decorator-driven issues
Every issue is a class annotated with @Issue(config). Config lives in a single source of truth — src/constants/product.ts.
import {Issue, BaseIssue, PRODUCT} from '@avada/aeo-score';
@Issue(PRODUCT.DESCRIPTION)
export class DescriptionQualityIssue extends BaseIssue<ProductData> {
check(data: ProductData): CheckResult {
const words = countWords(data.bodyHtml);
return {
passed: words >= 50,
currentValue: `${words} words`,
detail: words >= 50 ? `${words} words — good length` : 'Too short…',
suggestion: words >= 50 ? null : 'Add material, dimensions…'
};
}
}The decorator attaches config as a static property:
@Issue(cfg) class X { … }
// ≡ X.__aeoIssueConfig = cfgNo reflect-metadata dependency — the implementation is dependency-free and works with both SWC and esbuild.
Writing a custom issue
import {BaseIssue, Issue, ContentTypeAuditor} from '@avada/aeo-score';
@Issue({
id: 'product_warranty',
label: 'Warranty information',
severity: 'low',
weight: 3,
group: 'content_quality',
action: {type: 'view', anchor: 'sec-description'}
})
class WarrantyIssue extends BaseIssue<ProductData> {
check(data) {
const passed = /warranty|guarantee/i.test(data.bodyHtml || '');
return {
passed,
currentValue: passed ? 'Found' : 'Missing',
detail: passed ? 'Warranty mentioned' : 'Add warranty terms',
suggestion: passed ? null : 'Mention warranty length in description'
};
}
}
// Register at runtime
service.getAuditor('products')!.addIssue(new WarrantyIssue());API reference
AeoScoreService
| Method | Description |
| ------ | ----------- |
| getAuditor(type) | Returns the ContentTypeAuditor for a content type. |
| calculateStats(itemsByType) | Aggregates {totalScanned, needAttention, passed, criticalIssues} across types. |
| getScoreLabel(score) | veryBad | poor | medium | good | excellent. |
ContentTypeAuditor<TData>
| Method | Description |
| ------ | ----------- |
| auditItem(data, ctx?) | {score, checks} for one item. |
| auditAll(items, ctx?) | Batch audit, sorted by score asc. |
| categoryScore(scored) | Average item score across items. |
| addIssue(issue) / removeIssue(id) | Runtime mutation. |
BaseIssue<TData>
Abstract class. Subclasses must implement check(data, ctx) and be decorated with @Issue(config).
PRODUCT
Typed config constants — PRODUCT.SCHEMA, PRODUCT.BRAND, PRODUCT.DESCRIPTION, ... 14 entries.
Types
All domain types are exported:
import type {
Severity, Group, ActionType, IssueConfig,
CheckResult, CheckOutput, AuditContext, ProductData
} from '@avada/aeo-score';Development
yarn # install
yarn dev # watch build
yarn build # emit dist/ (cjs + mjs + d.ts)
yarn typecheck # type-check onlyPublish
npm version patch # or minor/major
npm publish --access public # pushes to registry.avada.io (scoped publishConfig)License
Private — Avada internal use.
Roadmap
- [ ] Collection audits (6 checks)
- [ ] Article audits (8 checks)
- [ ] Page audits (7 checks)
- [ ] Rule-based
@Issue.DependsOn(...)for conditional checks - [ ] Output schema versioning via
@Issue({version: 2})
