@rankcli/agent-runtime 0.0.15 → 0.0.17
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 +5 -4
- package/dist/index.d.mts +44 -393
- package/dist/index.d.ts +44 -393
- package/dist/index.js +22098 -26131
- package/dist/index.mjs +11834 -15857
- package/package.json +10 -4
- 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,322 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Asset Minification Check
|
|
3
|
-
*
|
|
4
|
-
* Checks if CSS and JavaScript files are properly minified.
|
|
5
|
-
* Unminified files contain unnecessary whitespace, comments, and long variable names
|
|
6
|
-
* that increase file size and slow down page load times.
|
|
7
|
-
*
|
|
8
|
-
* Detection methods:
|
|
9
|
-
* - Whitespace ratio (minified files have very low whitespace)
|
|
10
|
-
* - Average line length (minified files have very long lines)
|
|
11
|
-
* - Comment presence (minified files have no comments)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { httpGet } from '../../utils/http.js';
|
|
15
|
-
import * as cheerio from 'cheerio';
|
|
16
|
-
import type { AuditIssue } from '../types.js';
|
|
17
|
-
|
|
18
|
-
export interface MinificationData {
|
|
19
|
-
cssFiles: AssetMinificationInfo[];
|
|
20
|
-
jsFiles: AssetMinificationInfo[];
|
|
21
|
-
totalCssFiles: number;
|
|
22
|
-
totalJsFiles: number;
|
|
23
|
-
unminifiedCss: number;
|
|
24
|
-
unminifiedJs: number;
|
|
25
|
-
potentialSavings: {
|
|
26
|
-
css: number; // estimated bytes
|
|
27
|
-
js: number;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface AssetMinificationInfo {
|
|
32
|
-
url: string;
|
|
33
|
-
size: number;
|
|
34
|
-
isMinified: boolean;
|
|
35
|
-
indicators: {
|
|
36
|
-
avgLineLength: number;
|
|
37
|
-
whitespaceRatio: number;
|
|
38
|
-
hasComments: boolean;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Detect if content is minified
|
|
44
|
-
*/
|
|
45
|
-
function isMinified(content: string, type: 'css' | 'js'): { isMinified: boolean; indicators: AssetMinificationInfo['indicators'] } {
|
|
46
|
-
const lines = content.split('\n');
|
|
47
|
-
const totalLines = lines.length;
|
|
48
|
-
const avgLineLength = totalLines > 0 ? content.length / totalLines : 0;
|
|
49
|
-
|
|
50
|
-
// Count whitespace characters (spaces, tabs, newlines)
|
|
51
|
-
const whitespaceCount = (content.match(/\s/g) || []).length;
|
|
52
|
-
const whitespaceRatio = content.length > 0 ? whitespaceCount / content.length : 0;
|
|
53
|
-
|
|
54
|
-
// Check for comments
|
|
55
|
-
let hasComments = false;
|
|
56
|
-
if (type === 'css') {
|
|
57
|
-
// CSS comments: /* ... */
|
|
58
|
-
hasComments = /\/\*[\s\S]*?\*\//.test(content);
|
|
59
|
-
} else {
|
|
60
|
-
// JS comments: // ... or /* ... */
|
|
61
|
-
hasComments = /\/\/[^\n]*|\/\*[\s\S]*?\*\//.test(content);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Minification indicators:
|
|
65
|
-
// - Average line length > 500 chars (minified = long lines)
|
|
66
|
-
// - Whitespace ratio < 10% (minified = little whitespace)
|
|
67
|
-
// - No comments
|
|
68
|
-
const isMinifiedByLineLength = avgLineLength > 500;
|
|
69
|
-
const isMinifiedByWhitespace = whitespaceRatio < 0.1;
|
|
70
|
-
const isMinifiedByComments = !hasComments;
|
|
71
|
-
|
|
72
|
-
// Consider minified if at least 2 of 3 indicators pass
|
|
73
|
-
// OR if single-line file with reasonable length (common for small minified files)
|
|
74
|
-
const score = (isMinifiedByLineLength ? 1 : 0) + (isMinifiedByWhitespace ? 1 : 0) + (isMinifiedByComments ? 1 : 0);
|
|
75
|
-
const isSingleLineMinified = totalLines <= 2 && avgLineLength > 100;
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
isMinified: score >= 2 || isSingleLineMinified,
|
|
79
|
-
indicators: {
|
|
80
|
-
avgLineLength: Math.round(avgLineLength),
|
|
81
|
-
whitespaceRatio: Math.round(whitespaceRatio * 100) / 100,
|
|
82
|
-
hasComments,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Extract asset URLs from HTML
|
|
89
|
-
*/
|
|
90
|
-
function extractAssetUrls(html: string, baseUrl: string): { css: string[]; js: string[] } {
|
|
91
|
-
const $ = cheerio.load(html);
|
|
92
|
-
const base = new URL(baseUrl);
|
|
93
|
-
const css: string[] = [];
|
|
94
|
-
const js: string[] = [];
|
|
95
|
-
|
|
96
|
-
// Extract CSS files
|
|
97
|
-
$('link[rel="stylesheet"][href]').each((_, el) => {
|
|
98
|
-
const href = $(el).attr('href');
|
|
99
|
-
if (href && !href.startsWith('data:')) {
|
|
100
|
-
try {
|
|
101
|
-
const fullUrl = new URL(href, base).href;
|
|
102
|
-
// Only check first-party CSS
|
|
103
|
-
if (new URL(fullUrl).hostname === base.hostname) {
|
|
104
|
-
css.push(fullUrl);
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
// Invalid URL
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Extract JS files
|
|
113
|
-
$('script[src]').each((_, el) => {
|
|
114
|
-
const src = $(el).attr('src');
|
|
115
|
-
if (src && !src.startsWith('data:')) {
|
|
116
|
-
try {
|
|
117
|
-
const fullUrl = new URL(src, base).href;
|
|
118
|
-
// Only check first-party JS
|
|
119
|
-
if (new URL(fullUrl).hostname === base.hostname) {
|
|
120
|
-
js.push(fullUrl);
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
// Invalid URL
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return { css, js };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Analyze asset minification
|
|
133
|
-
*/
|
|
134
|
-
export async function analyzeAssetMinification(
|
|
135
|
-
html: string,
|
|
136
|
-
url: string
|
|
137
|
-
): Promise<{ issues: AuditIssue[]; data: MinificationData }> {
|
|
138
|
-
const issues: AuditIssue[] = [];
|
|
139
|
-
const assetUrls = extractAssetUrls(html, url);
|
|
140
|
-
|
|
141
|
-
const cssFiles: AssetMinificationInfo[] = [];
|
|
142
|
-
const jsFiles: AssetMinificationInfo[] = [];
|
|
143
|
-
|
|
144
|
-
// Check CSS files (limit to 3, parallel with short timeout)
|
|
145
|
-
const cssToCheck = assetUrls.css.slice(0, 3);
|
|
146
|
-
const cssPromises = cssToCheck.map(async (cssUrl) => {
|
|
147
|
-
try {
|
|
148
|
-
const response = await httpGet<string>(cssUrl, {
|
|
149
|
-
timeout: 3000, // Reduced timeout
|
|
150
|
-
validateStatus: () => true,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
if (response.status === 200 && typeof response.data === 'string') {
|
|
154
|
-
const content = response.data;
|
|
155
|
-
const { isMinified: minified, indicators } = isMinified(content, 'css');
|
|
156
|
-
|
|
157
|
-
cssFiles.push({
|
|
158
|
-
url: cssUrl,
|
|
159
|
-
size: content.length,
|
|
160
|
-
isMinified: minified,
|
|
161
|
-
indicators,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
// Skip failed requests
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Check JS files (limit to 3, parallel with short timeout)
|
|
170
|
-
const jsToCheck = assetUrls.js.slice(0, 3);
|
|
171
|
-
const jsPromises = jsToCheck.map(async (jsUrl) => {
|
|
172
|
-
try {
|
|
173
|
-
const response = await httpGet<string>(jsUrl, {
|
|
174
|
-
timeout: 3000, // Reduced timeout
|
|
175
|
-
validateStatus: () => true,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
if (response.status === 200 && typeof response.data === 'string') {
|
|
179
|
-
const content = response.data;
|
|
180
|
-
const { isMinified: minified, indicators } = isMinified(content, 'js');
|
|
181
|
-
|
|
182
|
-
jsFiles.push({
|
|
183
|
-
url: jsUrl,
|
|
184
|
-
size: content.length,
|
|
185
|
-
isMinified: minified,
|
|
186
|
-
indicators,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
} catch {
|
|
190
|
-
// Skip failed requests
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Run all checks in parallel with overall timeout
|
|
195
|
-
await Promise.race([
|
|
196
|
-
Promise.all([...cssPromises, ...jsPromises]),
|
|
197
|
-
new Promise<void>((resolve) => setTimeout(resolve, 8000)), // 8s overall timeout
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
// Count unminified files
|
|
201
|
-
const unminifiedCss = cssFiles.filter((f) => !f.isMinified);
|
|
202
|
-
const unminifiedJs = jsFiles.filter((f) => !f.isMinified);
|
|
203
|
-
|
|
204
|
-
// Estimate potential savings (minification typically saves 30-50%)
|
|
205
|
-
const cssSavings = unminifiedCss.reduce((sum, f) => sum + Math.round(f.size * 0.4), 0);
|
|
206
|
-
const jsSavings = unminifiedJs.reduce((sum, f) => sum + Math.round(f.size * 0.4), 0);
|
|
207
|
-
|
|
208
|
-
// Generate issues
|
|
209
|
-
if (unminifiedCss.length > 0) {
|
|
210
|
-
issues.push({
|
|
211
|
-
code: 'CSS_NOT_MINIFIED',
|
|
212
|
-
severity: 'warning',
|
|
213
|
-
category: 'performance',
|
|
214
|
-
title: 'CSS files not minified',
|
|
215
|
-
description: `${unminifiedCss.length} CSS file(s) appear to not be minified. Minification removes whitespace and comments to reduce file size.`,
|
|
216
|
-
impact: `Unminified CSS increases page load time. Potential savings: ~${Math.round(cssSavings / 1024)}KB.`,
|
|
217
|
-
howToFix:
|
|
218
|
-
'Use a CSS minifier like cssnano, clean-css, or build tools (Vite, Webpack) to minify CSS files. Most bundlers do this automatically in production builds.',
|
|
219
|
-
affectedUrls: unminifiedCss.map((f) => f.url),
|
|
220
|
-
details: {
|
|
221
|
-
files: unminifiedCss.map((f) => ({
|
|
222
|
-
url: f.url,
|
|
223
|
-
size: `${Math.round(f.size / 1024)}KB`,
|
|
224
|
-
avgLineLength: f.indicators.avgLineLength,
|
|
225
|
-
whitespaceRatio: `${Math.round(f.indicators.whitespaceRatio * 100)}%`,
|
|
226
|
-
hasComments: f.indicators.hasComments,
|
|
227
|
-
})),
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (unminifiedJs.length > 0) {
|
|
233
|
-
issues.push({
|
|
234
|
-
code: 'JS_NOT_MINIFIED',
|
|
235
|
-
severity: 'warning',
|
|
236
|
-
category: 'performance',
|
|
237
|
-
title: 'JavaScript files not minified',
|
|
238
|
-
description: `${unminifiedJs.length} JavaScript file(s) appear to not be minified. Minification removes whitespace, comments, and shortens variable names.`,
|
|
239
|
-
impact: `Unminified JavaScript increases page load time and parsing time. Potential savings: ~${Math.round(jsSavings / 1024)}KB.`,
|
|
240
|
-
howToFix:
|
|
241
|
-
'Use a JS minifier like Terser, UglifyJS, or build tools (Vite, Webpack, esbuild) to minify JavaScript. Most bundlers do this automatically in production builds.',
|
|
242
|
-
affectedUrls: unminifiedJs.map((f) => f.url),
|
|
243
|
-
details: {
|
|
244
|
-
files: unminifiedJs.map((f) => ({
|
|
245
|
-
url: f.url,
|
|
246
|
-
size: `${Math.round(f.size / 1024)}KB`,
|
|
247
|
-
avgLineLength: f.indicators.avgLineLength,
|
|
248
|
-
whitespaceRatio: `${Math.round(f.indicators.whitespaceRatio * 100)}%`,
|
|
249
|
-
hasComments: f.indicators.hasComments,
|
|
250
|
-
})),
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
issues,
|
|
257
|
-
data: {
|
|
258
|
-
cssFiles,
|
|
259
|
-
jsFiles,
|
|
260
|
-
totalCssFiles: assetUrls.css.length,
|
|
261
|
-
totalJsFiles: assetUrls.js.length,
|
|
262
|
-
unminifiedCss: unminifiedCss.length,
|
|
263
|
-
unminifiedJs: unminifiedJs.length,
|
|
264
|
-
potentialSavings: {
|
|
265
|
-
css: cssSavings,
|
|
266
|
-
js: jsSavings,
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
}
|