recker 1.0.27 → 1.0.28-next.32fe8ef

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 (46) 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 +1 -0
  5. package/dist/cli/tui/shell.js +157 -0
  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/types.d.ts +2 -1
  10. package/dist/seo/analyzer.d.ts +42 -0
  11. package/dist/seo/analyzer.js +715 -0
  12. package/dist/seo/index.d.ts +5 -0
  13. package/dist/seo/index.js +2 -0
  14. package/dist/seo/rules/accessibility.d.ts +2 -0
  15. package/dist/seo/rules/accessibility.js +128 -0
  16. package/dist/seo/rules/content.d.ts +2 -0
  17. package/dist/seo/rules/content.js +236 -0
  18. package/dist/seo/rules/images.d.ts +2 -0
  19. package/dist/seo/rules/images.js +180 -0
  20. package/dist/seo/rules/index.d.ts +20 -0
  21. package/dist/seo/rules/index.js +72 -0
  22. package/dist/seo/rules/links.d.ts +2 -0
  23. package/dist/seo/rules/links.js +150 -0
  24. package/dist/seo/rules/meta.d.ts +2 -0
  25. package/dist/seo/rules/meta.js +523 -0
  26. package/dist/seo/rules/mobile.d.ts +2 -0
  27. package/dist/seo/rules/mobile.js +71 -0
  28. package/dist/seo/rules/performance.d.ts +2 -0
  29. package/dist/seo/rules/performance.js +246 -0
  30. package/dist/seo/rules/schema.d.ts +2 -0
  31. package/dist/seo/rules/schema.js +54 -0
  32. package/dist/seo/rules/security.d.ts +2 -0
  33. package/dist/seo/rules/security.js +147 -0
  34. package/dist/seo/rules/structural.d.ts +2 -0
  35. package/dist/seo/rules/structural.js +155 -0
  36. package/dist/seo/rules/technical.d.ts +2 -0
  37. package/dist/seo/rules/technical.js +223 -0
  38. package/dist/seo/rules/thresholds.d.ts +196 -0
  39. package/dist/seo/rules/thresholds.js +118 -0
  40. package/dist/seo/rules/types.d.ts +191 -0
  41. package/dist/seo/rules/types.js +11 -0
  42. package/dist/seo/types.d.ts +160 -0
  43. package/dist/seo/types.js +1 -0
  44. package/dist/utils/columns.d.ts +14 -0
  45. package/dist/utils/columns.js +69 -0
  46. package/package.json +1 -1
