aeorank 1.4.0 → 1.5.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/README.md +52 -2
- package/dist/cli.js +369 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +365 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -19
- package/dist/index.d.ts +40 -19
- package/dist/index.js +363 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -36,18 +36,26 @@ interface PitchMetric {
|
|
|
36
36
|
significance: string;
|
|
37
37
|
}
|
|
38
38
|
type PageCategory$1 = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'faq' | 'content';
|
|
39
|
-
interface
|
|
39
|
+
interface PageCriterionScore$1 {
|
|
40
|
+
criterion: string;
|
|
41
|
+
criterion_label: string;
|
|
42
|
+
score: number;
|
|
43
|
+
weight: number;
|
|
44
|
+
}
|
|
45
|
+
interface PageIssue {
|
|
40
46
|
check: string;
|
|
41
47
|
label: string;
|
|
42
48
|
severity: 'error' | 'warning' | 'info';
|
|
43
49
|
}
|
|
44
|
-
interface PageReview
|
|
50
|
+
interface PageReview {
|
|
45
51
|
url: string;
|
|
46
52
|
title: string;
|
|
47
53
|
category: PageCategory$1;
|
|
48
54
|
wordCount: number;
|
|
49
|
-
issues: PageIssue
|
|
50
|
-
strengths: PageIssue
|
|
55
|
+
issues: PageIssue[];
|
|
56
|
+
strengths: PageIssue[];
|
|
57
|
+
aeoScore?: number;
|
|
58
|
+
criterionScores?: PageCriterionScore$1[];
|
|
51
59
|
}
|
|
52
60
|
interface AuditData {
|
|
53
61
|
site: string;
|
|
@@ -61,7 +69,7 @@ interface AuditData {
|
|
|
61
69
|
opportunities: Deliverable[];
|
|
62
70
|
pitchNumbers: PitchMetric[];
|
|
63
71
|
bottomLine: string;
|
|
64
|
-
pagesReviewed?: PageReview
|
|
72
|
+
pagesReviewed?: PageReview[];
|
|
65
73
|
}
|
|
66
74
|
type AuditStatus = 'pass' | 'fail' | 'partial' | 'not_found';
|
|
67
75
|
type Priority = 'P0' | 'P1' | 'P2' | 'P3';
|
|
@@ -260,22 +268,35 @@ declare function generateBottomLine(score: number, opportunities: Deliverable[],
|
|
|
260
268
|
* Runs 12 deterministic checks on each crawled page (no LLM).
|
|
261
269
|
*/
|
|
262
270
|
|
|
263
|
-
interface PageIssue {
|
|
264
|
-
check: string;
|
|
265
|
-
label: string;
|
|
266
|
-
severity: 'error' | 'warning' | 'info';
|
|
267
|
-
}
|
|
268
|
-
interface PageReview {
|
|
269
|
-
url: string;
|
|
270
|
-
title: string;
|
|
271
|
-
category: PageCategory;
|
|
272
|
-
wordCount: number;
|
|
273
|
-
issues: PageIssue[];
|
|
274
|
-
strengths: PageIssue[];
|
|
275
|
-
}
|
|
276
271
|
declare function analyzePage(html: string, url: string, category: PageCategory): PageReview;
|
|
277
272
|
declare function analyzeAllPages(siteData: SiteData): PageReview[];
|
|
278
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Per-page AEO scoring.
|
|
276
|
+
* Evaluates 14 of 26 criteria that apply at individual page level.
|
|
277
|
+
* Produces a 0-100 AEO score per page.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface PageCriterionScore {
|
|
281
|
+
criterion: string;
|
|
282
|
+
criterion_label: string;
|
|
283
|
+
score: number;
|
|
284
|
+
weight: number;
|
|
285
|
+
}
|
|
286
|
+
interface PageScoreResult {
|
|
287
|
+
aeoScore: number;
|
|
288
|
+
criterionScores: PageCriterionScore[];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Score a single page against 14 AEO criteria.
|
|
292
|
+
* Returns a 0-100 AEO score and individual criterion scores.
|
|
293
|
+
*/
|
|
294
|
+
declare function scorePage(html: string, url?: string): PageScoreResult;
|
|
295
|
+
/**
|
|
296
|
+
* Score all crawled pages (homepage + blogSample).
|
|
297
|
+
*/
|
|
298
|
+
declare function scoreAllPages(siteData: SiteData): PageScoreResult[];
|
|
299
|
+
|
|
279
300
|
/**
|
|
280
301
|
* Extended page discovery for instant audit.
|
|
281
302
|
* Fetches additional pages beyond what prefetchSiteData provides,
|
|
@@ -456,4 +477,4 @@ interface ComparisonResult {
|
|
|
456
477
|
*/
|
|
457
478
|
declare function compare(domainA: string, domainB: string, options?: AuditOptions): Promise<ComparisonResult>;
|
|
458
479
|
|
|
459
|
-
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type ComparisonResult, type CrawlOptions, type CrawlResult, type CriterionComparison, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type
|
|
480
|
+
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type ComparisonResult, type CrawlOptions, type CrawlResult, type CriterionComparison, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type PageCriterionScore$1 as PageCriterionScore, type PageIssue, type PageReview, type PageScoreResult, 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, compare, crawlFullSite, detectParkedDomain, extractAllUrlsFromSitemap, extractContentPagesFromSitemap, extractInternalLinks, extractNavLinks, extractRawDataSummary, fetchMultiPageData, fetchWithHeadless, generateBottomLine, generateComparisonHtmlReport, generateHtmlReport, generateOpportunities, generatePitchNumbers, generateVerdict, inferCategory, isSpaShell, prefetchSiteData, scoreAllPages, scorePage, scoreToStatus };
|
package/dist/index.d.ts
CHANGED
|
@@ -36,18 +36,26 @@ interface PitchMetric {
|
|
|
36
36
|
significance: string;
|
|
37
37
|
}
|
|
38
38
|
type PageCategory$1 = 'homepage' | 'blog' | 'about' | 'pricing' | 'services' | 'contact' | 'team' | 'resources' | 'docs' | 'cases' | 'faq' | 'content';
|
|
39
|
-
interface
|
|
39
|
+
interface PageCriterionScore$1 {
|
|
40
|
+
criterion: string;
|
|
41
|
+
criterion_label: string;
|
|
42
|
+
score: number;
|
|
43
|
+
weight: number;
|
|
44
|
+
}
|
|
45
|
+
interface PageIssue {
|
|
40
46
|
check: string;
|
|
41
47
|
label: string;
|
|
42
48
|
severity: 'error' | 'warning' | 'info';
|
|
43
49
|
}
|
|
44
|
-
interface PageReview
|
|
50
|
+
interface PageReview {
|
|
45
51
|
url: string;
|
|
46
52
|
title: string;
|
|
47
53
|
category: PageCategory$1;
|
|
48
54
|
wordCount: number;
|
|
49
|
-
issues: PageIssue
|
|
50
|
-
strengths: PageIssue
|
|
55
|
+
issues: PageIssue[];
|
|
56
|
+
strengths: PageIssue[];
|
|
57
|
+
aeoScore?: number;
|
|
58
|
+
criterionScores?: PageCriterionScore$1[];
|
|
51
59
|
}
|
|
52
60
|
interface AuditData {
|
|
53
61
|
site: string;
|
|
@@ -61,7 +69,7 @@ interface AuditData {
|
|
|
61
69
|
opportunities: Deliverable[];
|
|
62
70
|
pitchNumbers: PitchMetric[];
|
|
63
71
|
bottomLine: string;
|
|
64
|
-
pagesReviewed?: PageReview
|
|
72
|
+
pagesReviewed?: PageReview[];
|
|
65
73
|
}
|
|
66
74
|
type AuditStatus = 'pass' | 'fail' | 'partial' | 'not_found';
|
|
67
75
|
type Priority = 'P0' | 'P1' | 'P2' | 'P3';
|
|
@@ -260,22 +268,35 @@ declare function generateBottomLine(score: number, opportunities: Deliverable[],
|
|
|
260
268
|
* Runs 12 deterministic checks on each crawled page (no LLM).
|
|
261
269
|
*/
|
|
262
270
|
|
|
263
|
-
interface PageIssue {
|
|
264
|
-
check: string;
|
|
265
|
-
label: string;
|
|
266
|
-
severity: 'error' | 'warning' | 'info';
|
|
267
|
-
}
|
|
268
|
-
interface PageReview {
|
|
269
|
-
url: string;
|
|
270
|
-
title: string;
|
|
271
|
-
category: PageCategory;
|
|
272
|
-
wordCount: number;
|
|
273
|
-
issues: PageIssue[];
|
|
274
|
-
strengths: PageIssue[];
|
|
275
|
-
}
|
|
276
271
|
declare function analyzePage(html: string, url: string, category: PageCategory): PageReview;
|
|
277
272
|
declare function analyzeAllPages(siteData: SiteData): PageReview[];
|
|
278
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Per-page AEO scoring.
|
|
276
|
+
* Evaluates 14 of 26 criteria that apply at individual page level.
|
|
277
|
+
* Produces a 0-100 AEO score per page.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface PageCriterionScore {
|
|
281
|
+
criterion: string;
|
|
282
|
+
criterion_label: string;
|
|
283
|
+
score: number;
|
|
284
|
+
weight: number;
|
|
285
|
+
}
|
|
286
|
+
interface PageScoreResult {
|
|
287
|
+
aeoScore: number;
|
|
288
|
+
criterionScores: PageCriterionScore[];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Score a single page against 14 AEO criteria.
|
|
292
|
+
* Returns a 0-100 AEO score and individual criterion scores.
|
|
293
|
+
*/
|
|
294
|
+
declare function scorePage(html: string, url?: string): PageScoreResult;
|
|
295
|
+
/**
|
|
296
|
+
* Score all crawled pages (homepage + blogSample).
|
|
297
|
+
*/
|
|
298
|
+
declare function scoreAllPages(siteData: SiteData): PageScoreResult[];
|
|
299
|
+
|
|
279
300
|
/**
|
|
280
301
|
* Extended page discovery for instant audit.
|
|
281
302
|
* Fetches additional pages beyond what prefetchSiteData provides,
|
|
@@ -456,4 +477,4 @@ interface ComparisonResult {
|
|
|
456
477
|
*/
|
|
457
478
|
declare function compare(domainA: string, domainB: string, options?: AuditOptions): Promise<ComparisonResult>;
|
|
458
479
|
|
|
459
|
-
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type ComparisonResult, type CrawlOptions, type CrawlResult, type CriterionComparison, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type
|
|
480
|
+
export { type AuditData, type AuditFinding, type AuditOptions, type AuditResult, type AuditStatus, CRITERION_LABELS, type ComparisonResult, type CrawlOptions, type CrawlResult, type CriterionComparison, type CriterionDetail, type CriterionResult, type Deliverable, type DetailedFinding, type FetchResult, type FindingSeverity, type FindingType, type HeadlessOptions, type ImpactLevel, type PageCategory$1 as PageCategory, type PageCriterionScore$1 as PageCriterionScore, type PageIssue, type PageReview, type PageScoreResult, 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, compare, crawlFullSite, detectParkedDomain, extractAllUrlsFromSitemap, extractContentPagesFromSitemap, extractInternalLinks, extractNavLinks, extractRawDataSummary, fetchMultiPageData, fetchWithHeadless, generateBottomLine, generateComparisonHtmlReport, generateHtmlReport, generateOpportunities, generatePitchNumbers, generateVerdict, inferCategory, isSpaShell, prefetchSiteData, scoreAllPages, scorePage, scoreToStatus };
|
package/dist/index.js
CHANGED
|
@@ -2523,12 +2523,359 @@ async function fetchMultiPageData(siteData, options) {
|
|
|
2523
2523
|
return added;
|
|
2524
2524
|
}
|
|
2525
2525
|
|
|
2526
|
+
// src/page-scorer.ts
|
|
2527
|
+
var PAGE_CRITERIA = {
|
|
2528
|
+
schema_markup: { weight: 0.15, label: "Schema.org Structured Data" },
|
|
2529
|
+
qa_content_format: { weight: 0.15, label: "Q&A Content Format" },
|
|
2530
|
+
clean_html: { weight: 0.1, label: "Clean, Crawlable HTML" },
|
|
2531
|
+
faq_section: { weight: 0.1, label: "FAQ Section Content" },
|
|
2532
|
+
original_data: { weight: 0.1, label: "Original Data & Expert Content" },
|
|
2533
|
+
query_answer_alignment: { weight: 0.08, label: "Query-Answer Alignment" },
|
|
2534
|
+
content_freshness: { weight: 0.07, label: "Content Freshness Signals" },
|
|
2535
|
+
table_list_extractability: { weight: 0.07, label: "Table & List Extractability" },
|
|
2536
|
+
direct_answer_density: { weight: 0.07, label: "Direct Answer Paragraphs" },
|
|
2537
|
+
semantic_html: { weight: 0.05, label: "Semantic HTML5 & Accessibility" },
|
|
2538
|
+
fact_density: { weight: 0.05, label: "Fact & Data Density" },
|
|
2539
|
+
definition_patterns: { weight: 0.04, label: "Definition Patterns" },
|
|
2540
|
+
canonical_url: { weight: 0.04, label: "Canonical URL Strategy" },
|
|
2541
|
+
visible_date_signal: { weight: 0.04, label: "Visible Date Signal" }
|
|
2542
|
+
};
|
|
2543
|
+
function extractJsonLdBlocks(html) {
|
|
2544
|
+
const blocks = [];
|
|
2545
|
+
const regex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
2546
|
+
let match;
|
|
2547
|
+
while ((match = regex.exec(html)) !== null) {
|
|
2548
|
+
blocks.push(match[1]);
|
|
2549
|
+
}
|
|
2550
|
+
return blocks;
|
|
2551
|
+
}
|
|
2552
|
+
function extractTypesFromJsonLd(blocks) {
|
|
2553
|
+
const types = /* @__PURE__ */ new Set();
|
|
2554
|
+
for (const block of blocks) {
|
|
2555
|
+
const typeMatches = block.match(/"@type"\s*:\s*"([^"]+)"/g) || [];
|
|
2556
|
+
for (const m of typeMatches) {
|
|
2557
|
+
const t = m.match(/"@type"\s*:\s*"([^"]+)"/);
|
|
2558
|
+
if (t) types.add(t[1]);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return types;
|
|
2562
|
+
}
|
|
2563
|
+
function getTextContent(html) {
|
|
2564
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
2565
|
+
}
|
|
2566
|
+
function extractQuestionHeadings2(html) {
|
|
2567
|
+
const headings = html.match(/<h[2-3][^>]*>([\s\S]*?)<\/h[2-3]>/gi) || [];
|
|
2568
|
+
const questions = [];
|
|
2569
|
+
for (const h of headings) {
|
|
2570
|
+
const text = h.replace(/<[^>]*>/g, "").trim();
|
|
2571
|
+
if (/\?$/.test(text) || /^(what|how|why|when|where|who|which|can|do|does|is|are|should|will)\b/i.test(text)) {
|
|
2572
|
+
questions.push(text);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return questions;
|
|
2576
|
+
}
|
|
2577
|
+
function countAnsweredQuestions(html) {
|
|
2578
|
+
const questions = extractQuestionHeadings2(html);
|
|
2579
|
+
if (questions.length === 0) return { total: 0, answered: 0 };
|
|
2580
|
+
let answered = 0;
|
|
2581
|
+
for (const q of questions) {
|
|
2582
|
+
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2583
|
+
const pattern = new RegExp(escaped + "[\\s\\S]*?</h[2-3]>\\s*<p[^>]*>([\\s\\S]*?)</p>", "i");
|
|
2584
|
+
const match = html.match(pattern);
|
|
2585
|
+
if (match && match[1].replace(/<[^>]*>/g, "").trim().length >= 20) {
|
|
2586
|
+
answered++;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return { total: questions.length, answered };
|
|
2590
|
+
}
|
|
2591
|
+
function cap(value, max) {
|
|
2592
|
+
return Math.min(value, max);
|
|
2593
|
+
}
|
|
2594
|
+
function scoreSchemaMarkup(html) {
|
|
2595
|
+
const blocks = extractJsonLdBlocks(html);
|
|
2596
|
+
if (blocks.length === 0) return 0;
|
|
2597
|
+
let score = 3;
|
|
2598
|
+
const types = extractTypesFromJsonLd(blocks);
|
|
2599
|
+
const knownTypes = [
|
|
2600
|
+
"Organization",
|
|
2601
|
+
"LocalBusiness",
|
|
2602
|
+
"Article",
|
|
2603
|
+
"FAQPage",
|
|
2604
|
+
"Product",
|
|
2605
|
+
"WebPage",
|
|
2606
|
+
"BreadcrumbList",
|
|
2607
|
+
"HowTo",
|
|
2608
|
+
"Person",
|
|
2609
|
+
"WebSite",
|
|
2610
|
+
"BlogPosting",
|
|
2611
|
+
"Service"
|
|
2612
|
+
];
|
|
2613
|
+
let knownCount = 0;
|
|
2614
|
+
for (const t of types) {
|
|
2615
|
+
if (knownTypes.includes(t)) knownCount++;
|
|
2616
|
+
}
|
|
2617
|
+
score += cap(knownCount * 2, 4);
|
|
2618
|
+
if (types.has("Organization") || types.has("LocalBusiness")) score += 2;
|
|
2619
|
+
if (types.has("FAQPage")) score += 1;
|
|
2620
|
+
return cap(score, 10);
|
|
2621
|
+
}
|
|
2622
|
+
function scoreQAFormat(html) {
|
|
2623
|
+
const questions = extractQuestionHeadings2(html);
|
|
2624
|
+
let score = 0;
|
|
2625
|
+
if (questions.length >= 10) score += 5;
|
|
2626
|
+
else if (questions.length >= 3) score += 3;
|
|
2627
|
+
else if (questions.length >= 1) score += 1;
|
|
2628
|
+
const { answered } = countAnsweredQuestions(html);
|
|
2629
|
+
if (answered >= 1) score += 3;
|
|
2630
|
+
const h1Matches = html.match(/<h1[\s>]/gi) || [];
|
|
2631
|
+
if (h1Matches.length === 1) score += 2;
|
|
2632
|
+
return cap(score, 10);
|
|
2633
|
+
}
|
|
2634
|
+
function scoreCleanHtml(html) {
|
|
2635
|
+
let score = 0;
|
|
2636
|
+
const semantics = ["<main", "<article", "<section"];
|
|
2637
|
+
let semCount = 0;
|
|
2638
|
+
for (const tag of semantics) {
|
|
2639
|
+
if (html.toLowerCase().includes(tag)) semCount++;
|
|
2640
|
+
}
|
|
2641
|
+
score += cap(semCount, 3);
|
|
2642
|
+
const h1Matches = html.match(/<h1[\s>]/gi) || [];
|
|
2643
|
+
if (h1Matches.length === 1) score += 2;
|
|
2644
|
+
const text = getTextContent(html);
|
|
2645
|
+
if (text.length > 500) score += 3;
|
|
2646
|
+
const hasTitle = /<title[^>]*>[^<]+<\/title>/i.test(html);
|
|
2647
|
+
const hasDesc = /<meta\s[^>]*name=["']description["'][^>]*content=["'][^"']+["']/i.test(html) || /<meta\s[^>]*content=["'][^"']+["'][^>]*name=["']description["']/i.test(html);
|
|
2648
|
+
if (hasTitle && hasDesc) score += 2;
|
|
2649
|
+
return cap(score, 10);
|
|
2650
|
+
}
|
|
2651
|
+
function scoreFaqSection(html) {
|
|
2652
|
+
let score = 0;
|
|
2653
|
+
const lowerHtml = html.toLowerCase();
|
|
2654
|
+
if (/frequently\s*asked|faq/i.test(html)) score += 2;
|
|
2655
|
+
const blocks = extractJsonLdBlocks(html);
|
|
2656
|
+
const types = extractTypesFromJsonLd(blocks);
|
|
2657
|
+
if (types.has("FAQPage")) score += 3;
|
|
2658
|
+
const questions = extractQuestionHeadings2(html);
|
|
2659
|
+
if (questions.length >= 10) score += 1;
|
|
2660
|
+
if (/<details[\s>]/i.test(html) || /accordion|collapsible|toggle/i.test(lowerHtml)) score += 1;
|
|
2661
|
+
return cap(score, 10);
|
|
2662
|
+
}
|
|
2663
|
+
function scoreOriginalData(html) {
|
|
2664
|
+
let score = 0;
|
|
2665
|
+
const text = getTextContent(html);
|
|
2666
|
+
if (/\b(our (study|analysis|research|survey|data|findings))\b/i.test(text)) {
|
|
2667
|
+
score += 3;
|
|
2668
|
+
} else if (/\d+(\.\d+)?%|\$[\d,.]+|\b\d{1,3}(,\d{3})+\b/.test(text)) {
|
|
2669
|
+
score += 1;
|
|
2670
|
+
}
|
|
2671
|
+
if (/\bcase\s+stud(y|ies)\b/i.test(text) && /\d+(\.\d+)?%|\$[\d,.]+/.test(text)) {
|
|
2672
|
+
score += 3;
|
|
2673
|
+
} else if (/\bcase\s+stud(y|ies)\b/i.test(text)) {
|
|
2674
|
+
score += 1;
|
|
2675
|
+
}
|
|
2676
|
+
if (/\baccording\s+to\b|\bexpert|\b(Ph\.?D|MD|professor|analyst|researcher)\b/i.test(text)) {
|
|
2677
|
+
score += 2;
|
|
2678
|
+
}
|
|
2679
|
+
if (/href=["'][^"']*\/blog\b/i.test(html)) {
|
|
2680
|
+
score += 2;
|
|
2681
|
+
}
|
|
2682
|
+
return cap(score, 10);
|
|
2683
|
+
}
|
|
2684
|
+
function scoreQueryAnswerAlignment(html) {
|
|
2685
|
+
const { total, answered } = countAnsweredQuestions(html);
|
|
2686
|
+
if (total === 0) return 5;
|
|
2687
|
+
const ratio = answered / total;
|
|
2688
|
+
if (ratio >= 0.8) return 10;
|
|
2689
|
+
if (ratio >= 0.5) return 7;
|
|
2690
|
+
if (answered > 0) return 4;
|
|
2691
|
+
return 0;
|
|
2692
|
+
}
|
|
2693
|
+
function scoreContentFreshness(html) {
|
|
2694
|
+
let score = 0;
|
|
2695
|
+
const blocks = extractJsonLdBlocks(html);
|
|
2696
|
+
const allJsonLd = blocks.join(" ");
|
|
2697
|
+
if (/datePublished|dateModified/i.test(allJsonLd)) score += 3;
|
|
2698
|
+
const timeElements = html.match(/<time[\s>]/gi) || [];
|
|
2699
|
+
if (timeElements.length >= 2) score += 3;
|
|
2700
|
+
else if (timeElements.length === 1) score += 1;
|
|
2701
|
+
if (/<meta\s[^>]*property=["']article:(published_time|modified_time)["']/i.test(html)) score += 2;
|
|
2702
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2703
|
+
const yearPattern = new RegExp(`\\b(${currentYear}|${currentYear - 1})\\b`);
|
|
2704
|
+
if (yearPattern.test(html)) score += 2;
|
|
2705
|
+
return cap(score, 10);
|
|
2706
|
+
}
|
|
2707
|
+
function scoreTableListExtractability(html) {
|
|
2708
|
+
let score = 0;
|
|
2709
|
+
const tablesWithHeaders = html.match(/<table[\s\S]*?<th[\s>]/gi) || [];
|
|
2710
|
+
if (tablesWithHeaders.length >= 2) score += 4;
|
|
2711
|
+
else if (tablesWithHeaders.length === 1) score += 3;
|
|
2712
|
+
if (tablesWithHeaders.length === 0 && /<table[\s>]/i.test(html)) score += 1;
|
|
2713
|
+
if (/<ol[\s>]/i.test(html)) score += 2;
|
|
2714
|
+
if (/<ul[\s>]/i.test(html)) score += 2;
|
|
2715
|
+
const listItems = html.match(/<li[\s>]/gi) || [];
|
|
2716
|
+
if (listItems.length >= 10) score += 1;
|
|
2717
|
+
if (/<dl[\s>]/i.test(html)) score += 1;
|
|
2718
|
+
return cap(score, 10);
|
|
2719
|
+
}
|
|
2720
|
+
function scoreDirectAnswerDensity(html) {
|
|
2721
|
+
let score = 0;
|
|
2722
|
+
const { answered } = countAnsweredQuestions(html);
|
|
2723
|
+
if (answered >= 3) score += 6;
|
|
2724
|
+
else if (answered >= 1) score += 3;
|
|
2725
|
+
const paragraphs = html.match(/<p[^>]*>([\s\S]*?)<\/p>/gi) || [];
|
|
2726
|
+
let snippetCount = 0;
|
|
2727
|
+
for (const p of paragraphs) {
|
|
2728
|
+
const text = p.replace(/<[^>]*>/g, "").trim();
|
|
2729
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
2730
|
+
if (words >= 40 && words <= 150) snippetCount++;
|
|
2731
|
+
}
|
|
2732
|
+
if (snippetCount >= 3) score += 2;
|
|
2733
|
+
else if (snippetCount >= 1) score += 1;
|
|
2734
|
+
const directOpeners = getTextContent(html).match(/\b(yes|no|in short|the answer is|simply put|in summary)\b/gi) || [];
|
|
2735
|
+
if (directOpeners.length >= 2) score += 2;
|
|
2736
|
+
return cap(score, 10);
|
|
2737
|
+
}
|
|
2738
|
+
function scoreSemanticHtml(html) {
|
|
2739
|
+
let score = 0;
|
|
2740
|
+
const lowerHtml = html.toLowerCase();
|
|
2741
|
+
const elements = ["<main", "<article", "<time", "<nav", "<header", "<footer"];
|
|
2742
|
+
let count = 0;
|
|
2743
|
+
for (const el of elements) {
|
|
2744
|
+
if (lowerHtml.includes(el)) count++;
|
|
2745
|
+
}
|
|
2746
|
+
score += cap(Math.floor(count * 0.7), 4);
|
|
2747
|
+
const imgTags = html.match(/<img\s[^>]*>/gi) || [];
|
|
2748
|
+
if (imgTags.length > 0) {
|
|
2749
|
+
let withAlt = 0;
|
|
2750
|
+
for (const img of imgTags) {
|
|
2751
|
+
if (/\salt=["'][^"']*["']/i.test(img)) withAlt++;
|
|
2752
|
+
}
|
|
2753
|
+
if (withAlt / imgTags.length >= 0.8) score += 2;
|
|
2754
|
+
}
|
|
2755
|
+
if (/<html[^>]*\slang=["'][^"']+["']/i.test(html)) score += 2;
|
|
2756
|
+
if (/\baria-/i.test(html)) score += 2;
|
|
2757
|
+
return cap(score, 10);
|
|
2758
|
+
}
|
|
2759
|
+
function scoreFactDensity(html) {
|
|
2760
|
+
let score = 0;
|
|
2761
|
+
const text = getTextContent(html);
|
|
2762
|
+
const numericPatterns = text.match(/\d+(\.\d+)?%|\$[\d,.]+|\b\d{1,3}(,\d{3})+\b|\b\d+\s*(million|billion|thousand|users|customers|employees)\b/gi) || [];
|
|
2763
|
+
if (numericPatterns.length >= 6) score += 5;
|
|
2764
|
+
else if (numericPatterns.length >= 3) score += 3;
|
|
2765
|
+
else if (numericPatterns.length >= 1) score += 1;
|
|
2766
|
+
const years = /* @__PURE__ */ new Set();
|
|
2767
|
+
const yearMatches = text.match(/\b(19|20)\d{2}\b/g) || [];
|
|
2768
|
+
for (const y of yearMatches) years.add(y);
|
|
2769
|
+
if (years.size >= 2) score += 2;
|
|
2770
|
+
else if (years.size === 1) score += 1;
|
|
2771
|
+
if (/\baccording to\b|\bsource:\s|\bcited\b|\breported by\b/i.test(text)) score += 2;
|
|
2772
|
+
const units = text.match(/\b\d+\s*(kg|lb|miles|km|hours|minutes|days|months|years|GB|MB|TB)\b/gi) || [];
|
|
2773
|
+
if (units.length >= 2) score += 1;
|
|
2774
|
+
return cap(score, 10);
|
|
2775
|
+
}
|
|
2776
|
+
function scoreDefinitionPatterns(html) {
|
|
2777
|
+
let score = 0;
|
|
2778
|
+
const text = getTextContent(html);
|
|
2779
|
+
const defPatterns = text.match(/\b(is a|is an|refers to|defined as|means that|also known as|abbreviated as)\b/gi) || [];
|
|
2780
|
+
if (defPatterns.length >= 3) score += 5;
|
|
2781
|
+
else if (defPatterns.length >= 1) score += 3;
|
|
2782
|
+
const early = text.slice(0, 2e3);
|
|
2783
|
+
if (/\b(is a|is an|refers to|defined as)\b/i.test(early)) score += 2;
|
|
2784
|
+
if (/<dfn[\s>]/i.test(html) || /<abbr[\s>]/i.test(html)) score += 1;
|
|
2785
|
+
if (/<dl[\s>]/i.test(html) || /glossary/i.test(html)) score += 2;
|
|
2786
|
+
return cap(score, 10);
|
|
2787
|
+
}
|
|
2788
|
+
function scoreCanonicalUrl(html, url) {
|
|
2789
|
+
let score = 0;
|
|
2790
|
+
const canonicalMatch = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']+)["']/i) || html.match(/<link[^>]*href=["']([^"']+)["'][^>]*rel=["']canonical["']/i);
|
|
2791
|
+
if (!canonicalMatch) return 0;
|
|
2792
|
+
score += 4;
|
|
2793
|
+
const canonicalHref = canonicalMatch[1];
|
|
2794
|
+
if (url) {
|
|
2795
|
+
try {
|
|
2796
|
+
const canonicalUrl = new URL(canonicalHref, url);
|
|
2797
|
+
const pageUrl = new URL(url);
|
|
2798
|
+
if (canonicalUrl.pathname === pageUrl.pathname && canonicalUrl.hostname === pageUrl.hostname) {
|
|
2799
|
+
score += 3;
|
|
2800
|
+
}
|
|
2801
|
+
} catch {
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (canonicalHref.startsWith("https://")) score += 2;
|
|
2805
|
+
const allCanonicals = html.match(/<link[^>]*rel=["']canonical["'][^>]*>/gi) || [];
|
|
2806
|
+
if (allCanonicals.length === 1) score += 1;
|
|
2807
|
+
return cap(score, 10);
|
|
2808
|
+
}
|
|
2809
|
+
function scoreVisibleDateSignal(html) {
|
|
2810
|
+
let score = 0;
|
|
2811
|
+
const timeWithDatetime = html.match(/<time[^>]*datetime=["'][^"']+["'][^>]*>[^<]+<\/time>/gi) || [];
|
|
2812
|
+
if (timeWithDatetime.length > 0) score += 5;
|
|
2813
|
+
const blocks = extractJsonLdBlocks(html);
|
|
2814
|
+
const allJsonLd = blocks.join(" ");
|
|
2815
|
+
if (/datePublished|dateModified/i.test(allJsonLd)) score += 3;
|
|
2816
|
+
if (/<meta\s[^>]*property=["']article:(published_time|modified_time)["']/i.test(html)) score += 2;
|
|
2817
|
+
const modifiedMatch = allJsonLd.match(/"dateModified"\s*:\s*"([^"]+)"/i);
|
|
2818
|
+
if (modifiedMatch) {
|
|
2819
|
+
try {
|
|
2820
|
+
const modified = new Date(modifiedMatch[1]);
|
|
2821
|
+
const daysDiff = (Date.now() - modified.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2822
|
+
if (daysDiff <= 180) score += 1;
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
return cap(score, 10);
|
|
2827
|
+
}
|
|
2828
|
+
var SCORING_FUNCTIONS = {
|
|
2829
|
+
schema_markup: scoreSchemaMarkup,
|
|
2830
|
+
qa_content_format: scoreQAFormat,
|
|
2831
|
+
clean_html: scoreCleanHtml,
|
|
2832
|
+
faq_section: scoreFaqSection,
|
|
2833
|
+
original_data: scoreOriginalData,
|
|
2834
|
+
query_answer_alignment: scoreQueryAnswerAlignment,
|
|
2835
|
+
content_freshness: scoreContentFreshness,
|
|
2836
|
+
table_list_extractability: scoreTableListExtractability,
|
|
2837
|
+
direct_answer_density: scoreDirectAnswerDensity,
|
|
2838
|
+
semantic_html: scoreSemanticHtml,
|
|
2839
|
+
fact_density: scoreFactDensity,
|
|
2840
|
+
definition_patterns: scoreDefinitionPatterns,
|
|
2841
|
+
canonical_url: scoreCanonicalUrl,
|
|
2842
|
+
visible_date_signal: scoreVisibleDateSignal
|
|
2843
|
+
};
|
|
2844
|
+
function scorePage(html, url) {
|
|
2845
|
+
let totalWeight = 0;
|
|
2846
|
+
let weightedSum = 0;
|
|
2847
|
+
const criterionScores = [];
|
|
2848
|
+
for (const [criterion, { weight, label }] of Object.entries(PAGE_CRITERIA)) {
|
|
2849
|
+
const fn = SCORING_FUNCTIONS[criterion];
|
|
2850
|
+
const score = fn(html, url);
|
|
2851
|
+
criterionScores.push({ criterion, criterion_label: label, score, weight });
|
|
2852
|
+
weightedSum += score / 10 * weight * 100;
|
|
2853
|
+
totalWeight += weight;
|
|
2854
|
+
}
|
|
2855
|
+
const aeoScore = totalWeight === 0 ? 0 : Math.round(weightedSum / totalWeight);
|
|
2856
|
+
return { aeoScore, criterionScores };
|
|
2857
|
+
}
|
|
2858
|
+
function scoreAllPages(siteData) {
|
|
2859
|
+
const results = [];
|
|
2860
|
+
if (siteData.homepage) {
|
|
2861
|
+
const url = siteData.protocol ? `${siteData.protocol}://${siteData.domain}` : void 0;
|
|
2862
|
+
results.push(scorePage(siteData.homepage.text, url));
|
|
2863
|
+
}
|
|
2864
|
+
if (siteData.blogSample) {
|
|
2865
|
+
for (const page of siteData.blogSample) {
|
|
2866
|
+
const url = page.finalUrl || void 0;
|
|
2867
|
+
results.push(scorePage(page.text, url));
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return results;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2526
2873
|
// src/page-analyzer.ts
|
|
2527
2874
|
function extractTitle(html) {
|
|
2528
2875
|
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
2529
2876
|
return match ? match[1].replace(/\s+/g, " ").trim() : "";
|
|
2530
2877
|
}
|
|
2531
|
-
function
|
|
2878
|
+
function getTextContent2(html) {
|
|
2532
2879
|
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
2533
2880
|
}
|
|
2534
2881
|
function countWords(text) {
|
|
@@ -2681,7 +3028,7 @@ function checkHasQuestionHeadings(html) {
|
|
|
2681
3028
|
}
|
|
2682
3029
|
function analyzePage(html, url, category) {
|
|
2683
3030
|
const title = extractTitle(html);
|
|
2684
|
-
const textContent =
|
|
3031
|
+
const textContent = getTextContent2(html);
|
|
2685
3032
|
const wordCount = countWords(textContent);
|
|
2686
3033
|
const issues = [];
|
|
2687
3034
|
const strengths = [];
|
|
@@ -2707,7 +3054,8 @@ function analyzePage(html, url, category) {
|
|
|
2707
3054
|
for (const result of strengthChecks) {
|
|
2708
3055
|
if (result) strengths.push(result);
|
|
2709
3056
|
}
|
|
2710
|
-
|
|
3057
|
+
const { aeoScore, criterionScores } = scorePage(html, url);
|
|
3058
|
+
return { url, title, category, wordCount, issues, strengths, aeoScore, criterionScores };
|
|
2711
3059
|
}
|
|
2712
3060
|
function analyzeAllPages(siteData) {
|
|
2713
3061
|
const reviews = [];
|
|
@@ -2915,17 +3263,22 @@ function generateHtmlReport(result) {
|
|
|
2915
3263
|
<td>${escapeHtml(opp.description)}</td>
|
|
2916
3264
|
</tr>`;
|
|
2917
3265
|
}).join("\n");
|
|
2918
|
-
const
|
|
3266
|
+
const pagesReviewed = result.pagesReviewed || [];
|
|
3267
|
+
const pagesRows = pagesReviewed.map((page) => {
|
|
2919
3268
|
const issueCount = page.issues.length;
|
|
2920
3269
|
const strengthCount = page.strengths.length;
|
|
3270
|
+
const aeoDisplay = page.aeoScore != null ? `<span style="font-weight:600;color:${scoreColor(page.aeoScore)}">${page.aeoScore}</span>` : "-";
|
|
2921
3271
|
return `<tr>
|
|
2922
3272
|
<td>${escapeHtml(page.url)}</td>
|
|
2923
3273
|
<td>${escapeHtml(page.category)}</td>
|
|
2924
3274
|
<td>${page.wordCount}</td>
|
|
3275
|
+
<td>${aeoDisplay}</td>
|
|
2925
3276
|
<td>${issueCount}</td>
|
|
2926
3277
|
<td>${strengthCount}</td>
|
|
2927
3278
|
</tr>`;
|
|
2928
3279
|
}).join("\n");
|
|
3280
|
+
const scoredPages = pagesReviewed.filter((p) => p.aeoScore != null);
|
|
3281
|
+
const avgPageScore = scoredPages.length > 0 ? Math.round(scoredPages.reduce((sum, p) => sum + p.aeoScore, 0) / scoredPages.length) : null;
|
|
2929
3282
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2930
3283
|
return `<!DOCTYPE html>
|
|
2931
3284
|
<html lang="en">
|
|
@@ -2961,10 +3314,11 @@ function generateHtmlReport(result) {
|
|
|
2961
3314
|
</table>
|
|
2962
3315
|
` : ""}
|
|
2963
3316
|
|
|
2964
|
-
${
|
|
2965
|
-
<h2 class="section-title">Pages Reviewed (${
|
|
3317
|
+
${pagesReviewed.length > 0 ? `
|
|
3318
|
+
<h2 class="section-title">Pages Reviewed (${pagesReviewed.length})</h2>
|
|
3319
|
+
${avgPageScore != null ? `<div class="summary-box"><div class="summary-stat"><div class="num" style="color:${scoreColor(avgPageScore)}">${avgPageScore}</div><div class="label">Avg Page AEO Score</div></div></div>` : ""}
|
|
2966
3320
|
<table>
|
|
2967
|
-
<thead><tr><th>URL</th><th>Category</th><th>Words</th><th>Issues</th><th>Strengths</th></tr></thead>
|
|
3321
|
+
<thead><tr><th>URL</th><th>Category</th><th>Words</th><th>AEO Score</th><th>Issues</th><th>Strengths</th></tr></thead>
|
|
2968
3322
|
<tbody>${pagesRows}</tbody>
|
|
2969
3323
|
</table>
|
|
2970
3324
|
` : ""}
|
|
@@ -3157,6 +3511,8 @@ export {
|
|
|
3157
3511
|
inferCategory,
|
|
3158
3512
|
isSpaShell,
|
|
3159
3513
|
prefetchSiteData,
|
|
3514
|
+
scoreAllPages,
|
|
3515
|
+
scorePage,
|
|
3160
3516
|
scoreToStatus
|
|
3161
3517
|
};
|
|
3162
3518
|
//# sourceMappingURL=index.js.map
|