@rankcli/agent-runtime 0.0.1
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 +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
import type { AuditIssue } from '../types.js';
|
|
3
|
+
import { ISSUE_DEFINITIONS } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export interface AnchorTextData {
|
|
6
|
+
totalLinks: number;
|
|
7
|
+
internalLinks: number;
|
|
8
|
+
externalLinks: number;
|
|
9
|
+
emptyAnchors: number;
|
|
10
|
+
genericAnchors: number;
|
|
11
|
+
anchorDistribution: Record<string, number>;
|
|
12
|
+
topAnchors: { text: string; count: number }[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Generic anchor text patterns
|
|
16
|
+
const GENERIC_ANCHORS = new Set([
|
|
17
|
+
'click here',
|
|
18
|
+
'click',
|
|
19
|
+
'here',
|
|
20
|
+
'read more',
|
|
21
|
+
'more',
|
|
22
|
+
'learn more',
|
|
23
|
+
'see more',
|
|
24
|
+
'view more',
|
|
25
|
+
'continue',
|
|
26
|
+
'continue reading',
|
|
27
|
+
'link',
|
|
28
|
+
'this',
|
|
29
|
+
'this link',
|
|
30
|
+
'go',
|
|
31
|
+
'details',
|
|
32
|
+
'more details',
|
|
33
|
+
'more info',
|
|
34
|
+
'info',
|
|
35
|
+
'read',
|
|
36
|
+
'view',
|
|
37
|
+
'see',
|
|
38
|
+
'download',
|
|
39
|
+
'get it',
|
|
40
|
+
'get',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function analyzeAnchorText(
|
|
44
|
+
html: string,
|
|
45
|
+
url: string
|
|
46
|
+
): { issues: AuditIssue[]; data: AnchorTextData } {
|
|
47
|
+
const issues: AuditIssue[] = [];
|
|
48
|
+
const $ = cheerio.load(html);
|
|
49
|
+
|
|
50
|
+
const baseUrl = new URL(url);
|
|
51
|
+
const anchorCounts: Record<string, number> = {};
|
|
52
|
+
let totalLinks = 0;
|
|
53
|
+
let internalLinks = 0;
|
|
54
|
+
let externalLinks = 0;
|
|
55
|
+
let emptyAnchors = 0;
|
|
56
|
+
let genericAnchors = 0;
|
|
57
|
+
const emptyAnchorUrls: string[] = [];
|
|
58
|
+
const genericAnchorExamples: string[] = [];
|
|
59
|
+
|
|
60
|
+
$('a[href]').each((_, el) => {
|
|
61
|
+
const href = $(el).attr('href') || '';
|
|
62
|
+
const text = $(el).text().trim().toLowerCase();
|
|
63
|
+
const ariaLabel = $(el).attr('aria-label')?.trim();
|
|
64
|
+
const title = $(el).attr('title')?.trim();
|
|
65
|
+
|
|
66
|
+
// Skip non-navigation links
|
|
67
|
+
if (href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
totalLinks++;
|
|
72
|
+
|
|
73
|
+
// Determine if internal or external
|
|
74
|
+
try {
|
|
75
|
+
const linkUrl = new URL(href, url);
|
|
76
|
+
if (linkUrl.hostname === baseUrl.hostname) {
|
|
77
|
+
internalLinks++;
|
|
78
|
+
} else {
|
|
79
|
+
externalLinks++;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
internalLinks++; // Relative URLs are internal
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for empty anchor text
|
|
86
|
+
const hasAccessibleText = text.length > 0 || ariaLabel || title;
|
|
87
|
+
if (!hasAccessibleText) {
|
|
88
|
+
emptyAnchors++;
|
|
89
|
+
if (emptyAnchorUrls.length < 5) {
|
|
90
|
+
emptyAnchorUrls.push(href);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for generic anchor text
|
|
96
|
+
if (GENERIC_ANCHORS.has(text)) {
|
|
97
|
+
genericAnchors++;
|
|
98
|
+
if (genericAnchorExamples.length < 5) {
|
|
99
|
+
genericAnchorExamples.push(text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Track anchor text distribution
|
|
104
|
+
if (text.length > 0) {
|
|
105
|
+
anchorCounts[text] = (anchorCounts[text] || 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Calculate top anchors
|
|
110
|
+
const topAnchors = Object.entries(anchorCounts)
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.slice(0, 10)
|
|
113
|
+
.map(([text, count]) => ({ text, count }));
|
|
114
|
+
|
|
115
|
+
const data: AnchorTextData = {
|
|
116
|
+
totalLinks,
|
|
117
|
+
internalLinks,
|
|
118
|
+
externalLinks,
|
|
119
|
+
emptyAnchors,
|
|
120
|
+
genericAnchors,
|
|
121
|
+
anchorDistribution: anchorCounts,
|
|
122
|
+
topAnchors,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ==================== Issue Detection ====================
|
|
126
|
+
|
|
127
|
+
// Empty anchors
|
|
128
|
+
if (emptyAnchors > 0) {
|
|
129
|
+
issues.push({
|
|
130
|
+
...ISSUE_DEFINITIONS.ANCHOR_TEXT_EMPTY,
|
|
131
|
+
affectedUrls: [url],
|
|
132
|
+
details: {
|
|
133
|
+
count: emptyAnchors,
|
|
134
|
+
examples: emptyAnchorUrls,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Generic anchors (only flag if significant portion)
|
|
140
|
+
const genericRatio = totalLinks > 0 ? genericAnchors / totalLinks : 0;
|
|
141
|
+
if (genericAnchors >= 3 && genericRatio > 0.2) {
|
|
142
|
+
issues.push({
|
|
143
|
+
...ISSUE_DEFINITIONS.ANCHOR_TEXT_GENERIC,
|
|
144
|
+
affectedUrls: [url],
|
|
145
|
+
details: {
|
|
146
|
+
count: genericAnchors,
|
|
147
|
+
ratio: `${(genericRatio * 100).toFixed(1)}%`,
|
|
148
|
+
examples: genericAnchorExamples,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Over-optimized anchor text detection
|
|
154
|
+
// Check if any single anchor text is used too frequently
|
|
155
|
+
const overOptimizedAnchors: string[] = [];
|
|
156
|
+
for (const [text, count] of Object.entries(anchorCounts)) {
|
|
157
|
+
// Exclude very short or very long texts
|
|
158
|
+
if (text.length < 3 || text.length > 50) continue;
|
|
159
|
+
// Exclude common navigation items
|
|
160
|
+
if (['home', 'about', 'contact', 'blog', 'services', 'products'].includes(text)) continue;
|
|
161
|
+
|
|
162
|
+
// Flag if same anchor used more than 5 times and represents >15% of links
|
|
163
|
+
if (count > 5 && count / internalLinks > 0.15) {
|
|
164
|
+
overOptimizedAnchors.push(`"${text}" (${count} times)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (overOptimizedAnchors.length > 0) {
|
|
169
|
+
issues.push({
|
|
170
|
+
...ISSUE_DEFINITIONS.ANCHOR_TEXT_OVER_OPTIMIZED,
|
|
171
|
+
affectedUrls: [url],
|
|
172
|
+
details: {
|
|
173
|
+
overOptimized: overOptimizedAnchors,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { issues, data };
|
|
179
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer Conciseness Checks
|
|
3
|
+
*
|
|
4
|
+
* AI systems prefer succinct, quotable answers. Content that provides
|
|
5
|
+
* clear, direct answers to questions is more likely to be cited.
|
|
6
|
+
* This check analyzes answer quality and quotability.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as cheerio from 'cheerio';
|
|
10
|
+
import type { AuditIssue } from '../types.js';
|
|
11
|
+
|
|
12
|
+
export interface AnswerConcisenessData {
|
|
13
|
+
headings: {
|
|
14
|
+
total: number;
|
|
15
|
+
questions: number;
|
|
16
|
+
withDirectAnswers: number;
|
|
17
|
+
};
|
|
18
|
+
answers: {
|
|
19
|
+
averageFirstSentenceLength: number;
|
|
20
|
+
shortAnswersCount: number;
|
|
21
|
+
longAnswersCount: number;
|
|
22
|
+
quotableAnswers: string[];
|
|
23
|
+
};
|
|
24
|
+
definitions: {
|
|
25
|
+
count: number;
|
|
26
|
+
examples: string[];
|
|
27
|
+
};
|
|
28
|
+
keyTakeaways: {
|
|
29
|
+
hasSummary: boolean;
|
|
30
|
+
hasTLDR: boolean;
|
|
31
|
+
hasKeyPoints: boolean;
|
|
32
|
+
};
|
|
33
|
+
concisenessScore: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ideal answer lengths for AI
|
|
37
|
+
const IDEAL_ANSWER_LENGTH = { min: 40, max: 150 }; // Characters
|
|
38
|
+
const MAX_FIRST_SENTENCE = 200; // Characters
|
|
39
|
+
|
|
40
|
+
export function analyzeAnswerConciseness(
|
|
41
|
+
html: string,
|
|
42
|
+
url: string
|
|
43
|
+
): { issues: AuditIssue[]; data: AnswerConcisenessData } {
|
|
44
|
+
const issues: AuditIssue[] = [];
|
|
45
|
+
const $ = cheerio.load(html);
|
|
46
|
+
|
|
47
|
+
// Remove non-content elements
|
|
48
|
+
$('nav, footer, aside, script, style, noscript, header').remove();
|
|
49
|
+
|
|
50
|
+
const headings = $('h1, h2, h3, h4, h5, h6');
|
|
51
|
+
let totalHeadings = 0;
|
|
52
|
+
let questionHeadings = 0;
|
|
53
|
+
let headingsWithDirectAnswers = 0;
|
|
54
|
+
|
|
55
|
+
const firstSentenceLengths: number[] = [];
|
|
56
|
+
let shortAnswersCount = 0;
|
|
57
|
+
let longAnswersCount = 0;
|
|
58
|
+
const quotableAnswers: string[] = [];
|
|
59
|
+
|
|
60
|
+
// Question patterns
|
|
61
|
+
const questionPatterns = [
|
|
62
|
+
/\?$/,
|
|
63
|
+
/^(what|how|why|when|where|who|which|can|does|is|are|should|will|would|could)\s/i,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
headings.each((_, heading) => {
|
|
67
|
+
const $heading = $(heading);
|
|
68
|
+
const headingText = $heading.text().trim();
|
|
69
|
+
totalHeadings++;
|
|
70
|
+
|
|
71
|
+
// Check if heading is a question
|
|
72
|
+
const isQuestion = questionPatterns.some(p => p.test(headingText));
|
|
73
|
+
|
|
74
|
+
if (isQuestion) {
|
|
75
|
+
questionHeadings++;
|
|
76
|
+
|
|
77
|
+
// Get the content after this heading until the next heading
|
|
78
|
+
let $next = $heading.next();
|
|
79
|
+
let answerText = '';
|
|
80
|
+
|
|
81
|
+
while ($next.length && !$next.is('h1, h2, h3, h4, h5, h6')) {
|
|
82
|
+
if ($next.is('p')) {
|
|
83
|
+
answerText += $next.text().trim() + ' ';
|
|
84
|
+
}
|
|
85
|
+
$next = $next.next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (answerText) {
|
|
89
|
+
// Get first sentence
|
|
90
|
+
const firstSentenceMatch = answerText.match(/^[^.!?]+[.!?]/);
|
|
91
|
+
const firstSentence = firstSentenceMatch ? firstSentenceMatch[0].trim() : '';
|
|
92
|
+
|
|
93
|
+
if (firstSentence) {
|
|
94
|
+
firstSentenceLengths.push(firstSentence.length);
|
|
95
|
+
|
|
96
|
+
// Check if it's a direct answer (short, complete, starts definitively)
|
|
97
|
+
const isDirectAnswer =
|
|
98
|
+
firstSentence.length <= MAX_FIRST_SENTENCE &&
|
|
99
|
+
!firstSentence.toLowerCase().startsWith('well,') &&
|
|
100
|
+
!firstSentence.toLowerCase().startsWith('so,') &&
|
|
101
|
+
!firstSentence.toLowerCase().startsWith('basically,') &&
|
|
102
|
+
(
|
|
103
|
+
/^(yes|no|the|a|an|it|they|this|there|you|we)/i.test(firstSentence) ||
|
|
104
|
+
/^[A-Z][a-z]+\s+(is|are|was|were|can|should|will)/i.test(firstSentence)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (isDirectAnswer) {
|
|
108
|
+
headingsWithDirectAnswers++;
|
|
109
|
+
|
|
110
|
+
// Check if quotable (ideal length)
|
|
111
|
+
if (firstSentence.length >= IDEAL_ANSWER_LENGTH.min &&
|
|
112
|
+
firstSentence.length <= IDEAL_ANSWER_LENGTH.max) {
|
|
113
|
+
shortAnswersCount++;
|
|
114
|
+
quotableAnswers.push(firstSentence);
|
|
115
|
+
} else if (firstSentence.length > IDEAL_ANSWER_LENGTH.max) {
|
|
116
|
+
longAnswersCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Detect definitions
|
|
125
|
+
const definitions: string[] = [];
|
|
126
|
+
$('p').each((_, p) => {
|
|
127
|
+
const text = $(p).text().trim();
|
|
128
|
+
|
|
129
|
+
// Definition patterns: "X is a...", "X refers to...", "X means..."
|
|
130
|
+
const defMatch = text.match(/^([A-Z][^.]{0,50})\s+(is|are|refers to|means|describes|represents)\s+([^.]+\.)/);
|
|
131
|
+
if (defMatch) {
|
|
132
|
+
definitions.push(defMatch[0]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Check for summary elements
|
|
137
|
+
const bodyText = $('body').text().toLowerCase();
|
|
138
|
+
const headingText = headings.map((_, h) => $(h).text().toLowerCase()).get().join(' ');
|
|
139
|
+
|
|
140
|
+
const hasSummary =
|
|
141
|
+
headingText.includes('summary') ||
|
|
142
|
+
headingText.includes('conclusion') ||
|
|
143
|
+
headingText.includes('key takeaways') ||
|
|
144
|
+
$('[class*="summary"], [id*="summary"]').length > 0;
|
|
145
|
+
|
|
146
|
+
const hasTLDR =
|
|
147
|
+
bodyText.includes('tl;dr') ||
|
|
148
|
+
bodyText.includes('tldr') ||
|
|
149
|
+
bodyText.includes('in short') ||
|
|
150
|
+
bodyText.includes('in summary') ||
|
|
151
|
+
headingText.includes('tl;dr');
|
|
152
|
+
|
|
153
|
+
const hasKeyPoints =
|
|
154
|
+
headingText.includes('key points') ||
|
|
155
|
+
headingText.includes('main points') ||
|
|
156
|
+
headingText.includes('highlights') ||
|
|
157
|
+
$('[class*="key-points"], [class*="highlights"]').length > 0;
|
|
158
|
+
|
|
159
|
+
// Calculate average first sentence length
|
|
160
|
+
const averageFirstSentenceLength = firstSentenceLengths.length > 0
|
|
161
|
+
? firstSentenceLengths.reduce((a, b) => a + b, 0) / firstSentenceLengths.length
|
|
162
|
+
: 0;
|
|
163
|
+
|
|
164
|
+
// Calculate conciseness score
|
|
165
|
+
let concisenessScore = 50;
|
|
166
|
+
|
|
167
|
+
// Question/answer quality
|
|
168
|
+
if (questionHeadings > 0) {
|
|
169
|
+
const answerRatio = headingsWithDirectAnswers / questionHeadings;
|
|
170
|
+
concisenessScore += answerRatio * 20;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Quotable answers bonus
|
|
174
|
+
if (shortAnswersCount >= 3) concisenessScore += 15;
|
|
175
|
+
else if (shortAnswersCount >= 1) concisenessScore += 10;
|
|
176
|
+
|
|
177
|
+
// Long answers penalty
|
|
178
|
+
if (longAnswersCount > shortAnswersCount) concisenessScore -= 10;
|
|
179
|
+
|
|
180
|
+
// Definitions bonus
|
|
181
|
+
if (definitions.length >= 2) concisenessScore += 10;
|
|
182
|
+
else if (definitions.length === 1) concisenessScore += 5;
|
|
183
|
+
|
|
184
|
+
// Summary elements bonus
|
|
185
|
+
if (hasSummary) concisenessScore += 5;
|
|
186
|
+
if (hasTLDR) concisenessScore += 5;
|
|
187
|
+
if (hasKeyPoints) concisenessScore += 5;
|
|
188
|
+
|
|
189
|
+
// Average sentence length
|
|
190
|
+
if (averageFirstSentenceLength > 0 && averageFirstSentenceLength <= 150) {
|
|
191
|
+
concisenessScore += 5;
|
|
192
|
+
} else if (averageFirstSentenceLength > 200) {
|
|
193
|
+
concisenessScore -= 10;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
concisenessScore = Math.max(0, Math.min(100, concisenessScore));
|
|
197
|
+
|
|
198
|
+
// Generate issues
|
|
199
|
+
|
|
200
|
+
// Questions without direct answers
|
|
201
|
+
if (questionHeadings > 0 && headingsWithDirectAnswers < questionHeadings * 0.5) {
|
|
202
|
+
issues.push({
|
|
203
|
+
code: 'AI_INDIRECT_ANSWERS',
|
|
204
|
+
severity: 'warning',
|
|
205
|
+
category: 'ai-readiness',
|
|
206
|
+
title: 'Questions lack direct, quotable answers',
|
|
207
|
+
description: `${questionHeadings} question headings found but only ${headingsWithDirectAnswers} have direct answers. AI prefers content that answers questions immediately.`,
|
|
208
|
+
impact: 'AI may skip your content for sources with clearer, more direct answers.',
|
|
209
|
+
howToFix: 'Start answer paragraphs with a direct 1-2 sentence answer. Avoid filler words like "Well," "So," "Basically." Lead with the answer, then explain.',
|
|
210
|
+
affectedUrls: [url],
|
|
211
|
+
details: {
|
|
212
|
+
questionHeadings,
|
|
213
|
+
directAnswers: headingsWithDirectAnswers,
|
|
214
|
+
examples: quotableAnswers.slice(0, 3),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Answers too long
|
|
220
|
+
if (longAnswersCount > 0 && longAnswersCount > shortAnswersCount) {
|
|
221
|
+
issues.push({
|
|
222
|
+
code: 'AI_VERBOSE_ANSWERS',
|
|
223
|
+
severity: 'notice',
|
|
224
|
+
category: 'ai-readiness',
|
|
225
|
+
title: 'Answer opening sentences are too long',
|
|
226
|
+
description: `Found ${longAnswersCount} answers with opening sentences over 150 characters. AI quotes shorter, punchier statements.`,
|
|
227
|
+
impact: 'Long opening sentences are less likely to be quoted verbatim by AI.',
|
|
228
|
+
howToFix: 'Keep first sentences under 150 characters. Move details to subsequent sentences. Think "tweet-length" for opening answers.',
|
|
229
|
+
affectedUrls: [url],
|
|
230
|
+
details: {
|
|
231
|
+
averageLength: Math.round(averageFirstSentenceLength),
|
|
232
|
+
idealLength: `${IDEAL_ANSWER_LENGTH.min}-${IDEAL_ANSWER_LENGTH.max} characters`,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// No definitions for definitional content
|
|
238
|
+
const hasDefinitionalQueries =
|
|
239
|
+
bodyText.includes('what is') ||
|
|
240
|
+
bodyText.includes('what are') ||
|
|
241
|
+
bodyText.includes('definition') ||
|
|
242
|
+
bodyText.includes('meaning of');
|
|
243
|
+
|
|
244
|
+
if (hasDefinitionalQueries && definitions.length === 0) {
|
|
245
|
+
issues.push({
|
|
246
|
+
code: 'AI_NO_CLEAR_DEFINITIONS',
|
|
247
|
+
severity: 'notice',
|
|
248
|
+
category: 'ai-readiness',
|
|
249
|
+
title: 'Definitional content lacks clear definitions',
|
|
250
|
+
description: 'Content discusses "what is" topics but lacks Wikipedia-style definitions. AI heavily favors clear "[Term] is a [definition]" patterns.',
|
|
251
|
+
impact: 'Missing out on definitional search citations and AI answer boxes.',
|
|
252
|
+
howToFix: 'Add clear definitions: "[Term] is a [category] that [key characteristics]." Place definitions early in content or in dedicated sections.',
|
|
253
|
+
affectedUrls: [url],
|
|
254
|
+
details: {
|
|
255
|
+
suggestion: 'Example: "SEO is the practice of optimizing websites to rank higher in search engine results."',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// No summary or TLDR
|
|
261
|
+
const wordCount = $('body').text().split(/\s+/).length;
|
|
262
|
+
if (wordCount > 1500 && !hasSummary && !hasTLDR && !hasKeyPoints) {
|
|
263
|
+
issues.push({
|
|
264
|
+
code: 'AI_NO_SUMMARY',
|
|
265
|
+
severity: 'notice',
|
|
266
|
+
category: 'ai-readiness',
|
|
267
|
+
title: 'Long content lacks summary or key takeaways',
|
|
268
|
+
description: 'Content is over 1500 words but has no summary, TL;DR, or key points section. AI often pulls from summary sections.',
|
|
269
|
+
impact: 'Harder for AI to extract key points; may cite competitors with better-structured summaries.',
|
|
270
|
+
howToFix: 'Add a "Key Takeaways" section at the top or a "Summary" at the bottom. Use bullet points for scannable key points.',
|
|
271
|
+
affectedUrls: [url],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Low conciseness score
|
|
276
|
+
if (concisenessScore < 50) {
|
|
277
|
+
issues.push({
|
|
278
|
+
code: 'AI_LOW_CONCISENESS',
|
|
279
|
+
severity: 'warning',
|
|
280
|
+
category: 'ai-readiness',
|
|
281
|
+
title: 'Content lacks concise, quotable answers',
|
|
282
|
+
description: `Conciseness score: ${concisenessScore}/100. AI prefers content with clear, succinct answers it can quote directly.`,
|
|
283
|
+
impact: 'Reduced likelihood of AI citing your content in direct answers.',
|
|
284
|
+
howToFix: 'Restructure content: lead with direct answers, add definitions, include TL;DR sections, keep opening sentences short.',
|
|
285
|
+
affectedUrls: [url],
|
|
286
|
+
details: {
|
|
287
|
+
concisenessScore,
|
|
288
|
+
quotableAnswers: quotableAnswers.length,
|
|
289
|
+
definitions: definitions.length,
|
|
290
|
+
hasSummary,
|
|
291
|
+
hasTLDR,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
issues,
|
|
298
|
+
data: {
|
|
299
|
+
headings: {
|
|
300
|
+
total: totalHeadings,
|
|
301
|
+
questions: questionHeadings,
|
|
302
|
+
withDirectAnswers: headingsWithDirectAnswers,
|
|
303
|
+
},
|
|
304
|
+
answers: {
|
|
305
|
+
averageFirstSentenceLength: Math.round(averageFirstSentenceLength),
|
|
306
|
+
shortAnswersCount,
|
|
307
|
+
longAnswersCount,
|
|
308
|
+
quotableAnswers: quotableAnswers.slice(0, 5),
|
|
309
|
+
},
|
|
310
|
+
definitions: {
|
|
311
|
+
count: definitions.length,
|
|
312
|
+
examples: definitions.slice(0, 3),
|
|
313
|
+
},
|
|
314
|
+
keyTakeaways: {
|
|
315
|
+
hasSummary,
|
|
316
|
+
hasTLDR,
|
|
317
|
+
hasKeyPoints,
|
|
318
|
+
},
|
|
319
|
+
concisenessScore,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|