@@ -0,0 +1,72 @@
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
+ export * from './types.js';
13
+ export * from './thresholds.js';
14
+ export const ALL_SEO_RULES = [
15
+ ...metaRules,
16
+ ...structuralRules,
17
+ ...contentRules,
18
+ ...imageRules,
19
+ ...linkRules,
20
+ ...performanceRules,
21
+ ...technicalRules,
22
+ ...securityRules,
23
+ ...schemaRules,
24
+ ...accessibilityRules,
25
+ ...mobileRules,
26
+ ];
27
+ export class SeoRulesEngine {
28
+ rules;
29
+ constructor(options = {}) {
30
+ let rules = [...ALL_SEO_RULES];
31
+ if (options.categories?.length) {
32
+ rules = rules.filter((r) => options.categories.includes(r.category));
33
+ }
34
+ if (options.excludeCategories?.length) {
35
+ rules = rules.filter((r) => !options.excludeCategories.includes(r.category));
36
+ }
37
+ if (options.rules?.length) {
38
+ rules = rules.filter((r) => options.rules.includes(r.id));
39
+ }
40
+ if (options.excludeRules?.length) {
41
+ rules = rules.filter((r) => !options.excludeRules.includes(r.id));
42
+ }
43
+ if (options.minSeverity) {
44
+ const severityOrder = ['info', 'warning', 'error'];
45
+ const minIndex = severityOrder.indexOf(options.minSeverity);
46
+ rules = rules.filter((r) => severityOrder.indexOf(r.severity) >= minIndex);
47
+ }
48
+ this.rules = rules;
49
+ }
50
+ evaluate(context) {
51
+ const results = [];
52
+ for (const rule of this.rules) {
53
+ const result = rule.check(context);
54
+ if (result) {
55
+ results.push(result);
56
+ }
57
+ }
58
+ return results;
59
+ }
60
+ getRules() {
61
+ return [...this.rules];
62
+ }
63
+ getRulesByCategory(category) {
64
+ return this.rules.filter((r) => r.category === category);
65
+ }
66
+ getCategories() {
67
+ return [...new Set(this.rules.map((r) => r.category))];
68
+ }
69
+ }
70
+ export function createRulesEngine(options) {
71
+ return new SeoRulesEngine(options);
72
+ }
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const linkRules: SeoRule[];
@@ -0,0 +1,150 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const linkRules = [
4
+ {
5
+ id: 'links-descriptive-text',
6
+ name: 'Link Text',
7
+ category: 'links',
8
+ severity: 'warning',
9
+ description: 'Links should have descriptive anchor text',
10
+ check: (ctx) => {
11
+ if (ctx.totalLinks === undefined || ctx.totalLinks === 0)
12
+ return null;
13
+ const withoutText = ctx.problematicLinks?.withoutText ?? [];
14
+ if (withoutText.length > 0) {
15
+ const examples = withoutText.slice(0, 3).map((l) => l.href).join(', ');
16
+ return createResult({ id: 'links-descriptive-text', name: 'Link Text', category: 'links', severity: 'warning' }, 'warn', `${withoutText.length} link(s) without descriptive text`, {
17
+ value: withoutText.length,
18
+ recommendation: 'Add descriptive anchor text to all links for better accessibility and SEO',
19
+ evidence: {
20
+ found: withoutText.map((l) => l.href),
21
+ example: '<a href="/page">Learn more about our services</a>',
22
+ impact: 'Screen readers cannot describe the link destination to users',
23
+ },
24
+ });
25
+ }
26
+ return createResult({ id: 'links-descriptive-text', name: 'Link Text', category: 'links', severity: 'warning' }, 'pass', 'All links have descriptive text');
27
+ },
28
+ },
29
+ {
30
+ id: 'links-generic-text',
31
+ name: 'Generic Link Text',
32
+ category: 'links',
33
+ severity: 'warning',
34
+ description: 'Avoid generic link text like "click here" or "read more"',
35
+ check: (ctx) => {
36
+ const genericLinks = ctx.problematicLinks?.genericText ?? [];
37
+ if (genericLinks.length > 0) {
38
+ return createResult({ id: 'links-generic-text', name: 'Generic Link Text', category: 'links', severity: 'warning' }, 'warn', `${genericLinks.length} link(s) with generic text`, {
39
+ value: genericLinks.length,
40
+ recommendation: 'Replace generic anchor text with descriptive text that explains where the link goes',
41
+ evidence: {
42
+ found: genericLinks.map((l) => `"${l.text}" → ${l.href}`),
43
+ issue: 'Generic text like "click here", "read more", "here" provides no context',
44
+ example: 'Instead of <a href="/docs">Click here</a>, use <a href="/docs">View documentation</a>',
45
+ },
46
+ });
47
+ }
48
+ return null;
49
+ },
50
+ },
51
+ {
52
+ id: 'links-internal-count',
53
+ name: 'Internal Links',
54
+ category: 'links',
55
+ severity: 'info',
56
+ description: 'Page should have at least 3 internal links',
57
+ check: (ctx) => {
58
+ if (ctx.internalLinks === undefined)
59
+ return null;
60
+ const min = SEO_THRESHOLDS.links.minInternal;
61
+ if (ctx.internalLinks < min) {
62
+ return createResult({ id: 'links-internal-count', name: 'Internal Links', category: 'links', severity: 'info' }, 'info', `Few internal links (${ctx.internalLinks})`, { value: ctx.internalLinks, recommendation: `Add at least ${min} internal links for better navigation` });
63
+ }
64
+ return createResult({ id: 'links-internal-count', name: 'Internal Links', category: 'links', severity: 'info' }, 'pass', `Good internal linking (${ctx.internalLinks} links)`, { value: ctx.internalLinks });
65
+ },
66
+ },
67
+ {
68
+ id: 'links-external-count',
69
+ name: 'External Links',
70
+ category: 'links',
71
+ severity: 'info',
72
+ description: 'Page should not have too many external links',
73
+ check: (ctx) => {
74
+ if (ctx.externalLinks === undefined)
75
+ return null;
76
+ const max = SEO_THRESHOLDS.links.maxExternal;
77
+ if (ctx.externalLinks > max) {
78
+ return createResult({ id: 'links-external-count', name: 'External Links', category: 'links', severity: 'info' }, 'warn', `Too many external links (${ctx.externalLinks})`, { value: ctx.externalLinks, recommendation: `Reduce external links to under ${max}` });
79
+ }
80
+ return null;
81
+ },
82
+ },
83
+ {
84
+ id: 'links-external-noopener',
85
+ name: 'External Links Noopener',
86
+ category: 'security',
87
+ severity: 'warning',
88
+ description: 'External links with target="_blank" should have rel="noopener"',
89
+ check: (ctx) => {
90
+ const missingNoopener = ctx.problematicLinks?.missingNoopener ?? [];
91
+ if (missingNoopener.length > 0) {
92
+ return createResult({ id: 'links-external-noopener', name: 'External Links Noopener', category: 'security', severity: 'warning' }, 'warn', `${missingNoopener.length} external link(s) missing rel="noopener"`, {
93
+ value: missingNoopener.length,
94
+ recommendation: 'Add rel="noopener" to all external links with target="_blank"',
95
+ evidence: {
96
+ found: missingNoopener.map((l) => l.href),
97
+ issue: 'Links with target="_blank" without rel="noopener" allow the new page to access window.opener',
98
+ impact: 'Security vulnerability: the linked page can redirect your page or access sensitive data',
99
+ example: '<a href="https://external.com" target="_blank" rel="noopener noreferrer">External Site</a>',
100
+ },
101
+ });
102
+ }
103
+ return null;
104
+ },
105
+ },
106
+ {
107
+ id: 'links-external-noreferrer',
108
+ name: 'External Links Noreferrer',
109
+ category: 'security',
110
+ severity: 'info',
111
+ description: 'External links may benefit from rel="noreferrer" for privacy',
112
+ check: (ctx) => {
113
+ const missingNoreferrer = ctx.problematicLinks?.missingNoreferrer ?? [];
114
+ if (missingNoreferrer.length > 3) {
115
+ return createResult({ id: 'links-external-noreferrer', name: 'External Links Noreferrer', category: 'security', severity: 'info' }, 'info', `${missingNoreferrer.length} external link(s) without rel="noreferrer"`, {
116
+ value: missingNoreferrer.length,
117
+ recommendation: 'Consider adding rel="noreferrer" to prevent referrer leakage to external sites',
118
+ evidence: {
119
+ found: missingNoreferrer.slice(0, 5).map((l) => l.href),
120
+ issue: 'External sites can see your page URL in their analytics via the Referer header',
121
+ example: '<a href="https://external.com" target="_blank" rel="noopener noreferrer">External</a>',
122
+ },
123
+ });
124
+ }
125
+ return null;
126
+ },
127
+ },
128
+ {
129
+ id: 'links-sponsored-ugc-directives',
130
+ name: 'Sponsored/UGC Links',
131
+ category: 'links',
132
+ severity: 'info',
133
+ description: 'Rel attributes `sponsored` and `ugc` should be used for paid or user-generated content links.',
134
+ check: (ctx) => {
135
+ if (!ctx.totalLinks)
136
+ return null;
137
+ let messages = [];
138
+ if (ctx.sponsoredLinks && ctx.sponsoredLinks > 0) {
139
+ messages.push(`${ctx.sponsoredLinks} link(s) with rel="sponsored".`);
140
+ }
141
+ if (ctx.ugcLinks && ctx.ugcLinks > 0) {
142
+ messages.push(`${ctx.ugcLinks} link(s) with rel="ugc".`);
143
+ }
144
+ if (messages.length > 0) {
145
+ return createResult({ id: 'links-sponsored-ugc-directives', name: 'Sponsored/UGC Links', category: 'links', severity: 'info' }, 'info', messages.join(' '), { recommendation: 'Ensure rel="sponsored" is used for paid links and rel="ugc" for user-generated content.' });
146
+ }
147
+ return null;
148
+ },
149
+ },
150
+ ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const metaRules: SeoRule[];