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.
Files changed (72) hide show
  1. package/dist/browser/scrape/extractors.js +2 -1
  2. package/dist/browser/scrape/types.d.ts +2 -1
  3. package/dist/cli/index.js +142 -3
  4. package/dist/cli/tui/shell.d.ts +2 -0
  5. package/dist/cli/tui/shell.js +492 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/scrape/extractors.js +2 -1
  9. package/dist/scrape/index.d.ts +2 -0
  10. package/dist/scrape/index.js +1 -0
  11. package/dist/scrape/spider.d.ts +61 -0
  12. package/dist/scrape/spider.js +250 -0
  13. package/dist/scrape/types.d.ts +2 -1
  14. package/dist/seo/analyzer.d.ts +42 -0
  15. package/dist/seo/analyzer.js +742 -0
  16. package/dist/seo/index.d.ts +7 -0
  17. package/dist/seo/index.js +3 -0
  18. package/dist/seo/rules/accessibility.d.ts +2 -0
  19. package/dist/seo/rules/accessibility.js +694 -0
  20. package/dist/seo/rules/best-practices.d.ts +2 -0
  21. package/dist/seo/rules/best-practices.js +188 -0
  22. package/dist/seo/rules/content.d.ts +2 -0
  23. package/dist/seo/rules/content.js +236 -0
  24. package/dist/seo/rules/crawl.d.ts +2 -0
  25. package/dist/seo/rules/crawl.js +307 -0
  26. package/dist/seo/rules/cwv.d.ts +2 -0
  27. package/dist/seo/rules/cwv.js +337 -0
  28. package/dist/seo/rules/ecommerce.d.ts +2 -0
  29. package/dist/seo/rules/ecommerce.js +252 -0
  30. package/dist/seo/rules/i18n.d.ts +2 -0
  31. package/dist/seo/rules/i18n.js +222 -0
  32. package/dist/seo/rules/images.d.ts +2 -0
  33. package/dist/seo/rules/images.js +180 -0
  34. package/dist/seo/rules/index.d.ts +52 -0
  35. package/dist/seo/rules/index.js +143 -0
  36. package/dist/seo/rules/internal-linking.d.ts +2 -0
  37. package/dist/seo/rules/internal-linking.js +375 -0
  38. package/dist/seo/rules/links.d.ts +2 -0
  39. package/dist/seo/rules/links.js +150 -0
  40. package/dist/seo/rules/local.d.ts +2 -0
  41. package/dist/seo/rules/local.js +265 -0
  42. package/dist/seo/rules/meta.d.ts +2 -0
  43. package/dist/seo/rules/meta.js +523 -0
  44. package/dist/seo/rules/mobile.d.ts +2 -0
  45. package/dist/seo/rules/mobile.js +71 -0
  46. package/dist/seo/rules/performance.d.ts +2 -0
  47. package/dist/seo/rules/performance.js +246 -0
  48. package/dist/seo/rules/pwa.d.ts +2 -0
  49. package/dist/seo/rules/pwa.js +302 -0
  50. package/dist/seo/rules/readability.d.ts +2 -0
  51. package/dist/seo/rules/readability.js +255 -0
  52. package/dist/seo/rules/schema.d.ts +2 -0
  53. package/dist/seo/rules/schema.js +54 -0
  54. package/dist/seo/rules/security.d.ts +2 -0
  55. package/dist/seo/rules/security.js +525 -0
  56. package/dist/seo/rules/social.d.ts +2 -0
  57. package/dist/seo/rules/social.js +373 -0
  58. package/dist/seo/rules/structural.d.ts +2 -0
  59. package/dist/seo/rules/structural.js +155 -0
  60. package/dist/seo/rules/technical.d.ts +2 -0
  61. package/dist/seo/rules/technical.js +223 -0
  62. package/dist/seo/rules/thresholds.d.ts +196 -0
  63. package/dist/seo/rules/thresholds.js +118 -0
  64. package/dist/seo/rules/types.d.ts +346 -0
  65. package/dist/seo/rules/types.js +11 -0
  66. package/dist/seo/seo-spider.d.ts +47 -0
  67. package/dist/seo/seo-spider.js +362 -0
  68. package/dist/seo/types.d.ts +184 -0
  69. package/dist/seo/types.js +1 -0
  70. package/dist/utils/columns.d.ts +14 -0
  71. package/dist/utils/columns.js +69 -0
  72. package/package.json +1 -1
