recker 1.0.27 → 1.0.28-next.3bf98c7
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/browser/scrape/extractors.js +2 -1
- package/dist/browser/scrape/types.d.ts +2 -1
- package/dist/cli/index.js +142 -3
- package/dist/cli/tui/shell.d.ts +2 -0
- package/dist/cli/tui/shell.js +492 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scrape/extractors.js +2 -1
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/spider.d.ts +61 -0
- package/dist/scrape/spider.js +250 -0
- package/dist/scrape/types.d.ts +2 -1
- package/dist/seo/analyzer.d.ts +42 -0
- package/dist/seo/analyzer.js +742 -0
- package/dist/seo/index.d.ts +7 -0
- package/dist/seo/index.js +3 -0
- package/dist/seo/rules/accessibility.d.ts +2 -0
- package/dist/seo/rules/accessibility.js +694 -0
- package/dist/seo/rules/best-practices.d.ts +2 -0
- package/dist/seo/rules/best-practices.js +188 -0
- package/dist/seo/rules/content.d.ts +2 -0
- package/dist/seo/rules/content.js +236 -0
- package/dist/seo/rules/crawl.d.ts +2 -0
- package/dist/seo/rules/crawl.js +307 -0
- package/dist/seo/rules/cwv.d.ts +2 -0
- package/dist/seo/rules/cwv.js +337 -0
- package/dist/seo/rules/ecommerce.d.ts +2 -0
- package/dist/seo/rules/ecommerce.js +252 -0
- package/dist/seo/rules/i18n.d.ts +2 -0
- package/dist/seo/rules/i18n.js +222 -0
- package/dist/seo/rules/images.d.ts +2 -0
- package/dist/seo/rules/images.js +180 -0
- package/dist/seo/rules/index.d.ts +52 -0
- package/dist/seo/rules/index.js +143 -0
- package/dist/seo/rules/internal-linking.d.ts +2 -0
- package/dist/seo/rules/internal-linking.js +375 -0
- package/dist/seo/rules/links.d.ts +2 -0
- package/dist/seo/rules/links.js +150 -0
- package/dist/seo/rules/local.d.ts +2 -0
- package/dist/seo/rules/local.js +265 -0
- package/dist/seo/rules/meta.d.ts +2 -0
- package/dist/seo/rules/meta.js +523 -0
- package/dist/seo/rules/mobile.d.ts +2 -0
- package/dist/seo/rules/mobile.js +71 -0
- package/dist/seo/rules/performance.d.ts +2 -0
- package/dist/seo/rules/performance.js +246 -0
- package/dist/seo/rules/pwa.d.ts +2 -0
- package/dist/seo/rules/pwa.js +302 -0
- package/dist/seo/rules/readability.d.ts +2 -0
- package/dist/seo/rules/readability.js +255 -0
- package/dist/seo/rules/schema.d.ts +2 -0
- package/dist/seo/rules/schema.js +54 -0
- package/dist/seo/rules/security.d.ts +2 -0
- package/dist/seo/rules/security.js +525 -0
- package/dist/seo/rules/social.d.ts +2 -0
- package/dist/seo/rules/social.js +373 -0
- package/dist/seo/rules/structural.d.ts +2 -0
- package/dist/seo/rules/structural.js +155 -0
- package/dist/seo/rules/technical.d.ts +2 -0
- package/dist/seo/rules/technical.js +223 -0
- package/dist/seo/rules/thresholds.d.ts +196 -0
- package/dist/seo/rules/thresholds.js +118 -0
- package/dist/seo/rules/types.d.ts +346 -0
- package/dist/seo/rules/types.js +11 -0
- package/dist/seo/seo-spider.d.ts +47 -0
- package/dist/seo/seo-spider.js +362 -0
- package/dist/seo/types.d.ts +184 -0
- package/dist/seo/types.js +1 -0
- package/dist/utils/columns.d.ts +14 -0
- package/dist/utils/columns.js +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const readabilityRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'readability-flesch-score',
|
|
5
|
+
name: 'Flesch Reading Ease',
|
|
6
|
+
category: 'content',
|
|
7
|
+
severity: 'info',
|
|
8
|
+
description: 'Content should be easy to read for the target audience',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.fleschReadingEase === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const score = ctx.fleschReadingEase;
|
|
13
|
+
let level;
|
|
14
|
+
let status;
|
|
15
|
+
if (score >= 60) {
|
|
16
|
+
level = score >= 80 ? 'Easy' : score >= 70 ? 'Fairly Easy' : 'Standard';
|
|
17
|
+
status = 'pass';
|
|
18
|
+
}
|
|
19
|
+
else if (score >= 50) {
|
|
20
|
+
level = 'Fairly Difficult';
|
|
21
|
+
status = 'info';
|
|
22
|
+
}
|
|
23
|
+
else if (score >= 30) {
|
|
24
|
+
level = 'Difficult';
|
|
25
|
+
status = 'warn';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
level = 'Very Difficult';
|
|
29
|
+
status = 'warn';
|
|
30
|
+
}
|
|
31
|
+
if (status === 'warn') {
|
|
32
|
+
return createResult({ id: 'readability-flesch-score', name: 'Flesch Reading Ease', category: 'content', severity: 'info' }, status, `Flesch score: ${Math.round(score)} (${level})`, {
|
|
33
|
+
recommendation: 'Simplify content for better readability',
|
|
34
|
+
evidence: {
|
|
35
|
+
found: Math.round(score),
|
|
36
|
+
expected: '60-70 for general web content',
|
|
37
|
+
impact: 'Difficult content has higher bounce rates',
|
|
38
|
+
learnMore: 'https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return createResult({ id: 'readability-flesch-score', name: 'Flesch Reading Ease', category: 'content', severity: 'info' }, status, `Flesch score: ${Math.round(score)} (${level})`);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'readability-sentence-length',
|
|
47
|
+
name: 'Sentence Length',
|
|
48
|
+
category: 'content',
|
|
49
|
+
severity: 'info',
|
|
50
|
+
description: 'Sentences should be concise for better readability',
|
|
51
|
+
check: (ctx) => {
|
|
52
|
+
if (ctx.avgSentenceLength === undefined)
|
|
53
|
+
return null;
|
|
54
|
+
const avgLength = ctx.avgSentenceLength;
|
|
55
|
+
if (avgLength > 30) {
|
|
56
|
+
return createResult({ id: 'readability-sentence-length', name: 'Sentence Length', category: 'content', severity: 'info' }, 'warn', `Average sentence length: ${Math.round(avgLength)} words (too long)`, {
|
|
57
|
+
recommendation: 'Break long sentences into shorter ones',
|
|
58
|
+
evidence: {
|
|
59
|
+
found: Math.round(avgLength),
|
|
60
|
+
expected: '15-20 words per sentence ideal, max 25',
|
|
61
|
+
impact: 'Long sentences are harder to understand on mobile',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (avgLength > 25) {
|
|
66
|
+
return createResult({ id: 'readability-sentence-length', name: 'Sentence Length', category: 'content', severity: 'info' }, 'info', `Average sentence length: ${Math.round(avgLength)} words (slightly long)`, {
|
|
67
|
+
recommendation: 'Consider shortening some sentences',
|
|
68
|
+
evidence: {
|
|
69
|
+
found: Math.round(avgLength),
|
|
70
|
+
expected: '15-20 words per sentence',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return createResult({ id: 'readability-sentence-length', name: 'Sentence Length', category: 'content', severity: 'info' }, 'pass', `Average sentence length: ${Math.round(avgLength)} words`);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'readability-paragraph-length',
|
|
79
|
+
name: 'Paragraph Length',
|
|
80
|
+
category: 'content',
|
|
81
|
+
severity: 'info',
|
|
82
|
+
description: 'Paragraphs should be short for web readability',
|
|
83
|
+
check: (ctx) => {
|
|
84
|
+
if (ctx.avgParagraphLength === undefined)
|
|
85
|
+
return null;
|
|
86
|
+
const avgLength = ctx.avgParagraphLength;
|
|
87
|
+
if (avgLength > 150) {
|
|
88
|
+
return createResult({ id: 'readability-paragraph-length', name: 'Paragraph Length', category: 'content', severity: 'info' }, 'warn', `Average paragraph length: ${Math.round(avgLength)} words (too long)`, {
|
|
89
|
+
recommendation: 'Break paragraphs into smaller chunks',
|
|
90
|
+
evidence: {
|
|
91
|
+
found: Math.round(avgLength),
|
|
92
|
+
expected: '40-80 words per paragraph for web',
|
|
93
|
+
impact: 'Wall of text reduces engagement and increases bounce rate',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (avgLength > 100) {
|
|
98
|
+
return createResult({ id: 'readability-paragraph-length', name: 'Paragraph Length', category: 'content', severity: 'info' }, 'info', `Average paragraph length: ${Math.round(avgLength)} words`, {
|
|
99
|
+
recommendation: 'Consider breaking longer paragraphs',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return createResult({ id: 'readability-paragraph-length', name: 'Paragraph Length', category: 'content', severity: 'info' }, 'pass', `Average paragraph length: ${Math.round(avgLength)} words`);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'readability-passive-voice',
|
|
107
|
+
name: 'Passive Voice',
|
|
108
|
+
category: 'content',
|
|
109
|
+
severity: 'info',
|
|
110
|
+
description: 'Limit passive voice for clearer writing',
|
|
111
|
+
check: (ctx) => {
|
|
112
|
+
if (ctx.passiveVoicePercentage === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
const percentage = ctx.passiveVoicePercentage;
|
|
115
|
+
if (percentage > 20) {
|
|
116
|
+
return createResult({ id: 'readability-passive-voice', name: 'Passive Voice', category: 'content', severity: 'info' }, 'warn', `Passive voice: ${Math.round(percentage)}% of sentences`, {
|
|
117
|
+
recommendation: 'Convert passive sentences to active voice',
|
|
118
|
+
evidence: {
|
|
119
|
+
found: `${Math.round(percentage)}%`,
|
|
120
|
+
expected: 'Less than 10% passive voice',
|
|
121
|
+
example: 'Instead of "The button was clicked by the user" → "The user clicked the button"',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (percentage > 15) {
|
|
126
|
+
return createResult({ id: 'readability-passive-voice', name: 'Passive Voice', category: 'content', severity: 'info' }, 'info', `Passive voice: ${Math.round(percentage)}% of sentences`, {
|
|
127
|
+
recommendation: 'Consider reducing passive voice usage',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'readability-transition-words',
|
|
135
|
+
name: 'Transition Words',
|
|
136
|
+
category: 'content',
|
|
137
|
+
severity: 'info',
|
|
138
|
+
description: 'Use transition words for better flow',
|
|
139
|
+
check: (ctx) => {
|
|
140
|
+
if (ctx.transitionWordPercentage === undefined)
|
|
141
|
+
return null;
|
|
142
|
+
const percentage = ctx.transitionWordPercentage;
|
|
143
|
+
if (percentage < 20) {
|
|
144
|
+
return createResult({ id: 'readability-transition-words', name: 'Transition Words', category: 'content', severity: 'info' }, 'info', `Transition words: ${Math.round(percentage)}% of sentences`, {
|
|
145
|
+
recommendation: 'Add transition words for better content flow',
|
|
146
|
+
evidence: {
|
|
147
|
+
found: `${Math.round(percentage)}%`,
|
|
148
|
+
expected: 'At least 30% of sentences',
|
|
149
|
+
example: 'Words like: however, therefore, additionally, furthermore, for example',
|
|
150
|
+
impact: 'Transition words improve comprehension and engagement',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return createResult({ id: 'readability-transition-words', name: 'Transition Words', category: 'content', severity: 'info' }, 'pass', `Transition words: ${Math.round(percentage)}% of sentences`);
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'readability-subheading-distribution',
|
|
159
|
+
name: 'Subheading Distribution',
|
|
160
|
+
category: 'content',
|
|
161
|
+
severity: 'info',
|
|
162
|
+
description: 'Break content with subheadings every 300 words',
|
|
163
|
+
check: (ctx) => {
|
|
164
|
+
if (!ctx.wordCount || !ctx.h2Count)
|
|
165
|
+
return null;
|
|
166
|
+
const wordsPerSubheading = ctx.wordCount / (ctx.h2Count + 1);
|
|
167
|
+
if (ctx.wordCount > 500 && wordsPerSubheading > 400) {
|
|
168
|
+
return createResult({ id: 'readability-subheading-distribution', name: 'Subheading Distribution', category: 'content', severity: 'info' }, 'warn', `${Math.round(wordsPerSubheading)} words between subheadings (too many)`, {
|
|
169
|
+
recommendation: 'Add more H2/H3 subheadings to break up content',
|
|
170
|
+
evidence: {
|
|
171
|
+
found: `${ctx.h2Count} subheadings for ${ctx.wordCount} words`,
|
|
172
|
+
expected: 'One subheading every 250-350 words',
|
|
173
|
+
impact: 'Subheadings improve scannability and featured snippet eligibility',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (ctx.wordCount > 300 && wordsPerSubheading > 300) {
|
|
178
|
+
return createResult({ id: 'readability-subheading-distribution', name: 'Subheading Distribution', category: 'content', severity: 'info' }, 'info', `${Math.round(wordsPerSubheading)} words per section`, {
|
|
179
|
+
recommendation: 'Consider adding more subheadings',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return createResult({ id: 'readability-subheading-distribution', name: 'Subheading Distribution', category: 'content', severity: 'info' }, 'pass', `${Math.round(wordsPerSubheading)} words per section (good)`);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'readability-text-variety',
|
|
187
|
+
name: 'Text Variety',
|
|
188
|
+
category: 'content',
|
|
189
|
+
severity: 'info',
|
|
190
|
+
description: 'Use varied sentence structures and word choices',
|
|
191
|
+
check: (ctx) => {
|
|
192
|
+
if (ctx.consecutiveSentenceStarts === undefined)
|
|
193
|
+
return null;
|
|
194
|
+
if (ctx.consecutiveSentenceStarts > 3) {
|
|
195
|
+
return createResult({ id: 'readability-text-variety', name: 'Text Variety', category: 'content', severity: 'info' }, 'info', `${ctx.consecutiveSentenceStarts} consecutive sentences start similarly`, {
|
|
196
|
+
recommendation: 'Vary sentence beginnings for better flow',
|
|
197
|
+
evidence: {
|
|
198
|
+
impact: 'Repetitive sentence structures feel monotonous',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'readability-word-complexity',
|
|
207
|
+
name: 'Word Complexity',
|
|
208
|
+
category: 'content',
|
|
209
|
+
severity: 'info',
|
|
210
|
+
description: 'Avoid overly complex vocabulary',
|
|
211
|
+
check: (ctx) => {
|
|
212
|
+
if (ctx.complexWordPercentage === undefined)
|
|
213
|
+
return null;
|
|
214
|
+
if (ctx.complexWordPercentage > 15) {
|
|
215
|
+
return createResult({ id: 'readability-word-complexity', name: 'Word Complexity', category: 'content', severity: 'info' }, 'warn', `Complex words: ${Math.round(ctx.complexWordPercentage)}%`, {
|
|
216
|
+
recommendation: 'Use simpler vocabulary where possible',
|
|
217
|
+
evidence: {
|
|
218
|
+
found: `${Math.round(ctx.complexWordPercentage)}% words with 3+ syllables`,
|
|
219
|
+
expected: 'Less than 10% complex words for general audience',
|
|
220
|
+
impact: 'Complex vocabulary reduces comprehension',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (ctx.complexWordPercentage > 10) {
|
|
225
|
+
return createResult({ id: 'readability-word-complexity', name: 'Word Complexity', category: 'content', severity: 'info' }, 'info', `Complex words: ${Math.round(ctx.complexWordPercentage)}%`);
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'readability-list-usage',
|
|
232
|
+
name: 'List Usage',
|
|
233
|
+
category: 'content',
|
|
234
|
+
severity: 'info',
|
|
235
|
+
description: 'Use lists to improve scannability',
|
|
236
|
+
check: (ctx) => {
|
|
237
|
+
if (!ctx.wordCount || ctx.listCount === undefined)
|
|
238
|
+
return null;
|
|
239
|
+
if (ctx.wordCount > 500 && ctx.listCount === 0) {
|
|
240
|
+
return createResult({ id: 'readability-list-usage', name: 'List Usage', category: 'content', severity: 'info' }, 'info', 'No lists found in long-form content', {
|
|
241
|
+
recommendation: 'Consider using bullet points or numbered lists',
|
|
242
|
+
evidence: {
|
|
243
|
+
found: `${ctx.wordCount} words with 0 lists`,
|
|
244
|
+
expected: 'At least 1 list per 500 words for long content',
|
|
245
|
+
impact: 'Lists improve scannability and featured snippet eligibility',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (ctx.listCount > 0) {
|
|
250
|
+
return createResult({ id: 'readability-list-usage', name: 'List Usage', category: 'content', severity: 'info' }, 'pass', `${ctx.listCount} list(s) found`);
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createResult } from './types.js';
|
|
2
|
+
export const schemaRules = [
|
|
3
|
+
{
|
|
4
|
+
id: 'json-ld-exists',
|
|
5
|
+
name: 'Structured Data',
|
|
6
|
+
category: 'structured-data',
|
|
7
|
+
severity: 'info',
|
|
8
|
+
description: 'Page should have JSON-LD structured data',
|
|
9
|
+
check: (ctx) => {
|
|
10
|
+
if (ctx.jsonLdCount === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
if (ctx.jsonLdCount === 0) {
|
|
13
|
+
return createResult({ id: 'json-ld-exists', name: 'Structured Data', category: 'structured-data', severity: 'info' }, 'info', 'No JSON-LD structured data found', { recommendation: 'Add Schema.org structured data for rich snippets' });
|
|
14
|
+
}
|
|
15
|
+
const types = ctx.jsonLdTypes?.join(', ') || 'unknown';
|
|
16
|
+
return createResult({ id: 'json-ld-exists', name: 'Structured Data', category: 'structured-data', severity: 'info' }, 'pass', `${ctx.jsonLdCount} JSON-LD block(s) found`, { value: types });
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'schema-standard-types',
|
|
21
|
+
name: 'Schema Types',
|
|
22
|
+
category: 'structured-data',
|
|
23
|
+
severity: 'info',
|
|
24
|
+
description: 'Use standard Schema.org types',
|
|
25
|
+
check: (ctx) => {
|
|
26
|
+
if (!ctx.jsonLdTypes || ctx.jsonLdTypes.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const recommended = [
|
|
29
|
+
'WebSite', 'WebPage', 'Article', 'Product', 'BreadcrumbList',
|
|
30
|
+
'Organization', 'Person', 'LocalBusiness', 'Recipe', 'Event',
|
|
31
|
+
'JobPosting', 'FAQPage', 'HowTo', 'VideoObject',
|
|
32
|
+
'SoftwareApplication', 'Review'
|
|
33
|
+
];
|
|
34
|
+
const hasStandard = ctx.jsonLdTypes.some(t => recommended.includes(t));
|
|
35
|
+
if (!hasStandard) {
|
|
36
|
+
return createResult({ id: 'schema-standard-types', name: 'Schema Types', category: 'structured-data', severity: 'info' }, 'info', 'Using uncommon Schema.org types', { value: ctx.jsonLdTypes.join(', '), recommendation: `Consider using standard types like ${recommended.slice(0, 3).join(', ')}` });
|
|
37
|
+
}
|
|
38
|
+
return createResult({ id: 'schema-standard-types', name: 'Schema Types', category: 'structured-data', severity: 'info' }, 'pass', 'Using standard Schema.org types', { value: ctx.jsonLdTypes.join(', ') });
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'breadcrumbs-presence',
|
|
43
|
+
name: 'Breadcrumbs Presence',
|
|
44
|
+
category: 'structured-data',
|
|
45
|
+
severity: 'info',
|
|
46
|
+
description: 'Breadcrumbs improve navigation and SEO structure',
|
|
47
|
+
check: (ctx) => {
|
|
48
|
+
if (!ctx.hasBreadcrumbsHtml && !ctx.hasBreadcrumbsSchema) {
|
|
49
|
+
return createResult({ id: 'breadcrumbs-presence', name: 'Breadcrumbs Presence', category: 'structured-data', severity: 'info' }, 'info', 'No breadcrumbs found (HTML or Schema.org)', { recommendation: 'Add breadcrumbs using HTML and/or Schema.org BreadcrumbList' });
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|