aeorank 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/cli.js +2626 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2584 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +334 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +2536 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AEORank type definitions.
|
|
3
|
+
* Inlined from @aeo/shared to keep the package zero-dependency.
|
|
4
|
+
*/
|
|
5
|
+
type Status = 'MISSING' | 'NEARLY EMPTY' | 'POOR' | 'WEAK' | 'PARTIAL' | 'MODERATE' | 'GOOD' | 'STRONG';
|
|
6
|
+
type Severity = 'WORKING' | 'GOOD' | 'GOOD PATTERN' | 'PARTIAL' | 'MISSING' | 'ADD' | 'FIX' | 'FIX IMMEDIATELY' | 'REWRITE' | 'CONFUSING' | 'INCONSISTENT' | 'SPARSE' | 'PERFORMANCE' | 'CLUTTER' | 'PLATFORM LIMIT' | 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'BIG OPPORTUNITY' | 'AEO GOLDMINE' | 'AEO CORE' | 'CORE AEO' | 'AEO deliverable' | 'QUICK WIN' | 'MEASUREMENT';
|
|
7
|
+
type FindingType = 'Test' | 'Good' | 'Bad' | 'Missing' | 'Critical' | 'Issue' | 'Fix' | 'Exists' | 'Calc' | 'Present' | 'Note' | 'Current' | 'Volume' | 'Bonus' | 'Impact';
|
|
8
|
+
type ImpactLevel = 'QUICK WIN' | 'CRITICAL' | 'HIGH' | 'CORE AEO' | 'MEDIUM' | 'LOW' | 'MEASUREMENT' | 'BIG OPPORTUNITY';
|
|
9
|
+
interface ScoreCardItem {
|
|
10
|
+
id: number;
|
|
11
|
+
criterion: string;
|
|
12
|
+
score: number;
|
|
13
|
+
status: Status;
|
|
14
|
+
keyFindings: string;
|
|
15
|
+
}
|
|
16
|
+
interface DetailedFinding {
|
|
17
|
+
type: FindingType;
|
|
18
|
+
description: string;
|
|
19
|
+
severity: Severity;
|
|
20
|
+
}
|
|
21
|
+
interface CriterionDetail {
|
|
22
|
+
id: number;
|
|
23
|
+
name: string;
|
|
24
|
+
findings: DetailedFinding[];
|
|
25
|
+
}
|
|
26
|
+
interface Deliverable {
|
|
27
|
+
id: number;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
effort: string;
|
|
31
|
+
impact: ImpactLevel;
|
|
32
|
+
}
|
|
33
|
+
interface PitchMetric {
|
|
34
|
+
metric: string;
|
|
35
|
+
value: string;
|
|
36
|
+
significance: string;
|
|
37
|
+
}
|
|
38
|
+
type PageCategory$1 = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'content';
|
|
39
|
+
interface PageIssue$1 {
|
|
40
|
+
check: string;
|
|
41
|
+
label: string;
|
|
42
|
+
severity: 'error' | 'warning' | 'info';
|
|
43
|
+
}
|
|
44
|
+
interface PageReview$1 {
|
|
45
|
+
url: string;
|
|
46
|
+
title: string;
|
|
47
|
+
category: PageCategory$1;
|
|
48
|
+
wordCount: number;
|
|
49
|
+
issues: PageIssue$1[];
|
|
50
|
+
strengths: PageIssue$1[];
|
|
51
|
+
}
|
|
52
|
+
interface AuditData {
|
|
53
|
+
site: string;
|
|
54
|
+
auditDate: string;
|
|
55
|
+
auditor: string;
|
|
56
|
+
engine?: string;
|
|
57
|
+
overallScore: number;
|
|
58
|
+
verdict: string;
|
|
59
|
+
scorecard: ScoreCardItem[];
|
|
60
|
+
detailedFindings: CriterionDetail[];
|
|
61
|
+
opportunities: Deliverable[];
|
|
62
|
+
pitchNumbers: PitchMetric[];
|
|
63
|
+
bottomLine: string;
|
|
64
|
+
pagesReviewed?: PageReview$1[];
|
|
65
|
+
}
|
|
66
|
+
type AuditStatus = 'pass' | 'fail' | 'partial' | 'not_found';
|
|
67
|
+
type Priority = 'P0' | 'P1' | 'P2' | 'P3';
|
|
68
|
+
type FindingSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
69
|
+
interface AuditFinding {
|
|
70
|
+
severity: FindingSeverity;
|
|
71
|
+
detail: string;
|
|
72
|
+
fix?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Programmatic audit API.
|
|
77
|
+
* Runs the full 7-phase AEO audit pipeline and returns structured results.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
interface AuditOptions {
|
|
81
|
+
/** Skip Puppeteer SPA rendering (default: false) */
|
|
82
|
+
noHeadless?: boolean;
|
|
83
|
+
/** Homepage + blog only, skip extra page discovery (default: false) */
|
|
84
|
+
noMultiPage?: boolean;
|
|
85
|
+
/** Fetch timeout in ms (default: 15000) */
|
|
86
|
+
timeout?: number;
|
|
87
|
+
}
|
|
88
|
+
interface AuditResult extends AuditData {
|
|
89
|
+
/** True if headless browser was used for SPA rendering */
|
|
90
|
+
renderedWithHeadless?: boolean;
|
|
91
|
+
/** Wall-clock seconds */
|
|
92
|
+
elapsed: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run a complete AEO audit on a domain.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* import { audit } from 'aeorank';
|
|
100
|
+
* const result = await audit('example.com');
|
|
101
|
+
* console.log(result.overallScore); // 0-100
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function audit(domain: string, options?: AuditOptions): Promise<AuditResult>;
|
|
105
|
+
|
|
106
|
+
interface CriterionResult {
|
|
107
|
+
criterion: string;
|
|
108
|
+
criterion_label: string;
|
|
109
|
+
score: number;
|
|
110
|
+
status: AuditStatus;
|
|
111
|
+
findings: AuditFinding[];
|
|
112
|
+
fix_priority: Priority;
|
|
113
|
+
}
|
|
114
|
+
type PageCategory = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'content';
|
|
115
|
+
interface FetchResult {
|
|
116
|
+
text: string;
|
|
117
|
+
status: number;
|
|
118
|
+
finalUrl?: string;
|
|
119
|
+
category?: PageCategory;
|
|
120
|
+
}
|
|
121
|
+
interface SiteData {
|
|
122
|
+
domain: string;
|
|
123
|
+
protocol: 'https' | 'http' | null;
|
|
124
|
+
homepage: FetchResult | null;
|
|
125
|
+
llmsTxt: FetchResult | null;
|
|
126
|
+
robotsTxt: FetchResult | null;
|
|
127
|
+
faqPage: FetchResult | null;
|
|
128
|
+
sitemapXml: FetchResult | null;
|
|
129
|
+
rssFeed: FetchResult | null;
|
|
130
|
+
aiTxt: FetchResult | null;
|
|
131
|
+
/** Set when homepage redirects to a different (non-brand) domain */
|
|
132
|
+
redirectedTo: string | null;
|
|
133
|
+
/** Set when homepage is a parked/for-sale/lost domain */
|
|
134
|
+
parkedReason: string | null;
|
|
135
|
+
/** Sampled blog/content pages from sitemap (up to 5) */
|
|
136
|
+
blogSample?: FetchResult[];
|
|
137
|
+
}
|
|
138
|
+
interface RawDataSummary {
|
|
139
|
+
domain: string;
|
|
140
|
+
protocol: 'https' | 'http' | null;
|
|
141
|
+
homepage_length: number;
|
|
142
|
+
homepage_text_length: number;
|
|
143
|
+
has_https: boolean;
|
|
144
|
+
llms_txt_status: number | null;
|
|
145
|
+
llms_txt_length: number;
|
|
146
|
+
robots_txt_status: number | null;
|
|
147
|
+
robots_txt_snippet: string;
|
|
148
|
+
robots_txt_ai_crawlers: string[];
|
|
149
|
+
robots_txt_blocked_crawlers: string[];
|
|
150
|
+
schema_types_found: string[];
|
|
151
|
+
schema_block_count: number;
|
|
152
|
+
faq_page_status: number | null;
|
|
153
|
+
faq_page_length: number;
|
|
154
|
+
sitemap_status: number | null;
|
|
155
|
+
internal_link_count: number;
|
|
156
|
+
external_link_count: number;
|
|
157
|
+
question_headings_count: number;
|
|
158
|
+
h1_count: number;
|
|
159
|
+
has_meta_description: boolean;
|
|
160
|
+
has_title: boolean;
|
|
161
|
+
has_phone: boolean;
|
|
162
|
+
has_address: boolean;
|
|
163
|
+
has_org_schema: boolean;
|
|
164
|
+
has_social_links: boolean;
|
|
165
|
+
semantic_elements_found: string[];
|
|
166
|
+
img_count: number;
|
|
167
|
+
img_with_alt_count: number;
|
|
168
|
+
has_lang_attr: boolean;
|
|
169
|
+
has_aria: boolean;
|
|
170
|
+
has_breadcrumbs: boolean;
|
|
171
|
+
has_nav: boolean;
|
|
172
|
+
has_footer: boolean;
|
|
173
|
+
has_case_studies: boolean;
|
|
174
|
+
has_statistics: boolean;
|
|
175
|
+
has_expert_attribution: boolean;
|
|
176
|
+
has_blog_section: boolean;
|
|
177
|
+
has_date_modified_schema: boolean;
|
|
178
|
+
time_element_count: number;
|
|
179
|
+
sitemap_url_count: number;
|
|
180
|
+
has_rss_feed: boolean;
|
|
181
|
+
table_count: number;
|
|
182
|
+
ordered_list_count: number;
|
|
183
|
+
unordered_list_count: number;
|
|
184
|
+
definition_pattern_count: number;
|
|
185
|
+
has_ai_txt: boolean;
|
|
186
|
+
has_person_schema: boolean;
|
|
187
|
+
fact_data_point_count: number;
|
|
188
|
+
has_canonical: boolean;
|
|
189
|
+
has_license_schema: boolean;
|
|
190
|
+
sitemap_recent_lastmod_count: number;
|
|
191
|
+
rendered_with_headless?: boolean;
|
|
192
|
+
has_speakable_schema: boolean;
|
|
193
|
+
speakable_selector_count: number;
|
|
194
|
+
blog_sample_count: number;
|
|
195
|
+
blog_sample_urls: string[];
|
|
196
|
+
blog_sample_schema_types: string[];
|
|
197
|
+
blog_sample_question_headings: number;
|
|
198
|
+
blog_sample_faq_schema_found: boolean;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Fetches all site data in parallel with HTTPS/HTTP fallback.
|
|
202
|
+
* Single entry point for all HTTP requests - no redundant fetches.
|
|
203
|
+
*/
|
|
204
|
+
declare function prefetchSiteData(domain: string): Promise<SiteData>;
|
|
205
|
+
declare function extractRawDataSummary(data: SiteData): RawDataSummary;
|
|
206
|
+
/**
|
|
207
|
+
* Run all 23 criteria checks using pre-fetched site data.
|
|
208
|
+
* All functions are synchronous (no HTTP calls) - data was already fetched.
|
|
209
|
+
*/
|
|
210
|
+
declare function auditSiteFromData(data: SiteData): CriterionResult[];
|
|
211
|
+
|
|
212
|
+
declare function calculateOverallScore(criteria: CriterionResult[]): number;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Shared scorecard building functions.
|
|
216
|
+
* Extracted from cli/pre-crawl.ts for reuse in instant-audit and other consumers.
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
declare const CRITERION_LABELS: Record<string, string>;
|
|
220
|
+
declare function scoreToStatus(score: number): Status;
|
|
221
|
+
declare function buildScorecard(results: CriterionResult[]): ScoreCardItem[];
|
|
222
|
+
declare function buildDetailedFindings(results: CriterionResult[]): CriterionDetail[];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Deterministic narrative generation from scorecard data.
|
|
226
|
+
* Produces verdict, opportunities, pitchNumbers, and bottomLine
|
|
227
|
+
* without any LLM calls - pure template-based generation.
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
declare function generateVerdict(score: number, scorecard: ScoreCardItem[], rawData: RawDataSummary, domain: string): string;
|
|
231
|
+
declare function generateOpportunities(scorecard: ScoreCardItem[], criterionResults: CriterionResult[]): Deliverable[];
|
|
232
|
+
declare function generatePitchNumbers(score: number, rawData: RawDataSummary, scorecard: ScoreCardItem[]): PitchMetric[];
|
|
233
|
+
declare function generateBottomLine(score: number, opportunities: Deliverable[], scorecard: ScoreCardItem[], domain: string): string;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Per-page analysis for instant audit.
|
|
237
|
+
* Runs 12 deterministic checks on each crawled page (no LLM).
|
|
238
|
+
*/
|
|
239
|
+
|
|
240
|
+
interface PageIssue {
|
|
241
|
+
check: string;
|
|
242
|
+
label: string;
|
|
243
|
+
severity: 'error' | 'warning' | 'info';
|
|
244
|
+
}
|
|
245
|
+
interface PageReview {
|
|
246
|
+
url: string;
|
|
247
|
+
title: string;
|
|
248
|
+
category: PageCategory;
|
|
249
|
+
wordCount: number;
|
|
250
|
+
issues: PageIssue[];
|
|
251
|
+
strengths: PageIssue[];
|
|
252
|
+
}
|
|
253
|
+
declare function analyzePage(html: string, url: string, category: PageCategory): PageReview;
|
|
254
|
+
declare function analyzeAllPages(siteData: SiteData): PageReview[];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extended page discovery for instant audit.
|
|
258
|
+
* Fetches additional pages beyond what prefetchSiteData provides,
|
|
259
|
+
* including nav-linked pages, common paths, and content pages from sitemap.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract internal page paths from <nav> elements in homepage HTML.
|
|
264
|
+
* Returns deduplicated absolute paths (e.g. ['/about', '/pricing']).
|
|
265
|
+
*/
|
|
266
|
+
declare function extractNavLinks(html: string, domain: string): string[];
|
|
267
|
+
/**
|
|
268
|
+
* Extract non-blog deep content pages from sitemap XML.
|
|
269
|
+
* Targets service pages, product pages, etc. (not blog/article posts).
|
|
270
|
+
*/
|
|
271
|
+
declare function extractContentPagesFromSitemap(sitemapText: string, domain: string, limit?: number): string[];
|
|
272
|
+
interface MultiPageOptions {
|
|
273
|
+
timeoutMs?: number;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fetch additional pages beyond what prefetchSiteData provides.
|
|
277
|
+
* Discovers pages from nav links + common path variants + sitemap content pages.
|
|
278
|
+
* All fetched pages are appended to siteData.blogSample so existing
|
|
279
|
+
* getCombinedHtml() and criteria checks pick them up automatically.
|
|
280
|
+
*
|
|
281
|
+
* Mutates siteData in place and returns the count of new pages added.
|
|
282
|
+
*/
|
|
283
|
+
declare function fetchMultiPageData(siteData: SiteData, options?: MultiPageOptions): Promise<number>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* SPA detection and headless Chromium rendering for pre-crawl.
|
|
287
|
+
*
|
|
288
|
+
* When a site returns a thin JS-only shell (e.g. React CRA, Vite SPA),
|
|
289
|
+
* the regular fetch() gets almost no text content, causing false low scores.
|
|
290
|
+
* This module detects those shells and re-renders them with Puppeteer.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
type RenderingMethod = 'server' | 'client-spa';
|
|
294
|
+
interface RenderingClassification {
|
|
295
|
+
method: RenderingMethod;
|
|
296
|
+
framework: string | null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Detect whether raw HTML is a thin SPA shell that needs client-side rendering.
|
|
300
|
+
* Both conditions required:
|
|
301
|
+
* 1. Visible text content < 500 chars (thin page)
|
|
302
|
+
* 2. At least one SPA framework indicator present
|
|
303
|
+
*/
|
|
304
|
+
declare function isSpaShell(html: string): boolean;
|
|
305
|
+
/**
|
|
306
|
+
* Classify a page's rendering method from its raw (non-headless) HTML.
|
|
307
|
+
* Returns the method ('server' | 'client-spa') and detected framework if any.
|
|
308
|
+
*/
|
|
309
|
+
declare function classifyRendering(html: string): RenderingClassification;
|
|
310
|
+
interface HeadlessOptions {
|
|
311
|
+
timeout?: number;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Render a URL with headless Chromium and return the fully-rendered HTML.
|
|
315
|
+
* Returns null if Puppeteer is not installed or any error occurs.
|
|
316
|
+
* The caller should fall back to the raw HTML in that case.
|
|
317
|
+
*/
|
|
318
|
+
declare function fetchWithHeadless(url: string, options?: HeadlessOptions): Promise<FetchResult | null>;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Parked domain detection.
|
|
322
|
+
* Vendored from @aeo/queue/redirect-check (pure functions, no network).
|
|
323
|
+
*/
|
|
324
|
+
interface ParkedDomainResult {
|
|
325
|
+
isParked: boolean;
|
|
326
|
+
reason?: string;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Detect if a page is a parked/lost/for-sale domain.
|
|
330
|
+
* Pure function - no network calls.
|
|
331
|
+
*/
|
|
332
|
+
declare function detectParkedDomain(bodySnippet: string): ParkedDomainResult;
|
|
333
|
+
|
|
334
|
+
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type PageIssue$1 as PageIssue, type PageReview$1 as PageReview, type ParkedDomainResult, type PitchMetric, type Priority, type RawDataSummary, type RenderingMethod, type ScoreCardItem, type Severity, type SiteData, type Status, analyzeAllPages, analyzePage, audit, auditSiteFromData, buildDetailedFindings, buildScorecard, calculateOverallScore, classifyRendering, detectParkedDomain, extractContentPagesFromSitemap, extractNavLinks, extractRawDataSummary, fetchMultiPageData, fetchWithHeadless, generateBottomLine, generateOpportunities, generatePitchNumbers, generateVerdict, isSpaShell, prefetchSiteData, scoreToStatus };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AEORank type definitions.
|
|
3
|
+
* Inlined from @aeo/shared to keep the package zero-dependency.
|
|
4
|
+
*/
|
|
5
|
+
type Status = 'MISSING' | 'NEARLY EMPTY' | 'POOR' | 'WEAK' | 'PARTIAL' | 'MODERATE' | 'GOOD' | 'STRONG';
|
|
6
|
+
type Severity = 'WORKING' | 'GOOD' | 'GOOD PATTERN' | 'PARTIAL' | 'MISSING' | 'ADD' | 'FIX' | 'FIX IMMEDIATELY' | 'REWRITE' | 'CONFUSING' | 'INCONSISTENT' | 'SPARSE' | 'PERFORMANCE' | 'CLUTTER' | 'PLATFORM LIMIT' | 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'BIG OPPORTUNITY' | 'AEO GOLDMINE' | 'AEO CORE' | 'CORE AEO' | 'AEO deliverable' | 'QUICK WIN' | 'MEASUREMENT';
|
|
7
|
+
type FindingType = 'Test' | 'Good' | 'Bad' | 'Missing' | 'Critical' | 'Issue' | 'Fix' | 'Exists' | 'Calc' | 'Present' | 'Note' | 'Current' | 'Volume' | 'Bonus' | 'Impact';
|
|
8
|
+
type ImpactLevel = 'QUICK WIN' | 'CRITICAL' | 'HIGH' | 'CORE AEO' | 'MEDIUM' | 'LOW' | 'MEASUREMENT' | 'BIG OPPORTUNITY';
|
|
9
|
+
interface ScoreCardItem {
|
|
10
|
+
id: number;
|
|
11
|
+
criterion: string;
|
|
12
|
+
score: number;
|
|
13
|
+
status: Status;
|
|
14
|
+
keyFindings: string;
|
|
15
|
+
}
|
|
16
|
+
interface DetailedFinding {
|
|
17
|
+
type: FindingType;
|
|
18
|
+
description: string;
|
|
19
|
+
severity: Severity;
|
|
20
|
+
}
|
|
21
|
+
interface CriterionDetail {
|
|
22
|
+
id: number;
|
|
23
|
+
name: string;
|
|
24
|
+
findings: DetailedFinding[];
|
|
25
|
+
}
|
|
26
|
+
interface Deliverable {
|
|
27
|
+
id: number;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
effort: string;
|
|
31
|
+
impact: ImpactLevel;
|
|
32
|
+
}
|
|
33
|
+
interface PitchMetric {
|
|
34
|
+
metric: string;
|
|
35
|
+
value: string;
|
|
36
|
+
significance: string;
|
|
37
|
+
}
|
|
38
|
+
type PageCategory$1 = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'content';
|
|
39
|
+
interface PageIssue$1 {
|
|
40
|
+
check: string;
|
|
41
|
+
label: string;
|
|
42
|
+
severity: 'error' | 'warning' | 'info';
|
|
43
|
+
}
|
|
44
|
+
interface PageReview$1 {
|
|
45
|
+
url: string;
|
|
46
|
+
title: string;
|
|
47
|
+
category: PageCategory$1;
|
|
48
|
+
wordCount: number;
|
|
49
|
+
issues: PageIssue$1[];
|
|
50
|
+
strengths: PageIssue$1[];
|
|
51
|
+
}
|
|
52
|
+
interface AuditData {
|
|
53
|
+
site: string;
|
|
54
|
+
auditDate: string;
|
|
55
|
+
auditor: string;
|
|
56
|
+
engine?: string;
|
|
57
|
+
overallScore: number;
|
|
58
|
+
verdict: string;
|
|
59
|
+
scorecard: ScoreCardItem[];
|
|
60
|
+
detailedFindings: CriterionDetail[];
|
|
61
|
+
opportunities: Deliverable[];
|
|
62
|
+
pitchNumbers: PitchMetric[];
|
|
63
|
+
bottomLine: string;
|
|
64
|
+
pagesReviewed?: PageReview$1[];
|
|
65
|
+
}
|
|
66
|
+
type AuditStatus = 'pass' | 'fail' | 'partial' | 'not_found';
|
|
67
|
+
type Priority = 'P0' | 'P1' | 'P2' | 'P3';
|
|
68
|
+
type FindingSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
69
|
+
interface AuditFinding {
|
|
70
|
+
severity: FindingSeverity;
|
|
71
|
+
detail: string;
|
|
72
|
+
fix?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Programmatic audit API.
|
|
77
|
+
* Runs the full 7-phase AEO audit pipeline and returns structured results.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
interface AuditOptions {
|
|
81
|
+
/** Skip Puppeteer SPA rendering (default: false) */
|
|
82
|
+
noHeadless?: boolean;
|
|
83
|
+
/** Homepage + blog only, skip extra page discovery (default: false) */
|
|
84
|
+
noMultiPage?: boolean;
|
|
85
|
+
/** Fetch timeout in ms (default: 15000) */
|
|
86
|
+
timeout?: number;
|
|
87
|
+
}
|
|
88
|
+
interface AuditResult extends AuditData {
|
|
89
|
+
/** True if headless browser was used for SPA rendering */
|
|
90
|
+
renderedWithHeadless?: boolean;
|
|
91
|
+
/** Wall-clock seconds */
|
|
92
|
+
elapsed: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run a complete AEO audit on a domain.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* import { audit } from 'aeorank';
|
|
100
|
+
* const result = await audit('example.com');
|
|
101
|
+
* console.log(result.overallScore); // 0-100
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function audit(domain: string, options?: AuditOptions): Promise<AuditResult>;
|
|
105
|
+
|
|
106
|
+
interface CriterionResult {
|
|
107
|
+
criterion: string;
|
|
108
|
+
criterion_label: string;
|
|
109
|
+
score: number;
|
|
110
|
+
status: AuditStatus;
|
|
111
|
+
findings: AuditFinding[];
|
|
112
|
+
fix_priority: Priority;
|
|
113
|
+
}
|
|
114
|
+
type PageCategory = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'content';
|
|
115
|
+
interface FetchResult {
|
|
116
|
+
text: string;
|
|
117
|
+
status: number;
|
|
118
|
+
finalUrl?: string;
|
|
119
|
+
category?: PageCategory;
|
|
120
|
+
}
|
|
121
|
+
interface SiteData {
|
|
122
|
+
domain: string;
|
|
123
|
+
protocol: 'https' | 'http' | null;
|
|
124
|
+
homepage: FetchResult | null;
|
|
125
|
+
llmsTxt: FetchResult | null;
|
|
126
|
+
robotsTxt: FetchResult | null;
|
|
127
|
+
faqPage: FetchResult | null;
|
|
128
|
+
sitemapXml: FetchResult | null;
|
|
129
|
+
rssFeed: FetchResult | null;
|
|
130
|
+
aiTxt: FetchResult | null;
|
|
131
|
+
/** Set when homepage redirects to a different (non-brand) domain */
|
|
132
|
+
redirectedTo: string | null;
|
|
133
|
+
/** Set when homepage is a parked/for-sale/lost domain */
|
|
134
|
+
parkedReason: string | null;
|
|
135
|
+
/** Sampled blog/content pages from sitemap (up to 5) */
|
|
136
|
+
blogSample?: FetchResult[];
|
|
137
|
+
}
|
|
138
|
+
interface RawDataSummary {
|
|
139
|
+
domain: string;
|
|
140
|
+
protocol: 'https' | 'http' | null;
|
|
141
|
+
homepage_length: number;
|
|
142
|
+
homepage_text_length: number;
|
|
143
|
+
has_https: boolean;
|
|
144
|
+
llms_txt_status: number | null;
|
|
145
|
+
llms_txt_length: number;
|
|
146
|
+
robots_txt_status: number | null;
|
|
147
|
+
robots_txt_snippet: string;
|
|
148
|
+
robots_txt_ai_crawlers: string[];
|
|
149
|
+
robots_txt_blocked_crawlers: string[];
|
|
150
|
+
schema_types_found: string[];
|
|
151
|
+
schema_block_count: number;
|
|
152
|
+
faq_page_status: number | null;
|
|
153
|
+
faq_page_length: number;
|
|
154
|
+
sitemap_status: number | null;
|
|
155
|
+
internal_link_count: number;
|
|
156
|
+
external_link_count: number;
|
|
157
|
+
question_headings_count: number;
|
|
158
|
+
h1_count: number;
|
|
159
|
+
has_meta_description: boolean;
|
|
160
|
+
has_title: boolean;
|
|
161
|
+
has_phone: boolean;
|
|
162
|
+
has_address: boolean;
|
|
163
|
+
has_org_schema: boolean;
|
|
164
|
+
has_social_links: boolean;
|
|
165
|
+
semantic_elements_found: string[];
|
|
166
|
+
img_count: number;
|
|
167
|
+
img_with_alt_count: number;
|
|
168
|
+
has_lang_attr: boolean;
|
|
169
|
+
has_aria: boolean;
|
|
170
|
+
has_breadcrumbs: boolean;
|
|
171
|
+
has_nav: boolean;
|
|
172
|
+
has_footer: boolean;
|
|
173
|
+
has_case_studies: boolean;
|
|
174
|
+
has_statistics: boolean;
|
|
175
|
+
has_expert_attribution: boolean;
|
|
176
|
+
has_blog_section: boolean;
|
|
177
|
+
has_date_modified_schema: boolean;
|
|
178
|
+
time_element_count: number;
|
|
179
|
+
sitemap_url_count: number;
|
|
180
|
+
has_rss_feed: boolean;
|
|
181
|
+
table_count: number;
|
|
182
|
+
ordered_list_count: number;
|
|
183
|
+
unordered_list_count: number;
|
|
184
|
+
definition_pattern_count: number;
|
|
185
|
+
has_ai_txt: boolean;
|
|
186
|
+
has_person_schema: boolean;
|
|
187
|
+
fact_data_point_count: number;
|
|
188
|
+
has_canonical: boolean;
|
|
189
|
+
has_license_schema: boolean;
|
|
190
|
+
sitemap_recent_lastmod_count: number;
|
|
191
|
+
rendered_with_headless?: boolean;
|
|
192
|
+
has_speakable_schema: boolean;
|
|
193
|
+
speakable_selector_count: number;
|
|
194
|
+
blog_sample_count: number;
|
|
195
|
+
blog_sample_urls: string[];
|
|
196
|
+
blog_sample_schema_types: string[];
|
|
197
|
+
blog_sample_question_headings: number;
|
|
198
|
+
blog_sample_faq_schema_found: boolean;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Fetches all site data in parallel with HTTPS/HTTP fallback.
|
|
202
|
+
* Single entry point for all HTTP requests - no redundant fetches.
|
|
203
|
+
*/
|
|
204
|
+
declare function prefetchSiteData(domain: string): Promise<SiteData>;
|
|
205
|
+
declare function extractRawDataSummary(data: SiteData): RawDataSummary;
|
|
206
|
+
/**
|
|
207
|
+
* Run all 23 criteria checks using pre-fetched site data.
|
|
208
|
+
* All functions are synchronous (no HTTP calls) - data was already fetched.
|
|
209
|
+
*/
|
|
210
|
+
declare function auditSiteFromData(data: SiteData): CriterionResult[];
|
|
211
|
+
|
|
212
|
+
declare function calculateOverallScore(criteria: CriterionResult[]): number;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Shared scorecard building functions.
|
|
216
|
+
* Extracted from cli/pre-crawl.ts for reuse in instant-audit and other consumers.
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
declare const CRITERION_LABELS: Record<string, string>;
|
|
220
|
+
declare function scoreToStatus(score: number): Status;
|
|
221
|
+
declare function buildScorecard(results: CriterionResult[]): ScoreCardItem[];
|
|
222
|
+
declare function buildDetailedFindings(results: CriterionResult[]): CriterionDetail[];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Deterministic narrative generation from scorecard data.
|
|
226
|
+
* Produces verdict, opportunities, pitchNumbers, and bottomLine
|
|
227
|
+
* without any LLM calls - pure template-based generation.
|
|
228
|
+
*/
|
|
229
|
+
|
|
230
|
+
declare function generateVerdict(score: number, scorecard: ScoreCardItem[], rawData: RawDataSummary, domain: string): string;
|
|
231
|
+
declare function generateOpportunities(scorecard: ScoreCardItem[], criterionResults: CriterionResult[]): Deliverable[];
|
|
232
|
+
declare function generatePitchNumbers(score: number, rawData: RawDataSummary, scorecard: ScoreCardItem[]): PitchMetric[];
|
|
233
|
+
declare function generateBottomLine(score: number, opportunities: Deliverable[], scorecard: ScoreCardItem[], domain: string): string;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Per-page analysis for instant audit.
|
|
237
|
+
* Runs 12 deterministic checks on each crawled page (no LLM).
|
|
238
|
+
*/
|
|
239
|
+
|
|
240
|
+
interface PageIssue {
|
|
241
|
+
check: string;
|
|
242
|
+
label: string;
|
|
243
|
+
severity: 'error' | 'warning' | 'info';
|
|
244
|
+
}
|
|
245
|
+
interface PageReview {
|
|
246
|
+
url: string;
|
|
247
|
+
title: string;
|
|
248
|
+
category: PageCategory;
|
|
249
|
+
wordCount: number;
|
|
250
|
+
issues: PageIssue[];
|
|
251
|
+
strengths: PageIssue[];
|
|
252
|
+
}
|
|
253
|
+
declare function analyzePage(html: string, url: string, category: PageCategory): PageReview;
|
|
254
|
+
declare function analyzeAllPages(siteData: SiteData): PageReview[];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extended page discovery for instant audit.
|
|
258
|
+
* Fetches additional pages beyond what prefetchSiteData provides,
|
|
259
|
+
* including nav-linked pages, common paths, and content pages from sitemap.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract internal page paths from <nav> elements in homepage HTML.
|
|
264
|
+
* Returns deduplicated absolute paths (e.g. ['/about', '/pricing']).
|
|
265
|
+
*/
|
|
266
|
+
declare function extractNavLinks(html: string, domain: string): string[];
|
|
267
|
+
/**
|
|
268
|
+
* Extract non-blog deep content pages from sitemap XML.
|
|
269
|
+
* Targets service pages, product pages, etc. (not blog/article posts).
|
|
270
|
+
*/
|
|
271
|
+
declare function extractContentPagesFromSitemap(sitemapText: string, domain: string, limit?: number): string[];
|
|
272
|
+
interface MultiPageOptions {
|
|
273
|
+
timeoutMs?: number;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fetch additional pages beyond what prefetchSiteData provides.
|
|
277
|
+
* Discovers pages from nav links + common path variants + sitemap content pages.
|
|
278
|
+
* All fetched pages are appended to siteData.blogSample so existing
|
|
279
|
+
* getCombinedHtml() and criteria checks pick them up automatically.
|
|
280
|
+
*
|
|
281
|
+
* Mutates siteData in place and returns the count of new pages added.
|
|
282
|
+
*/
|
|
283
|
+
declare function fetchMultiPageData(siteData: SiteData, options?: MultiPageOptions): Promise<number>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* SPA detection and headless Chromium rendering for pre-crawl.
|
|
287
|
+
*
|
|
288
|
+
* When a site returns a thin JS-only shell (e.g. React CRA, Vite SPA),
|
|
289
|
+
* the regular fetch() gets almost no text content, causing false low scores.
|
|
290
|
+
* This module detects those shells and re-renders them with Puppeteer.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
type RenderingMethod = 'server' | 'client-spa';
|
|
294
|
+
interface RenderingClassification {
|
|
295
|
+
method: RenderingMethod;
|
|
296
|
+
framework: string | null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Detect whether raw HTML is a thin SPA shell that needs client-side rendering.
|
|
300
|
+
* Both conditions required:
|
|
301
|
+
* 1. Visible text content < 500 chars (thin page)
|
|
302
|
+
* 2. At least one SPA framework indicator present
|
|
303
|
+
*/
|
|
304
|
+
declare function isSpaShell(html: string): boolean;
|
|
305
|
+
/**
|
|
306
|
+
* Classify a page's rendering method from its raw (non-headless) HTML.
|
|
307
|
+
* Returns the method ('server' | 'client-spa') and detected framework if any.
|
|
308
|
+
*/
|
|
309
|
+
declare function classifyRendering(html: string): RenderingClassification;
|
|
310
|
+
interface HeadlessOptions {
|
|
311
|
+
timeout?: number;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Render a URL with headless Chromium and return the fully-rendered HTML.
|
|
315
|
+
* Returns null if Puppeteer is not installed or any error occurs.
|
|
316
|
+
* The caller should fall back to the raw HTML in that case.
|
|
317
|
+
*/
|
|
318
|
+
declare function fetchWithHeadless(url: string, options?: HeadlessOptions): Promise<FetchResult | null>;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Parked domain detection.
|
|
322
|
+
* Vendored from @aeo/queue/redirect-check (pure functions, no network).
|
|
323
|
+
*/
|
|
324
|
+
interface ParkedDomainResult {
|
|
325
|
+
isParked: boolean;
|
|
326
|
+
reason?: string;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Detect if a page is a parked/lost/for-sale domain.
|
|
330
|
+
* Pure function - no network calls.
|
|
331
|
+
*/
|
|
332
|
+
declare function detectParkedDomain(bodySnippet: string): ParkedDomainResult;
|
|
333
|
+
|
|
334
|
+
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type PageIssue$1 as PageIssue, type PageReview$1 as PageReview, type ParkedDomainResult, type PitchMetric, type Priority, type RawDataSummary, type RenderingMethod, type ScoreCardItem, type Severity, type SiteData, type Status, analyzeAllPages, analyzePage, audit, auditSiteFromData, buildDetailedFindings, buildScorecard, calculateOverallScore, classifyRendering, detectParkedDomain, extractContentPagesFromSitemap, extractNavLinks, extractRawDataSummary, fetchMultiPageData, fetchWithHeadless, generateBottomLine, generateOpportunities, generatePitchNumbers, generateVerdict, isSpaShell, prefetchSiteData, scoreToStatus };
|