@@ -0,0 +1,180 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const imageRules = [
4
+ {
5
+ id: 'images-alt-text',
6
+ name: 'Image Alt Text',
7
+ category: 'images',
8
+ severity: 'error',
9
+ description: 'All images must have alt text',
10
+ check: (ctx) => {
11
+ if (ctx.totalImages === undefined || ctx.totalImages === 0)
12
+ return null;
13
+ const withoutAlt = ctx.imagesWithoutAlt ?? 0;
14
+ if (withoutAlt > 0) {
15
+ const percentage = Math.round((withoutAlt / ctx.totalImages) * 100);
16
+ const severity = withoutAlt > ctx.totalImages / 2 ? 'fail' : 'warn';
17
+ return createResult({ id: 'images-alt-text', name: 'Image Alt Text', category: 'images', severity: 'error' }, severity, `${withoutAlt} of ${ctx.totalImages} images missing alt text (${percentage}%)`, { value: withoutAlt, recommendation: 'Add descriptive alt text to all images' });
18
+ }
19
+ return createResult({ id: 'images-alt-text', name: 'Image Alt Text', category: 'images', severity: 'error' }, 'pass', 'All images have alt text');
20
+ },
21
+ },
22
+ {
23
+ id: 'images-dimensions',
24
+ name: 'Image Dimensions',
25
+ category: 'images',
26
+ severity: 'warning',
27
+ description: 'Images should have width and height attributes to prevent CLS',
28
+ check: (ctx) => {
29
+ if (ctx.totalImages === undefined || ctx.totalImages === 0)
30
+ return null;
31
+ const missing = ctx.imagesMissingDimensions ?? 0;
32
+ if (missing > 0) {
33
+ return createResult({ id: 'images-dimensions', name: 'Image Dimensions', category: 'images', severity: 'warning' }, 'warn', `${missing} images missing width/height attributes`, { value: missing, recommendation: 'Add width and height to prevent layout shifts (CLS)' });
34
+ }
35
+ return createResult({ id: 'images-dimensions', name: 'Image Dimensions', category: 'images', severity: 'warning' }, 'pass', 'All images have dimensions defined');
36
+ },
37
+ },
38
+ {
39
+ id: 'images-lazy-loading',
40
+ name: 'Lazy Loading',
41
+ category: 'images',
42
+ severity: 'info',
43
+ description: 'Below-the-fold images should use lazy loading',
44
+ check: (ctx) => {
45
+ if (ctx.totalImages === undefined || ctx.totalImages <= 3)
46
+ return null;
47
+ const lazy = ctx.imagesWithLazyLoad ?? 0;
48
+ if (lazy === 0) {
49
+ return createResult({ id: 'images-lazy-loading', name: 'Lazy Loading', category: 'images', severity: 'info' }, 'info', 'No images use lazy loading', { recommendation: 'Add loading="lazy" to below-the-fold images' });
50
+ }
51
+ return createResult({ id: 'images-lazy-loading', name: 'Lazy Loading', category: 'images', severity: 'info' }, 'pass', `${lazy} images use lazy loading`);
52
+ },
53
+ },
54
+ {
55
+ id: 'images-format-modern',
56
+ name: 'Modern Image Formats',
57
+ category: 'images',
58
+ severity: 'info',
59
+ description: 'Images should use modern formats like WebP or AVIF',
60
+ check: (ctx) => {
61
+ if (ctx.totalImages === undefined || ctx.totalImages === 0)
62
+ return null;
63
+ const modern = ctx.imagesUsingModernFormats ?? 0;
64
+ if (ctx.totalImages > 0 && modern === 0) {
65
+ return createResult({ id: 'images-format-modern', name: 'Modern Image Formats', category: 'images', severity: 'info' }, 'info', 'No images using modern formats (WebP/AVIF)', { value: modern, recommendation: 'Serve images in WebP or AVIF format for better compression' });
66
+ }
67
+ return createResult({ id: 'images-format-modern', name: 'Modern Image Formats', category: 'images', severity: 'info' }, 'pass', `${modern} images using modern formats`, { value: modern });
68
+ },
69
+ },
70
+ {
71
+ id: 'images-empty-alt',
72
+ name: 'Empty Alt Text',
73
+ category: 'images',
74
+ severity: 'info',
75
+ description: 'Images with empty alt="" are treated as decorative',
76
+ check: (ctx) => {
77
+ const emptyAlt = ctx.imagesWithEmptyAlt ?? 0;
78
+ if (emptyAlt > 0) {
79
+ return createResult({ id: 'images-empty-alt', name: 'Empty Alt Text', category: 'images', severity: 'info' }, 'info', `${emptyAlt} image(s) with empty alt="" (decorative)`, { value: emptyAlt, recommendation: 'Ensure these images are truly decorative' });
80
+ }
81
+ return null;
82
+ },
83
+ },
84
+ {
85
+ id: 'images-alt-length',
86
+ name: 'Alt Text Length',
87
+ category: 'images',
88
+ severity: 'warning',
89
+ description: 'Alt text should be descriptive (min 10, max 125 chars)',
90
+ check: (ctx) => {
91
+ if (!ctx.altTextLengths || ctx.altTextLengths.length === 0)
92
+ return null;
93
+ const { minLength, maxLength } = SEO_THRESHOLDS.images.alt;
94
+ let shortAlts = 0;
95
+ let longAlts = 0;
96
+ ctx.altTextLengths.forEach(len => {
97
+ if (len < minLength)
98
+ shortAlts++;
99
+ if (len > maxLength)
100
+ longAlts++;
101
+ });
102
+ if (shortAlts > 0) {
103
+ return createResult({ id: 'images-alt-length', name: 'Alt Text Length', category: 'images', severity: 'warning' }, 'warn', `${shortAlts} alt text(s) are too short (min: ${minLength} chars)`, { value: shortAlts, recommendation: `Make alt texts more descriptive, at least ${minLength} characters.` });
104
+ }
105
+ if (longAlts > 0) {
106
+ return createResult({ id: 'images-alt-length', name: 'Alt Text Length', category: 'images', severity: 'warning' }, 'warn', `${longAlts} alt text(s) are too long (max: ${maxLength} chars)`, { value: longAlts, recommendation: `Shorten alt texts to be concise, under ${maxLength} characters.` });
107
+ }
108
+ return null;
109
+ },
110
+ },
111
+ {
112
+ id: 'images-alt-length',
113
+ name: 'Alt Text Length',
114
+ category: 'images',
115
+ severity: 'warning',
116
+ description: 'Alt text should be descriptive (ideal 80-120, max 150 chars)',
117
+ check: (ctx) => {
118
+ if (!ctx.altTextLengths || ctx.altTextLengths.length === 0)
119
+ return null;
120
+ const { minLength, idealLength, maxLength } = SEO_THRESHOLDS.images.alt;
121
+ let shortAlts = 0;
122
+ let longAlts = 0;
123
+ let nonIdealAlts = 0;
124
+ ctx.altTextLengths.forEach(len => {
125
+ if (len < minLength)
126
+ shortAlts++;
127
+ else if (len > maxLength)
128
+ longAlts++;
129
+ else if (len < idealLength.min || len > idealLength.max)
130
+ nonIdealAlts++;
131
+ });
132
+ if (shortAlts > 0) {
133
+ return createResult({ id: 'images-alt-length', name: 'Alt Text Length', category: 'images', severity: 'warning' }, 'warn', `${shortAlts} alt text(s) are too short (min: ${minLength} chars)`, { value: shortAlts, recommendation: `Make alt texts more descriptive, at least ${minLength} characters.` });
134
+ }
135
+ if (longAlts > 0) {
136
+ return createResult({ id: 'images-alt-length', name: 'Alt Text Length', category: 'images', severity: 'warning' }, 'warn', `${longAlts} alt text(s) are too long (max: ${maxLength} chars)`, { value: longAlts, recommendation: `Shorten alt texts to be concise, under ${maxLength} characters.` });
137
+ }
138
+ if (nonIdealAlts > 0) {
139
+ return createResult({ id: 'images-alt-length', name: 'Alt Text Length', category: 'images', severity: 'info' }, 'info', `${nonIdealAlts} alt text(s) are not in the ideal length range (${idealLength.min}-${idealLength.max} chars)`, { value: nonIdealAlts, recommendation: `Aim for alt texts between ${idealLength.min} and ${idealLength.max} characters for best results.` });
140
+ }
141
+ return null;
142
+ },
143
+ },
144
+ {
145
+ id: 'images-clean-filenames',
146
+ name: 'Image Filenames',
147
+ category: 'images',
148
+ severity: 'info',
149
+ description: 'Image filenames should be descriptive and use keywords, not generic names.',
150
+ check: (ctx) => {
151
+ if (!ctx.imageFilenames || ctx.imageFilenames.length === 0)
152
+ return null;
153
+ const genericFilenames = ctx.imageFilenames.filter(name => /^(img|image|photo|pic)\d*\.(jpg|jpeg|png|webp|avif|gif)$/i.test(name) ||
154
+ /^screenshot_\d*\.(jpg|jpeg|png)$/i.test(name) ||
155
+ /^untitled-\d*\.(jpg|jpeg|png)$/i.test(name));
156
+ if (genericFilenames.length > 0) {
157
+ return createResult({ id: 'images-clean-filenames', name: 'Image Filenames', category: 'images', severity: 'warning' }, 'warn', `${genericFilenames.length} image(s) have generic filenames (e.g., IMG_1234.jpg)`, { value: genericFilenames.length, recommendation: 'Rename image files to be descriptive and include keywords (e.g., open-graph-example.jpg).' });
158
+ }
159
+ return null;
160
+ },
161
+ },
162
+ {
163
+ id: 'images-decoding-async',
164
+ name: 'Image Decoding Async',
165
+ category: 'images',
166
+ severity: 'info',
167
+ description: 'Use decoding="async" for non-critical images to improve rendering performance.',
168
+ check: (ctx) => {
169
+ if (ctx.totalImages === undefined || ctx.totalImages === 0)
170
+ return null;
171
+ if (ctx.imagesWithAsyncDecoding === undefined)
172
+ return null;
173
+ const nonAsync = ctx.totalImages - ctx.imagesWithAsyncDecoding;
174
+ if (nonAsync > 0 && ctx.totalImages > 3) {
175
+ return createResult({ id: 'images-decoding-async', name: 'Image Decoding Async', category: 'images', severity: 'info' }, 'info', `${nonAsync} image(s) do not use decoding="async"`, { value: nonAsync, recommendation: 'Consider adding decoding="async" to non-critical images for performance benefits.' });
176
+ }
177
+ return null;
178
+ },
179
+ },
180
+ ];
@@ -0,0 +1,52 @@
1
+ import { SeoRule, RuleCategory, RuleSeverity, RuleContext, RuleResult } from './types.js';
2
+ export * from './types.js';
3
+ export * from './thresholds.js';
4
+ export declare const ALL_SEO_RULES: SeoRule[];
5
+ export declare const SCORING_WEIGHTS: {
6
+ readonly severity: {
7
+ readonly error: {
8
+ readonly pass: 10;
9
+ readonly fail: -15;
10
+ readonly warn: -10;
11
+ readonly info: 0;
12
+ };
13
+ readonly warning: {
14
+ readonly pass: 5;
15
+ readonly fail: -8;
16
+ readonly warn: -5;
17
+ readonly info: 0;
18
+ };
19
+ readonly info: {
20
+ readonly pass: 2;
21
+ readonly fail: -3;
22
+ readonly warn: -2;
23
+ readonly info: 0;
24
+ };
25
+ };
26
+ readonly category: Record<RuleCategory, number>;
27
+ };
28
+ export declare function calculateWeightedScore(results: RuleResult[]): {
29
+ score: number;
30
+ maxPossible: number;
31
+ details: {
32
+ category: RuleCategory;
33
+ score: number;
34
+ weight: number;
35
+ }[];
36
+ };
37
+ export interface RulesEngineOptions {
38
+ categories?: RuleCategory[];
39
+ excludeCategories?: RuleCategory[];
40
+ rules?: string[];
41
+ excludeRules?: string[];
42
+ minSeverity?: RuleSeverity;
43
+ }
44
+ export declare class SeoRulesEngine {
45
+ private rules;
46
+ constructor(options?: RulesEngineOptions);
47
+ evaluate(context: RuleContext): RuleResult[];
48
+ getRules(): SeoRule[];
49
+ getRulesByCategory(category: RuleCategory): SeoRule[];
50
+ getCategories(): RuleCategory[];
51
+ }
52
+ export declare function createRulesEngine(options?: RulesEngineOptions): SeoRulesEngine;
@@ -0,0 +1,143 @@
1
+ import { metaRules } from './meta.js';
2
+ import { structuralRules } from './structural.js';
3
+ import { contentRules } from './content.js';
4
+ import { imageRules } from './images.js';
5
+ import { linkRules } from './links.js';
6
+ import { performanceRules } from './performance.js';
7
+ import { technicalRules } from './technical.js';
8
+ import { securityRules } from './security.js';
9
+ import { schemaRules } from './schema.js';
10
+ import { accessibilityRules } from './accessibility.js';
11
+ import { mobileRules } from './mobile.js';
12
+ import { i18nRules } from './i18n.js';
13
+ import { ecommerceRules } from './ecommerce.js';
14
+ import { localRules } from './local.js';
15
+ import { cwvRules } from './cwv.js';
16
+ import { crawlRules } from './crawl.js';
17
+ import { readabilityRules } from './readability.js';
18
+ import { pwaRules } from './pwa.js';
19
+ import { socialRules } from './social.js';
20
+ import { internalLinkingRules } from './internal-linking.js';
21
+ import { bestPracticesRules } from './best-practices.js';
22
+ export * from './types.js';
23
+ export * from './thresholds.js';
24
+ export const ALL_SEO_RULES = [
25
+ ...metaRules,
26
+ ...structuralRules,
27
+ ...contentRules,
28
+ ...imageRules,
29
+ ...linkRules,
30
+ ...performanceRules,
31
+ ...technicalRules,
32
+ ...securityRules,
33
+ ...schemaRules,
34
+ ...accessibilityRules,
35
+ ...mobileRules,
36
+ ...i18nRules,
37
+ ...ecommerceRules,
38
+ ...localRules,
39
+ ...cwvRules,
40
+ ...crawlRules,
41
+ ...readabilityRules,
42
+ ...pwaRules,
43
+ ...socialRules,
44
+ ...internalLinkingRules,
45
+ ...bestPracticesRules,
46
+ ];
47
+ export const SCORING_WEIGHTS = {
48
+ severity: {
49
+ error: { pass: 10, fail: -15, warn: -10, info: 0 },
50
+ warning: { pass: 5, fail: -8, warn: -5, info: 0 },
51
+ info: { pass: 2, fail: -3, warn: -2, info: 0 },
52
+ },
53
+ category: {
54
+ title: 1.5,
55
+ meta: 1.3,
56
+ og: 1.0,
57
+ twitter: 0.8,
58
+ headings: 1.2,
59
+ images: 1.0,
60
+ links: 1.1,
61
+ content: 1.2,
62
+ technical: 1.3,
63
+ security: 0.9,
64
+ mobile: 1.2,
65
+ 'structured-data': 1.0,
66
+ performance: 1.4,
67
+ accessibility: 0.8,
68
+ },
69
+ };
70
+ export function calculateWeightedScore(results) {
71
+ const categoryScores = {};
72
+ for (const result of results) {
73
+ const severityWeight = SCORING_WEIGHTS.severity[result.severity];
74
+ const statusScore = severityWeight[result.status] ?? 0;
75
+ if (!categoryScores[result.category]) {
76
+ categoryScores[result.category] = { score: 0, count: 0 };
77
+ }
78
+ categoryScores[result.category].score += statusScore;
79
+ categoryScores[result.category].count++;
80
+ }
81
+ let totalScore = 0;
82
+ let totalWeight = 0;
83
+ const details = [];
84
+ for (const [cat, data] of Object.entries(categoryScores)) {
85
+ const category = cat;
86
+ const categoryWeight = SCORING_WEIGHTS.category[category] ?? 1;
87
+ const normalizedScore = data.count > 0 ? data.score / data.count : 0;
88
+ const weightedScore = normalizedScore * categoryWeight;
89
+ details.push({ category, score: normalizedScore, weight: categoryWeight });
90
+ totalScore += weightedScore;
91
+ totalWeight += categoryWeight;
92
+ }
93
+ const maxPossible = 100;
94
+ const baseScore = 70;
95
+ const adjustedScore = Math.max(0, Math.min(100, baseScore + totalScore));
96
+ return { score: Math.round(adjustedScore), maxPossible, details };
97
+ }
98
+ export class SeoRulesEngine {
99
+ rules;
100
+ constructor(options = {}) {
101
+ let rules = [...ALL_SEO_RULES];
102
+ if (options.categories?.length) {
103
+ rules = rules.filter((r) => options.categories.includes(r.category));
104
+ }
105
+ if (options.excludeCategories?.length) {
106
+ rules = rules.filter((r) => !options.excludeCategories.includes(r.category));
107
+ }
108
+ if (options.rules?.length) {
109
+ rules = rules.filter((r) => options.rules.includes(r.id));
110
+ }
111
+ if (options.excludeRules?.length) {
112
+ rules = rules.filter((r) => !options.excludeRules.includes(r.id));
113
+ }
114
+ if (options.minSeverity) {
115
+ const severityOrder = ['info', 'warning', 'error'];
116
+ const minIndex = severityOrder.indexOf(options.minSeverity);
117
+ rules = rules.filter((r) => severityOrder.indexOf(r.severity) >= minIndex);
118
+ }
119
+ this.rules = rules;
120
+ }
121
+ evaluate(context) {
122
+ const results = [];
123
+ for (const rule of this.rules) {
124
+ const result = rule.check(context);
125
+ if (result) {
126
+ results.push(result);
127
+ }
128
+ }
129
+ return results;
130
+ }
131
+ getRules() {
132
+ return [...this.rules];
133
+ }
134
+ getRulesByCategory(category) {
135
+ return this.rules.filter((r) => r.category === category);
136
+ }
137
+ getCategories() {
138
+ return [...new Set(this.rules.map((r) => r.category))];
139
+ }
140
+ }
141
+ export function createRulesEngine(options) {
142
+ return new SeoRulesEngine(options);
143
+ }
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const internalLinkingRules: SeoRule[];