@rankcli/agent-runtime 0.0.15 → 0.0.16
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/dist/index.d.mts +43 -393
- package/dist/index.d.ts +43 -393
- package/dist/index.js +306 -6526
- package/dist/index.mjs +202 -6412
- package/package.json +6 -1
- package/scripts/build-deno.ts +0 -134
- package/src/analyzers/core-web-vitals-analyzer.test.ts +0 -236
- package/src/analyzers/core-web-vitals-analyzer.ts +0 -557
- package/src/analyzers/geo-analyzer.test.ts +0 -310
- package/src/analyzers/geo-analyzer.ts +0 -814
- package/src/analyzers/image-optimization-analyzer.test.ts +0 -145
- package/src/analyzers/image-optimization-analyzer.ts +0 -348
- package/src/analyzers/index.ts +0 -233
- package/src/analyzers/internal-linking-analyzer.test.ts +0 -141
- package/src/analyzers/internal-linking-analyzer.ts +0 -419
- package/src/analyzers/mobile-seo-analyzer.test.ts +0 -140
- package/src/analyzers/mobile-seo-analyzer.ts +0 -455
- package/src/analyzers/security-headers-analyzer.test.ts +0 -115
- package/src/analyzers/security-headers-analyzer.ts +0 -318
- package/src/analyzers/structured-data-analyzer.test.ts +0 -210
- package/src/analyzers/structured-data-analyzer.ts +0 -590
- package/src/audit/ai/analyzer.ts +0 -347
- package/src/audit/ai/index.ts +0 -29
- package/src/audit/ai/prompts/content-analysis.ts +0 -271
- package/src/audit/ai/types.ts +0 -179
- package/src/audit/checks/additional-checks.ts +0 -439
- package/src/audit/checks/agent-experience.ts +0 -108
- package/src/audit/checks/ai-citation-worthiness.ts +0 -399
- package/src/audit/checks/ai-content-structure.ts +0 -325
- package/src/audit/checks/ai-readiness.ts +0 -406
- package/src/audit/checks/anchor-text.ts +0 -179
- package/src/audit/checks/answer-conciseness.ts +0 -322
- package/src/audit/checks/asset-minification.ts +0 -270
- package/src/audit/checks/bing-optimization.ts +0 -206
- package/src/audit/checks/brand-mention-optimization.ts +0 -349
- package/src/audit/checks/caching-headers.ts +0 -305
- package/src/audit/checks/canonical-advanced.ts +0 -150
- package/src/audit/checks/canonical-domain.ts +0 -196
- package/src/audit/checks/citation-quality.ts +0 -358
- package/src/audit/checks/client-rendering.ts +0 -542
- package/src/audit/checks/color-contrast.ts +0 -342
- package/src/audit/checks/content-freshness.ts +0 -170
- package/src/audit/checks/content-science.ts +0 -589
- package/src/audit/checks/conversion-elements.ts +0 -526
- package/src/audit/checks/crawlability.ts +0 -220
- package/src/audit/checks/directory-listing.ts +0 -172
- package/src/audit/checks/dom-analysis.ts +0 -191
- package/src/audit/checks/dom-size.ts +0 -246
- package/src/audit/checks/duplicate-content.ts +0 -194
- package/src/audit/checks/eeat-signals.ts +0 -990
- package/src/audit/checks/entity-seo.ts +0 -396
- package/src/audit/checks/featured-snippet.ts +0 -473
- package/src/audit/checks/freshness-signals.ts +0 -443
- package/src/audit/checks/funnel-intent.ts +0 -463
- package/src/audit/checks/hreflang.ts +0 -174
- package/src/audit/checks/html-compliance.ts +0 -302
- package/src/audit/checks/image-dimensions.ts +0 -167
- package/src/audit/checks/images.ts +0 -160
- package/src/audit/checks/indexnow.ts +0 -275
- package/src/audit/checks/interactive-tools.ts +0 -475
- package/src/audit/checks/internal-link-graph.ts +0 -436
- package/src/audit/checks/keyword-analysis.ts +0 -239
- package/src/audit/checks/keyword-cannibalization.ts +0 -385
- package/src/audit/checks/keyword-placement.ts +0 -471
- package/src/audit/checks/links.ts +0 -273
- package/src/audit/checks/llms-txt.ts +0 -224
- package/src/audit/checks/local-seo.ts +0 -296
- package/src/audit/checks/mobile.ts +0 -167
- package/src/audit/checks/modern-images.ts +0 -226
- package/src/audit/checks/navboost-signals.ts +0 -395
- package/src/audit/checks/on-page.ts +0 -209
- package/src/audit/checks/page-resources.ts +0 -285
- package/src/audit/checks/pagination.ts +0 -180
- package/src/audit/checks/performance.ts +0 -153
- package/src/audit/checks/platform-presence.ts +0 -580
- package/src/audit/checks/rag-chunk-readiness.test.ts +0 -159
- package/src/audit/checks/rag-chunk-readiness.ts +0 -163
- package/src/audit/checks/redirect-analysis.ts +0 -153
- package/src/audit/checks/redirect-chain.ts +0 -389
- package/src/audit/checks/resource-hints.ts +0 -420
- package/src/audit/checks/responsive-css.ts +0 -247
- package/src/audit/checks/responsive-images.ts +0 -396
- package/src/audit/checks/review-ecosystem.ts +0 -415
- package/src/audit/checks/robots-validation.ts +0 -373
- package/src/audit/checks/security-headers.ts +0 -188
- package/src/audit/checks/security.ts +0 -144
- package/src/audit/checks/serp-preview.ts +0 -251
- package/src/audit/checks/site-maturity.ts +0 -453
- package/src/audit/checks/social-meta.test.ts +0 -275
- package/src/audit/checks/social-meta.ts +0 -134
- package/src/audit/checks/soft-404.ts +0 -151
- package/src/audit/checks/structured-data.ts +0 -238
- package/src/audit/checks/tech-detection.ts +0 -496
- package/src/audit/checks/topical-clusters.ts +0 -435
- package/src/audit/checks/tracker-bloat.ts +0 -462
- package/src/audit/checks/tracking-verification.test.ts +0 -371
- package/src/audit/checks/tracking-verification.ts +0 -636
- package/src/audit/checks/url-safety.ts +0 -682
- package/src/audit/deno-entry.ts +0 -66
- package/src/audit/discovery/index.ts +0 -15
- package/src/audit/discovery/link-crawler.ts +0 -233
- package/src/audit/discovery/repo-routes.ts +0 -347
- package/src/audit/engine.ts +0 -633
- package/src/audit/fixes/index.ts +0 -209
- package/src/audit/fixes/social-meta-fixes.test.ts +0 -329
- package/src/audit/fixes/social-meta-fixes.ts +0 -463
- package/src/audit/index.ts +0 -74
- package/src/audit/runner.test.ts +0 -299
- package/src/audit/runner.ts +0 -130
- package/src/audit/types.ts +0 -1987
- package/src/content/featured-snippet.ts +0 -367
- package/src/content/generator.test.ts +0 -534
- package/src/content/generator.ts +0 -501
- package/src/content/headline.ts +0 -317
- package/src/content/index.ts +0 -62
- package/src/content/intent.ts +0 -258
- package/src/content/keyword-density.ts +0 -349
- package/src/content/readability.ts +0 -262
- package/src/executor.ts +0 -336
- package/src/fixer/framework-fixes.test.ts +0 -489
- package/src/fixer/framework-fixes.ts +0 -5287
- package/src/fixer/index.ts +0 -6
- package/src/fixer/schemas.ts +0 -971
- package/src/fixer.ts +0 -642
- package/src/frameworks/detector.test.ts +0 -248
- package/src/frameworks/detector.ts +0 -899
- package/src/frameworks/index.ts +0 -68
- package/src/frameworks/recipes/angular.yaml +0 -171
- package/src/frameworks/recipes/astro.yaml +0 -206
- package/src/frameworks/recipes/django.yaml +0 -180
- package/src/frameworks/recipes/laravel.yaml +0 -137
- package/src/frameworks/recipes/nextjs.yaml +0 -268
- package/src/frameworks/recipes/nuxt.yaml +0 -175
- package/src/frameworks/recipes/rails.yaml +0 -188
- package/src/frameworks/recipes/react.yaml +0 -202
- package/src/frameworks/recipes/sveltekit.yaml +0 -154
- package/src/frameworks/recipes/vue.yaml +0 -137
- package/src/frameworks/recipes/wordpress.yaml +0 -209
- package/src/frameworks/suggestion-engine.ts +0 -357
- package/src/geo/geo-content.test.ts +0 -305
- package/src/geo/geo-content.ts +0 -266
- package/src/geo/geo-history.test.ts +0 -473
- package/src/geo/geo-history.ts +0 -433
- package/src/geo/geo-tracker.test.ts +0 -359
- package/src/geo/geo-tracker.ts +0 -411
- package/src/geo/index.ts +0 -11
- package/src/geo/llm-citation-checker.ts +0 -188
- package/src/git/commit-helper.test.ts +0 -261
- package/src/git/commit-helper.ts +0 -329
- package/src/git/index.ts +0 -12
- package/src/git/pr-helper.test.ts +0 -284
- package/src/git/pr-helper.ts +0 -307
- package/src/index.ts +0 -76
- package/src/keywords/ai-keyword-engine.ts +0 -1062
- package/src/keywords/ai-summarizer.ts +0 -387
- package/src/keywords/ci-mode.ts +0 -555
- package/src/keywords/engine.ts +0 -359
- package/src/keywords/index.ts +0 -151
- package/src/keywords/llm-judge.ts +0 -357
- package/src/keywords/nlp-analysis.ts +0 -706
- package/src/keywords/prioritizer.ts +0 -295
- package/src/keywords/site-crawler.ts +0 -342
- package/src/keywords/sources/autocomplete.ts +0 -139
- package/src/keywords/sources/competitive-search.ts +0 -450
- package/src/keywords/sources/competitor-analysis.ts +0 -374
- package/src/keywords/sources/dataforseo.ts +0 -206
- package/src/keywords/sources/free-sources.ts +0 -294
- package/src/keywords/sources/gsc.ts +0 -123
- package/src/keywords/topic-grouping.ts +0 -327
- package/src/keywords/types.ts +0 -144
- package/src/keywords/wizard.ts +0 -457
- package/src/loader.ts +0 -40
- package/src/ranking/index.ts +0 -5
- package/src/ranking/serp-client.ts +0 -348
- package/src/ranking/tracker.ts +0 -380
- package/src/ranking/types.ts +0 -123
- package/src/reports/index.ts +0 -7
- package/src/reports/report-generator.test.ts +0 -293
- package/src/reports/report-generator.ts +0 -713
- package/src/scheduler/alerts.test.ts +0 -458
- package/src/scheduler/alerts.ts +0 -328
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/scheduled-audit.test.ts +0 -377
- package/src/scheduler/scheduled-audit.ts +0 -149
- package/src/test/integration-test.ts +0 -325
- package/src/tools/analyzer.ts +0 -373
- package/src/tools/crawl.ts +0 -293
- package/src/tools/files.ts +0 -301
- package/src/tools/h1-fixer.ts +0 -249
- package/src/tools/index.ts +0 -67
- package/src/tracking/github-action.ts +0 -326
- package/src/tracking/google-analytics.ts +0 -265
- package/src/tracking/index.ts +0 -45
- package/src/tracking/report-generator.ts +0 -386
- package/src/tracking/search-console.ts +0 -335
- package/src/types.ts +0 -148
- package/src/utils/http.ts +0 -302
- package/src/wasm-adapter.ts +0 -297
- package/src/wasm-entry.ts +0 -14
- package/tsconfig.json +0 -17
- package/tsup.wasm.config.ts +0 -26
- package/vitest.config.ts +0 -15
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
// NavBoost & User Signal Optimization
|
|
2
|
-
// Reference: Google Algorithm Leak 2024 + DOJ vs Google Antitrust Trial
|
|
3
|
-
// NavBoost uses memorized click data over 13 months
|
|
4
|
-
// "goodClicks", "badClicks", "lastLongestClicks" - confirmed ranking signals
|
|
5
|
-
// Bounce rate >70% + session <30 seconds = quality signal problem
|
|
6
|
-
|
|
7
|
-
import * as cheerio from 'cheerio';
|
|
8
|
-
import type { AuditIssue } from '../types.js';
|
|
9
|
-
|
|
10
|
-
export interface NavBoostSignalsData {
|
|
11
|
-
clickOptimization: {
|
|
12
|
-
hasClearTitle: boolean;
|
|
13
|
-
hasCompellingMetaDesc: boolean;
|
|
14
|
-
titleTruncated: boolean;
|
|
15
|
-
metaDescTruncated: boolean;
|
|
16
|
-
estimatedCTRPotential: 'high' | 'medium' | 'low';
|
|
17
|
-
};
|
|
18
|
-
engagementSignals: {
|
|
19
|
-
hasTableOfContents: boolean;
|
|
20
|
-
hasJumpLinks: boolean;
|
|
21
|
-
estimatedScrollDepth: 'deep' | 'medium' | 'shallow';
|
|
22
|
-
hasProgressIndicator: boolean;
|
|
23
|
-
hasStickyNavigation: boolean;
|
|
24
|
-
contentSections: number;
|
|
25
|
-
};
|
|
26
|
-
bounceRiskFactors: {
|
|
27
|
-
hasAboveFoldContent: boolean;
|
|
28
|
-
hasSlowLoadingIndicators: boolean;
|
|
29
|
-
hasPopupRisk: boolean;
|
|
30
|
-
hasIntrusiveAds: boolean;
|
|
31
|
-
contentMatchesTitle: boolean;
|
|
32
|
-
hasImmediateValue: boolean;
|
|
33
|
-
};
|
|
34
|
-
dwellTimeFactors: {
|
|
35
|
-
estimatedReadTime: number; // minutes
|
|
36
|
-
hasVideo: boolean;
|
|
37
|
-
hasInteractiveElements: boolean;
|
|
38
|
-
contentDepth: 'comprehensive' | 'moderate' | 'thin';
|
|
39
|
-
hasRelatedContent: boolean;
|
|
40
|
-
};
|
|
41
|
-
overallNavBoostScore: number; // 0-100
|
|
42
|
-
recommendations: string[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Analyze title and meta description for CTR optimization
|
|
47
|
-
*/
|
|
48
|
-
function analyzeClickOptimization($: cheerio.CheerioAPI): NavBoostSignalsData['clickOptimization'] {
|
|
49
|
-
const title = $('title').text().trim();
|
|
50
|
-
const metaDesc = $('meta[name="description"]').attr('content')?.trim() || '';
|
|
51
|
-
|
|
52
|
-
// Title analysis
|
|
53
|
-
const hasClearTitle = title.length > 10 && title.length <= 60;
|
|
54
|
-
const titleTruncated = title.length > 60;
|
|
55
|
-
|
|
56
|
-
// Meta description analysis
|
|
57
|
-
const hasCompellingMetaDesc = metaDesc.length >= 70 && metaDesc.length <= 160;
|
|
58
|
-
const metaDescTruncated = metaDesc.length > 160;
|
|
59
|
-
|
|
60
|
-
// CTR potential indicators
|
|
61
|
-
const hasPowerWords = /free|best|guide|how to|ultimate|proven|secret|new|exclusive/i.test(title + ' ' + metaDesc);
|
|
62
|
-
const hasNumbers = /\d+/.test(title);
|
|
63
|
-
const hasYear = /202[4-9]|203\d/.test(title + ' ' + metaDesc);
|
|
64
|
-
|
|
65
|
-
let ctrScore = 0;
|
|
66
|
-
if (hasClearTitle) ctrScore += 2;
|
|
67
|
-
if (hasCompellingMetaDesc) ctrScore += 2;
|
|
68
|
-
if (hasPowerWords) ctrScore += 1;
|
|
69
|
-
if (hasNumbers) ctrScore += 1;
|
|
70
|
-
if (hasYear) ctrScore += 1;
|
|
71
|
-
|
|
72
|
-
const estimatedCTRPotential = ctrScore >= 5 ? 'high' : ctrScore >= 3 ? 'medium' : 'low';
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
hasClearTitle,
|
|
76
|
-
hasCompellingMetaDesc,
|
|
77
|
-
titleTruncated,
|
|
78
|
-
metaDescTruncated,
|
|
79
|
-
estimatedCTRPotential,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Analyze engagement signals that affect NavBoost
|
|
85
|
-
*/
|
|
86
|
-
function analyzeEngagementSignals($: cheerio.CheerioAPI, html: string): NavBoostSignalsData['engagementSignals'] {
|
|
87
|
-
// Table of contents detection
|
|
88
|
-
const hasTableOfContents =
|
|
89
|
-
$('[class*="toc"], [id*="toc"], [class*="table-of-contents"], nav[aria-label*="content"]').length > 0 ||
|
|
90
|
-
$('a[href^="#"]').filter((_, el) => {
|
|
91
|
-
const text = $(el).text().toLowerCase();
|
|
92
|
-
return /^(\d+\.?\s*)?[a-z]/i.test(text) && text.length > 3;
|
|
93
|
-
}).length >= 3;
|
|
94
|
-
|
|
95
|
-
// Jump links (in-page navigation)
|
|
96
|
-
const hasJumpLinks = $('a[href^="#"]').length >= 3;
|
|
97
|
-
|
|
98
|
-
// Count content sections (H2s as proxy)
|
|
99
|
-
const contentSections = $('h2, h3').length;
|
|
100
|
-
|
|
101
|
-
// Scroll depth estimation based on content length
|
|
102
|
-
const bodyText = $('body').text();
|
|
103
|
-
const wordCount = bodyText.split(/\s+/).filter(w => w.length > 0).length;
|
|
104
|
-
const estimatedScrollDepth = wordCount > 2000 ? 'deep' : wordCount > 800 ? 'medium' : 'shallow';
|
|
105
|
-
|
|
106
|
-
// Progress indicator
|
|
107
|
-
const hasProgressIndicator =
|
|
108
|
-
$('[class*="progress"], [class*="reading-progress"], [role="progressbar"]').length > 0;
|
|
109
|
-
|
|
110
|
-
// Sticky navigation
|
|
111
|
-
const hasStickyNavigation =
|
|
112
|
-
$('[class*="sticky"], [class*="fixed"], [style*="position: sticky"], [style*="position: fixed"]').filter((_, el) => {
|
|
113
|
-
return $(el).find('nav, a').length > 0;
|
|
114
|
-
}).length > 0;
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
hasTableOfContents,
|
|
118
|
-
hasJumpLinks,
|
|
119
|
-
estimatedScrollDepth,
|
|
120
|
-
hasProgressIndicator,
|
|
121
|
-
hasStickyNavigation,
|
|
122
|
-
contentSections,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Analyze bounce risk factors
|
|
128
|
-
*/
|
|
129
|
-
function analyzeBounceRiskFactors($: cheerio.CheerioAPI, html: string): NavBoostSignalsData['bounceRiskFactors'] {
|
|
130
|
-
const title = $('title').text().toLowerCase();
|
|
131
|
-
const h1 = $('h1').first().text().toLowerCase();
|
|
132
|
-
const firstParagraph = $('p').first().text().toLowerCase();
|
|
133
|
-
|
|
134
|
-
// Above fold content check
|
|
135
|
-
const hasAboveFoldContent =
|
|
136
|
-
$('h1').length > 0 &&
|
|
137
|
-
($('p').first().text().length > 50 ||
|
|
138
|
-
$('main, article, .content').first().text().length > 100);
|
|
139
|
-
|
|
140
|
-
// Slow loading indicators (large images, heavy scripts)
|
|
141
|
-
const hasSlowLoadingIndicators =
|
|
142
|
-
$('script[src*="bundle"], script[src*="vendor"]').length > 5 ||
|
|
143
|
-
$('img:not([loading="lazy"])').length > 10;
|
|
144
|
-
|
|
145
|
-
// Popup risk
|
|
146
|
-
const hasPopupRisk =
|
|
147
|
-
html.includes('modal') ||
|
|
148
|
-
html.includes('popup') ||
|
|
149
|
-
html.includes('newsletter-overlay') ||
|
|
150
|
-
$('[class*="popup"], [class*="modal"]').length > 0;
|
|
151
|
-
|
|
152
|
-
// Intrusive ads
|
|
153
|
-
const hasIntrusiveAds =
|
|
154
|
-
$('[class*="ad-"], [id*="ad-"], [class*="advertisement"]').length > 3 ||
|
|
155
|
-
html.includes('interstitial');
|
|
156
|
-
|
|
157
|
-
// Content-title match (basic check)
|
|
158
|
-
const titleWords = title.split(/\s+/).filter(w => w.length > 3);
|
|
159
|
-
const matchingWords = titleWords.filter(w =>
|
|
160
|
-
h1.includes(w) || firstParagraph.includes(w)
|
|
161
|
-
);
|
|
162
|
-
const contentMatchesTitle = matchingWords.length >= Math.ceil(titleWords.length * 0.5);
|
|
163
|
-
|
|
164
|
-
// Immediate value (answer in first paragraph)
|
|
165
|
-
const hasImmediateValue =
|
|
166
|
-
firstParagraph.length > 100 ||
|
|
167
|
-
$('main p, article p').first().text().length > 100;
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
hasAboveFoldContent,
|
|
171
|
-
hasSlowLoadingIndicators,
|
|
172
|
-
hasPopupRisk,
|
|
173
|
-
hasIntrusiveAds,
|
|
174
|
-
contentMatchesTitle,
|
|
175
|
-
hasImmediateValue,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Analyze dwell time factors
|
|
181
|
-
*/
|
|
182
|
-
function analyzeDwellTimeFactors($: cheerio.CheerioAPI, html: string): NavBoostSignalsData['dwellTimeFactors'] {
|
|
183
|
-
const bodyText = $('main, article, .content, body').text();
|
|
184
|
-
const wordCount = bodyText.split(/\s+/).filter(w => w.length > 0).length;
|
|
185
|
-
|
|
186
|
-
// Estimated read time (200 words per minute average)
|
|
187
|
-
const estimatedReadTime = Math.ceil(wordCount / 200);
|
|
188
|
-
|
|
189
|
-
// Video presence
|
|
190
|
-
const hasVideo =
|
|
191
|
-
$('video, iframe[src*="youtube"], iframe[src*="vimeo"], [class*="video"]').length > 0;
|
|
192
|
-
|
|
193
|
-
// Interactive elements
|
|
194
|
-
const hasInteractiveElements =
|
|
195
|
-
$('form, [class*="calculator"], [class*="quiz"], [class*="interactive"], input, select').length > 0;
|
|
196
|
-
|
|
197
|
-
// Content depth
|
|
198
|
-
const contentDepth = wordCount > 2000 ? 'comprehensive' : wordCount > 800 ? 'moderate' : 'thin';
|
|
199
|
-
|
|
200
|
-
// Related content
|
|
201
|
-
const hasRelatedContent =
|
|
202
|
-
$('[class*="related"], [class*="recommended"], [class*="similar"]').length > 0 ||
|
|
203
|
-
$('a').filter((_, el) => {
|
|
204
|
-
const text = $(el).text().toLowerCase();
|
|
205
|
-
return text.includes('related') || text.includes('see also') || text.includes('read next');
|
|
206
|
-
}).length > 0;
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
estimatedReadTime,
|
|
210
|
-
hasVideo,
|
|
211
|
-
hasInteractiveElements,
|
|
212
|
-
contentDepth,
|
|
213
|
-
hasRelatedContent,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Calculate overall NavBoost optimization score
|
|
219
|
-
*/
|
|
220
|
-
function calculateNavBoostScore(
|
|
221
|
-
click: NavBoostSignalsData['clickOptimization'],
|
|
222
|
-
engagement: NavBoostSignalsData['engagementSignals'],
|
|
223
|
-
bounce: NavBoostSignalsData['bounceRiskFactors'],
|
|
224
|
-
dwell: NavBoostSignalsData['dwellTimeFactors']
|
|
225
|
-
): number {
|
|
226
|
-
let score = 50; // Start at baseline
|
|
227
|
-
|
|
228
|
-
// Click optimization (max +20)
|
|
229
|
-
if (click.estimatedCTRPotential === 'high') score += 20;
|
|
230
|
-
else if (click.estimatedCTRPotential === 'medium') score += 10;
|
|
231
|
-
if (click.titleTruncated) score -= 5;
|
|
232
|
-
if (click.metaDescTruncated) score -= 3;
|
|
233
|
-
|
|
234
|
-
// Engagement signals (max +20)
|
|
235
|
-
if (engagement.hasTableOfContents) score += 10;
|
|
236
|
-
if (engagement.hasJumpLinks) score += 5;
|
|
237
|
-
if (engagement.contentSections >= 5) score += 5;
|
|
238
|
-
|
|
239
|
-
// Bounce risk (max -20)
|
|
240
|
-
if (!bounce.hasAboveFoldContent) score -= 10;
|
|
241
|
-
if (bounce.hasPopupRisk) score -= 5;
|
|
242
|
-
if (bounce.hasIntrusiveAds) score -= 10;
|
|
243
|
-
if (!bounce.contentMatchesTitle) score -= 5;
|
|
244
|
-
if (bounce.hasImmediateValue) score += 5;
|
|
245
|
-
|
|
246
|
-
// Dwell time factors (max +20)
|
|
247
|
-
if (dwell.hasVideo) score += 10;
|
|
248
|
-
if (dwell.hasInteractiveElements) score += 5;
|
|
249
|
-
if (dwell.contentDepth === 'comprehensive') score += 5;
|
|
250
|
-
else if (dwell.contentDepth === 'thin') score -= 5;
|
|
251
|
-
if (dwell.hasRelatedContent) score += 5;
|
|
252
|
-
|
|
253
|
-
return Math.max(0, Math.min(100, score));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Main function: Analyze NavBoost optimization signals
|
|
258
|
-
*/
|
|
259
|
-
export function analyzeNavBoostSignals(
|
|
260
|
-
html: string,
|
|
261
|
-
url: string
|
|
262
|
-
): { issues: AuditIssue[]; data: NavBoostSignalsData } {
|
|
263
|
-
const $ = cheerio.load(html);
|
|
264
|
-
const issues: AuditIssue[] = [];
|
|
265
|
-
|
|
266
|
-
// Run all analyses
|
|
267
|
-
const clickOptimization = analyzeClickOptimization($);
|
|
268
|
-
const engagementSignals = analyzeEngagementSignals($, html);
|
|
269
|
-
const bounceRiskFactors = analyzeBounceRiskFactors($, html);
|
|
270
|
-
const dwellTimeFactors = analyzeDwellTimeFactors($, html);
|
|
271
|
-
|
|
272
|
-
// Calculate overall score
|
|
273
|
-
const overallNavBoostScore = calculateNavBoostScore(
|
|
274
|
-
clickOptimization,
|
|
275
|
-
engagementSignals,
|
|
276
|
-
bounceRiskFactors,
|
|
277
|
-
dwellTimeFactors
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
// Generate recommendations
|
|
281
|
-
const recommendations: string[] = [];
|
|
282
|
-
|
|
283
|
-
if (clickOptimization.estimatedCTRPotential !== 'high') {
|
|
284
|
-
recommendations.push('Optimize title/meta for higher CTR: use power words, numbers, or current year');
|
|
285
|
-
}
|
|
286
|
-
if (!engagementSignals.hasTableOfContents && engagementSignals.contentSections >= 5) {
|
|
287
|
-
recommendations.push('Add table of contents for long-form content to improve engagement');
|
|
288
|
-
}
|
|
289
|
-
if (bounceRiskFactors.hasPopupRisk) {
|
|
290
|
-
recommendations.push('Delay or remove popup modals to reduce bounce rate');
|
|
291
|
-
}
|
|
292
|
-
if (!dwellTimeFactors.hasRelatedContent) {
|
|
293
|
-
recommendations.push('Add related content section to increase session duration');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Generate issues
|
|
297
|
-
|
|
298
|
-
// Low CTR potential
|
|
299
|
-
if (clickOptimization.estimatedCTRPotential === 'low') {
|
|
300
|
-
issues.push({
|
|
301
|
-
code: 'LOW_CTR_POTENTIAL',
|
|
302
|
-
severity: 'warning',
|
|
303
|
-
category: 'on-page',
|
|
304
|
-
title: 'Title and meta description have low CTR potential',
|
|
305
|
-
description: 'The title and meta description lack compelling elements that drive clicks.',
|
|
306
|
-
impact: 'NavBoost uses click data as a ranking signal. Higher CTR improves rankings over time.',
|
|
307
|
-
howToFix: 'Add power words (free, best, ultimate), numbers, or current year to increase CTR.',
|
|
308
|
-
affectedUrls: [url],
|
|
309
|
-
details: {
|
|
310
|
-
ctrPotential: clickOptimization.estimatedCTRPotential,
|
|
311
|
-
titleTruncated: clickOptimization.titleTruncated,
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// High bounce risk
|
|
317
|
-
if (!bounceRiskFactors.hasAboveFoldContent || !bounceRiskFactors.hasImmediateValue) {
|
|
318
|
-
issues.push({
|
|
319
|
-
code: 'HIGH_BOUNCE_RISK',
|
|
320
|
-
severity: 'warning',
|
|
321
|
-
category: 'content',
|
|
322
|
-
title: 'Page has high bounce risk signals',
|
|
323
|
-
description: 'The page lacks above-fold content or immediate value for visitors.',
|
|
324
|
-
impact: 'NavBoost tracks bounce rate. >70% bounce with <30s sessions hurts rankings.',
|
|
325
|
-
howToFix: 'Add compelling content above the fold that delivers immediate value to visitors.',
|
|
326
|
-
affectedUrls: [url],
|
|
327
|
-
details: {
|
|
328
|
-
hasAboveFoldContent: bounceRiskFactors.hasAboveFoldContent,
|
|
329
|
-
hasImmediateValue: bounceRiskFactors.hasImmediateValue,
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Popup/interstitial risk
|
|
335
|
-
if (bounceRiskFactors.hasPopupRisk) {
|
|
336
|
-
issues.push({
|
|
337
|
-
code: 'POPUP_BOUNCE_RISK',
|
|
338
|
-
severity: 'notice',
|
|
339
|
-
category: 'content',
|
|
340
|
-
title: 'Popup/modal detected - potential bounce risk',
|
|
341
|
-
description: 'Popups detected that may cause visitors to leave immediately.',
|
|
342
|
-
impact: 'Intrusive interstitials increase bounce rate and hurt NavBoost signals.',
|
|
343
|
-
howToFix: 'Delay popups by 30+ seconds, or use exit-intent only. Avoid fullscreen overlays.',
|
|
344
|
-
affectedUrls: [url],
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Thin content with low dwell time
|
|
349
|
-
if (dwellTimeFactors.contentDepth === 'thin' && dwellTimeFactors.estimatedReadTime < 3) {
|
|
350
|
-
issues.push({
|
|
351
|
-
code: 'LOW_DWELL_TIME_CONTENT',
|
|
352
|
-
severity: 'notice',
|
|
353
|
-
category: 'content',
|
|
354
|
-
title: 'Thin content may result in low dwell time',
|
|
355
|
-
description: `Estimated read time is only ${dwellTimeFactors.estimatedReadTime} minute(s).`,
|
|
356
|
-
impact: 'Google tracks "lastLongestClicks" - longer sessions signal quality content.',
|
|
357
|
-
howToFix: 'Add comprehensive content, videos, or interactive elements to increase time on page.',
|
|
358
|
-
affectedUrls: [url],
|
|
359
|
-
details: {
|
|
360
|
-
estimatedReadTime: dwellTimeFactors.estimatedReadTime,
|
|
361
|
-
contentDepth: dwellTimeFactors.contentDepth,
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Long content without navigation aids
|
|
367
|
-
if (engagementSignals.estimatedScrollDepth === 'deep' && !engagementSignals.hasTableOfContents) {
|
|
368
|
-
issues.push({
|
|
369
|
-
code: 'LONG_CONTENT_NO_TOC',
|
|
370
|
-
severity: 'notice',
|
|
371
|
-
category: 'content',
|
|
372
|
-
title: 'Long content lacks table of contents',
|
|
373
|
-
description: 'Long-form content without navigation aids may cause users to abandon the page.',
|
|
374
|
-
impact: 'Table of contents improves engagement signals by helping users find relevant sections.',
|
|
375
|
-
howToFix: 'Add a table of contents with jump links to major sections.',
|
|
376
|
-
affectedUrls: [url],
|
|
377
|
-
details: {
|
|
378
|
-
contentSections: engagementSignals.contentSections,
|
|
379
|
-
scrollDepth: engagementSignals.estimatedScrollDepth,
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return {
|
|
385
|
-
issues,
|
|
386
|
-
data: {
|
|
387
|
-
clickOptimization,
|
|
388
|
-
engagementSignals,
|
|
389
|
-
bounceRiskFactors,
|
|
390
|
-
dwellTimeFactors,
|
|
391
|
-
overallNavBoostScore,
|
|
392
|
-
recommendations,
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import * as cheerio from 'cheerio';
|
|
2
|
-
import type { AuditIssue } from '../types.js';
|
|
3
|
-
import { ISSUE_DEFINITIONS } from '../types.js';
|
|
4
|
-
|
|
5
|
-
export interface OnPageData {
|
|
6
|
-
title?: string;
|
|
7
|
-
titleLength: number;
|
|
8
|
-
description?: string;
|
|
9
|
-
descriptionLength: number;
|
|
10
|
-
canonical?: string;
|
|
11
|
-
h1s: string[];
|
|
12
|
-
headings: { level: number; text: string }[];
|
|
13
|
-
wordCount: number;
|
|
14
|
-
textToHtmlRatio: number;
|
|
15
|
-
hasNoindex: boolean;
|
|
16
|
-
metaRobots?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function analyzeOnPage(html: string, url: string): { issues: AuditIssue[]; data: OnPageData } {
|
|
20
|
-
const issues: AuditIssue[] = [];
|
|
21
|
-
const $ = cheerio.load(html);
|
|
22
|
-
|
|
23
|
-
// Extract data
|
|
24
|
-
const title = $('title').text().trim();
|
|
25
|
-
const description = $('meta[name="description"]').attr('content')?.trim();
|
|
26
|
-
const canonical = $('link[rel="canonical"]').attr('href');
|
|
27
|
-
const metaRobots = $('meta[name="robots"]').attr('content');
|
|
28
|
-
|
|
29
|
-
// Check noindex
|
|
30
|
-
const hasNoindex = metaRobots?.toLowerCase().includes('noindex') || false;
|
|
31
|
-
|
|
32
|
-
// Extract headings
|
|
33
|
-
const h1s: string[] = [];
|
|
34
|
-
$('h1').each((_, el) => {
|
|
35
|
-
h1s.push($(el).text().trim());
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const headings: { level: number; text: string }[] = [];
|
|
39
|
-
$('h1, h2, h3, h4, h5, h6').each((_, el) => {
|
|
40
|
-
const level = parseInt(el.tagName.charAt(1), 10);
|
|
41
|
-
headings.push({ level, text: $(el).text().trim() });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Calculate word count (text content only)
|
|
45
|
-
const bodyText = $('body').text().replace(/\s+/g, ' ').trim();
|
|
46
|
-
const wordCount = bodyText.split(/\s+/).filter(w => w.length > 0).length;
|
|
47
|
-
|
|
48
|
-
// Calculate text-to-HTML ratio
|
|
49
|
-
const textLength = bodyText.length;
|
|
50
|
-
const htmlLength = html.length;
|
|
51
|
-
const textToHtmlRatio = htmlLength > 0 ? (textLength / htmlLength) * 100 : 0;
|
|
52
|
-
|
|
53
|
-
const data: OnPageData = {
|
|
54
|
-
title: title || undefined,
|
|
55
|
-
titleLength: title?.length || 0,
|
|
56
|
-
description,
|
|
57
|
-
descriptionLength: description?.length || 0,
|
|
58
|
-
canonical,
|
|
59
|
-
h1s,
|
|
60
|
-
headings,
|
|
61
|
-
wordCount,
|
|
62
|
-
textToHtmlRatio,
|
|
63
|
-
hasNoindex,
|
|
64
|
-
metaRobots,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// ==================== TITLE CHECKS ====================
|
|
68
|
-
if (!title) {
|
|
69
|
-
issues.push({
|
|
70
|
-
...ISSUE_DEFINITIONS.TITLE_MISSING,
|
|
71
|
-
affectedUrls: [url],
|
|
72
|
-
});
|
|
73
|
-
} else {
|
|
74
|
-
if (title.length < 30) {
|
|
75
|
-
issues.push({
|
|
76
|
-
...ISSUE_DEFINITIONS.TITLE_TOO_SHORT,
|
|
77
|
-
affectedUrls: [url],
|
|
78
|
-
details: { title, length: title.length },
|
|
79
|
-
});
|
|
80
|
-
} else if (title.length > 60) {
|
|
81
|
-
issues.push({
|
|
82
|
-
...ISSUE_DEFINITIONS.TITLE_TOO_LONG,
|
|
83
|
-
affectedUrls: [url],
|
|
84
|
-
details: { title, length: title.length },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ==================== META DESCRIPTION CHECKS ====================
|
|
90
|
-
if (!description) {
|
|
91
|
-
issues.push({
|
|
92
|
-
...ISSUE_DEFINITIONS.META_DESC_MISSING,
|
|
93
|
-
affectedUrls: [url],
|
|
94
|
-
});
|
|
95
|
-
} else {
|
|
96
|
-
if (description.length < 120) {
|
|
97
|
-
issues.push({
|
|
98
|
-
...ISSUE_DEFINITIONS.META_DESC_TOO_SHORT,
|
|
99
|
-
affectedUrls: [url],
|
|
100
|
-
details: { description, length: description.length },
|
|
101
|
-
});
|
|
102
|
-
} else if (description.length > 160) {
|
|
103
|
-
issues.push({
|
|
104
|
-
...ISSUE_DEFINITIONS.META_DESC_TOO_LONG,
|
|
105
|
-
affectedUrls: [url],
|
|
106
|
-
details: { description, length: description.length },
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ==================== CANONICAL CHECKS ====================
|
|
112
|
-
if (!canonical) {
|
|
113
|
-
issues.push({
|
|
114
|
-
...ISSUE_DEFINITIONS.CANONICAL_MISSING,
|
|
115
|
-
affectedUrls: [url],
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Check for multiple canonicals
|
|
120
|
-
const canonicalCount = $('link[rel="canonical"]').length;
|
|
121
|
-
if (canonicalCount > 1) {
|
|
122
|
-
issues.push({
|
|
123
|
-
...ISSUE_DEFINITIONS.MULTIPLE_CANONICALS,
|
|
124
|
-
affectedUrls: [url],
|
|
125
|
-
details: { count: canonicalCount },
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ==================== H1 CHECKS ====================
|
|
130
|
-
if (h1s.length === 0) {
|
|
131
|
-
issues.push({
|
|
132
|
-
...ISSUE_DEFINITIONS.H1_MISSING,
|
|
133
|
-
affectedUrls: [url],
|
|
134
|
-
});
|
|
135
|
-
} else if (h1s.length > 1) {
|
|
136
|
-
issues.push({
|
|
137
|
-
...ISSUE_DEFINITIONS.H1_MULTIPLE,
|
|
138
|
-
affectedUrls: [url],
|
|
139
|
-
details: { h1s, count: h1s.length },
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Check if H1 duplicates title
|
|
144
|
-
if (h1s.length === 1 && title && h1s[0].toLowerCase() === title.toLowerCase()) {
|
|
145
|
-
issues.push({
|
|
146
|
-
...ISSUE_DEFINITIONS.H1_DUPLICATE_OF_TITLE,
|
|
147
|
-
affectedUrls: [url],
|
|
148
|
-
details: { h1: h1s[0], title },
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check for missing H2 tags (important for content structure)
|
|
153
|
-
const h2Count = headings.filter(h => h.level === 2).length;
|
|
154
|
-
if (h2Count === 0 && wordCount > 100) {
|
|
155
|
-
// Only warn if there's enough content to warrant H2 headings
|
|
156
|
-
issues.push({
|
|
157
|
-
code: 'H2_MISSING',
|
|
158
|
-
severity: 'warning',
|
|
159
|
-
category: 'on-page',
|
|
160
|
-
title: 'No H2 headings found',
|
|
161
|
-
description: 'No H2 tags were found on the page. H2 headings help structure content and improve readability.',
|
|
162
|
-
impact: 'Without H2 headings, search engines and users may find it harder to understand content structure. H2s are important for featuring in search results.',
|
|
163
|
-
howToFix: 'Break your content into logical sections using H2 headings. Include relevant keywords naturally in your H2 tags.',
|
|
164
|
-
affectedUrls: [url],
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Check heading hierarchy
|
|
169
|
-
let previousLevel = 0;
|
|
170
|
-
for (const heading of headings) {
|
|
171
|
-
if (heading.level > previousLevel + 1 && previousLevel > 0) {
|
|
172
|
-
issues.push({
|
|
173
|
-
...ISSUE_DEFINITIONS.HEADING_SKIP,
|
|
174
|
-
affectedUrls: [url],
|
|
175
|
-
details: { from: previousLevel, to: heading.level },
|
|
176
|
-
});
|
|
177
|
-
break; // Only report once
|
|
178
|
-
}
|
|
179
|
-
previousLevel = heading.level;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ==================== CONTENT CHECKS ====================
|
|
183
|
-
if (wordCount < 300) {
|
|
184
|
-
issues.push({
|
|
185
|
-
...ISSUE_DEFINITIONS.THIN_CONTENT,
|
|
186
|
-
affectedUrls: [url],
|
|
187
|
-
details: { wordCount },
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (textToHtmlRatio < 10) {
|
|
192
|
-
issues.push({
|
|
193
|
-
...ISSUE_DEFINITIONS.LOW_TEXT_HTML_RATIO,
|
|
194
|
-
affectedUrls: [url],
|
|
195
|
-
details: { ratio: textToHtmlRatio.toFixed(1) + '%' },
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ==================== NOINDEX CHECK ====================
|
|
200
|
-
if (hasNoindex) {
|
|
201
|
-
issues.push({
|
|
202
|
-
...ISSUE_DEFINITIONS.NOINDEX_TAG,
|
|
203
|
-
affectedUrls: [url],
|
|
204
|
-
details: { metaRobots },
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return { issues, data };
|
|
209
|
-
}
|