recker 1.0.30 → 1.0.32-next.02f2bae

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/cli/index.js +2653 -197
  2. package/dist/cli/tui/shell-search.js +10 -8
  3. package/dist/cli/tui/shell.d.ts +29 -0
  4. package/dist/cli/tui/shell.js +1733 -9
  5. package/dist/mcp/search/hybrid-search.js +4 -2
  6. package/dist/seo/analyzer.d.ts +7 -0
  7. package/dist/seo/analyzer.js +200 -4
  8. package/dist/seo/rules/ai-search.d.ts +2 -0
  9. package/dist/seo/rules/ai-search.js +423 -0
  10. package/dist/seo/rules/canonical.d.ts +12 -0
  11. package/dist/seo/rules/canonical.js +249 -0
  12. package/dist/seo/rules/crawl.js +113 -0
  13. package/dist/seo/rules/cwv.js +0 -95
  14. package/dist/seo/rules/i18n.js +27 -0
  15. package/dist/seo/rules/images.js +23 -27
  16. package/dist/seo/rules/index.js +14 -0
  17. package/dist/seo/rules/internal-linking.js +6 -6
  18. package/dist/seo/rules/links.js +321 -0
  19. package/dist/seo/rules/meta.js +24 -0
  20. package/dist/seo/rules/mobile.js +0 -20
  21. package/dist/seo/rules/performance.js +124 -0
  22. package/dist/seo/rules/redirects.d.ts +16 -0
  23. package/dist/seo/rules/redirects.js +193 -0
  24. package/dist/seo/rules/resources.d.ts +2 -0
  25. package/dist/seo/rules/resources.js +373 -0
  26. package/dist/seo/rules/security.js +290 -0
  27. package/dist/seo/rules/technical-advanced.d.ts +10 -0
  28. package/dist/seo/rules/technical-advanced.js +283 -0
  29. package/dist/seo/rules/technical.js +74 -18
  30. package/dist/seo/rules/types.d.ts +103 -3
  31. package/dist/seo/seo-spider.d.ts +2 -0
  32. package/dist/seo/seo-spider.js +47 -2
  33. package/dist/seo/types.d.ts +48 -28
  34. package/dist/seo/utils/index.d.ts +1 -0
  35. package/dist/seo/utils/index.js +1 -0
  36. package/dist/seo/utils/similarity.d.ts +47 -0
  37. package/dist/seo/utils/similarity.js +273 -0
  38. package/dist/seo/validators/index.d.ts +3 -0
  39. package/dist/seo/validators/index.js +3 -0
  40. package/dist/seo/validators/llms-txt.d.ts +57 -0
  41. package/dist/seo/validators/llms-txt.js +317 -0
  42. package/dist/seo/validators/robots.d.ts +54 -0
  43. package/dist/seo/validators/robots.js +382 -0
  44. package/dist/seo/validators/sitemap.d.ts +69 -0
  45. package/dist/seo/validators/sitemap.js +424 -0
  46. package/package.json +1 -1
@@ -0,0 +1,249 @@
1
+ import { createResult } from './types.js';
2
+ export const canonicalRules = [
3
+ {
4
+ id: 'canonical-present',
5
+ name: 'Canonical Tag Present',
6
+ category: 'canonicalization',
7
+ severity: 'warning',
8
+ description: 'Pages should have a canonical URL defined',
9
+ check: (ctx) => {
10
+ if (ctx.hasCanonical === undefined)
11
+ return null;
12
+ if (!ctx.hasCanonical) {
13
+ return createResult({ id: 'canonical-present', name: 'Canonical Tag Present', category: 'canonicalization', severity: 'warning' }, 'warn', 'Page is missing canonical tag', {
14
+ recommendation: 'Add <link rel="canonical" href="..."> to specify the preferred URL',
15
+ evidence: {
16
+ expected: '<link rel="canonical" href="https://example.com/page">',
17
+ impact: 'Without canonical, search engines may index duplicate versions'
18
+ }
19
+ });
20
+ }
21
+ return createResult({ id: 'canonical-present', name: 'Canonical Tag Present', category: 'canonicalization', severity: 'warning' }, 'pass', 'Canonical tag is present');
22
+ },
23
+ },
24
+ {
25
+ id: 'canonical-multiple',
26
+ name: 'Multiple Canonical Tags',
27
+ category: 'canonicalization',
28
+ severity: 'error',
29
+ description: 'Page should have only one canonical tag',
30
+ check: (ctx) => {
31
+ if (ctx.canonicalCount === undefined)
32
+ return null;
33
+ if (ctx.canonicalCount > 1) {
34
+ return createResult({ id: 'canonical-multiple', name: 'Multiple Canonical Tags', category: 'canonicalization', severity: 'error' }, 'fail', `Page has ${ctx.canonicalCount} canonical tags`, {
35
+ value: ctx.canonicalCount,
36
+ recommendation: 'Remove duplicate canonical tags; keep only one',
37
+ evidence: {
38
+ found: ctx.canonicalUrls || [],
39
+ expected: 'Single canonical tag',
40
+ impact: 'Multiple canonicals confuse search engines about preferred URL'
41
+ }
42
+ });
43
+ }
44
+ return null;
45
+ },
46
+ },
47
+ {
48
+ id: 'canonical-self-referencing',
49
+ name: 'Canonical Self-Reference',
50
+ category: 'canonicalization',
51
+ severity: 'info',
52
+ description: 'Canonical should typically point to the current page URL',
53
+ check: (ctx) => {
54
+ if (!ctx.canonicalUrl || !ctx.url)
55
+ return null;
56
+ const isSelfReferencing = normalizeUrl(ctx.canonicalUrl) === normalizeUrl(ctx.url);
57
+ if (!isSelfReferencing) {
58
+ return createResult({ id: 'canonical-self-referencing', name: 'Canonical Self-Reference', category: 'canonicalization', severity: 'info' }, 'info', 'Canonical points to different URL', {
59
+ recommendation: 'Verify this is intentional canonicalization',
60
+ evidence: {
61
+ found: ctx.canonicalUrl,
62
+ expected: ctx.url,
63
+ impact: 'This page signals to search engines that another URL is preferred'
64
+ }
65
+ });
66
+ }
67
+ return createResult({ id: 'canonical-self-referencing', name: 'Canonical Self-Reference', category: 'canonicalization', severity: 'info' }, 'pass', 'Canonical is self-referencing');
68
+ },
69
+ },
70
+ {
71
+ id: 'canonical-broken',
72
+ name: 'Broken Canonical URL',
73
+ category: 'canonicalization',
74
+ severity: 'error',
75
+ description: 'Canonical URL should be accessible (not 404)',
76
+ check: (ctx) => {
77
+ if (ctx.canonicalStatus === undefined)
78
+ return null;
79
+ if (ctx.canonicalStatus === 404) {
80
+ return createResult({ id: 'canonical-broken', name: 'Broken Canonical URL', category: 'canonicalization', severity: 'error' }, 'fail', 'Canonical URL returns 404', {
81
+ value: ctx.canonicalUrl,
82
+ recommendation: 'Fix canonical to point to an existing, accessible page',
83
+ evidence: {
84
+ found: `${ctx.canonicalUrl} → 404`,
85
+ expected: '200 OK',
86
+ impact: 'Broken canonical prevents proper indexing'
87
+ }
88
+ });
89
+ }
90
+ if (ctx.canonicalStatus >= 400) {
91
+ return createResult({ id: 'canonical-broken', name: 'Broken Canonical URL', category: 'canonicalization', severity: 'error' }, 'fail', `Canonical URL returns error (${ctx.canonicalStatus})`, {
92
+ value: ctx.canonicalUrl,
93
+ recommendation: 'Fix canonical to point to an accessible page',
94
+ evidence: {
95
+ found: `${ctx.canonicalUrl} → ${ctx.canonicalStatus}`,
96
+ expected: '200 OK',
97
+ impact: 'Canonical errors prevent proper indexing'
98
+ }
99
+ });
100
+ }
101
+ if (ctx.canonicalStatus >= 300 && ctx.canonicalFinalUrl) {
102
+ return createResult({ id: 'canonical-broken', name: 'Broken Canonical URL', category: 'canonicalization', severity: 'error' }, 'warn', 'Canonical URL redirects', {
103
+ value: ctx.canonicalUrl,
104
+ recommendation: 'Update canonical to final destination URL',
105
+ evidence: {
106
+ found: `${ctx.canonicalUrl} → ${ctx.canonicalFinalUrl}`,
107
+ expected: 'Direct URL without redirect',
108
+ impact: 'Canonical redirects waste crawl budget'
109
+ }
110
+ });
111
+ }
112
+ return createResult({ id: 'canonical-broken', name: 'Broken Canonical URL', category: 'canonicalization', severity: 'error' }, 'pass', 'Canonical URL is accessible');
113
+ },
114
+ },
115
+ {
116
+ id: 'canonical-protocol',
117
+ name: 'Canonical Protocol',
118
+ category: 'canonicalization',
119
+ severity: 'warning',
120
+ description: 'Canonical should use HTTPS protocol',
121
+ check: (ctx) => {
122
+ if (!ctx.canonicalUrl)
123
+ return null;
124
+ if (ctx.canonicalUrl.startsWith('http://')) {
125
+ return createResult({ id: 'canonical-protocol', name: 'Canonical Protocol', category: 'canonicalization', severity: 'warning' }, 'warn', 'Canonical uses HTTP instead of HTTPS', {
126
+ value: ctx.canonicalUrl,
127
+ recommendation: 'Update canonical to use HTTPS',
128
+ evidence: {
129
+ found: ctx.canonicalUrl,
130
+ expected: ctx.canonicalUrl.replace('http://', 'https://'),
131
+ impact: 'HTTP canonicals may cause indexing preference issues'
132
+ }
133
+ });
134
+ }
135
+ return createResult({ id: 'canonical-protocol', name: 'Canonical Protocol', category: 'canonicalization', severity: 'warning' }, 'pass', 'Canonical uses HTTPS');
136
+ },
137
+ },
138
+ {
139
+ id: 'canonical-absolute',
140
+ name: 'Canonical Absolute URL',
141
+ category: 'canonicalization',
142
+ severity: 'warning',
143
+ description: 'Canonical URL should be absolute, not relative',
144
+ check: (ctx) => {
145
+ if (!ctx.canonicalUrl)
146
+ return null;
147
+ const isRelative = !ctx.canonicalUrl.startsWith('http://') &&
148
+ !ctx.canonicalUrl.startsWith('https://') &&
149
+ !ctx.canonicalUrl.startsWith('//');
150
+ if (isRelative) {
151
+ return createResult({ id: 'canonical-absolute', name: 'Canonical Absolute URL', category: 'canonicalization', severity: 'warning' }, 'warn', 'Canonical URL is relative', {
152
+ value: ctx.canonicalUrl,
153
+ recommendation: 'Use absolute URL including protocol and domain',
154
+ evidence: {
155
+ found: ctx.canonicalUrl,
156
+ expected: 'https://example.com/page',
157
+ impact: 'Relative canonicals may be misinterpreted by crawlers'
158
+ }
159
+ });
160
+ }
161
+ return createResult({ id: 'canonical-absolute', name: 'Canonical Absolute URL', category: 'canonicalization', severity: 'warning' }, 'pass', 'Canonical URL is absolute');
162
+ },
163
+ },
164
+ {
165
+ id: 'canonical-chain',
166
+ name: 'Canonical Chain',
167
+ category: 'canonicalization',
168
+ severity: 'warning',
169
+ description: 'Canonical should not create chains',
170
+ check: (ctx) => {
171
+ if (ctx.canonicalChainLength === undefined)
172
+ return null;
173
+ if (ctx.canonicalChainLength > 1) {
174
+ return createResult({ id: 'canonical-chain', name: 'Canonical Chain', category: 'canonicalization', severity: 'warning' }, 'warn', `Canonical chain detected (${ctx.canonicalChainLength} hops)`, {
175
+ value: ctx.canonicalChainLength,
176
+ recommendation: 'Update canonical to point directly to final canonical URL',
177
+ evidence: {
178
+ found: ctx.canonicalChain || [],
179
+ expected: 'Direct canonical to final URL',
180
+ impact: 'Canonical chains may cause consolidation issues'
181
+ }
182
+ });
183
+ }
184
+ return null;
185
+ },
186
+ },
187
+ {
188
+ id: 'canonical-parameters',
189
+ name: 'Canonical Query Parameters',
190
+ category: 'canonicalization',
191
+ severity: 'info',
192
+ description: 'Canonical URLs with query parameters should be intentional',
193
+ check: (ctx) => {
194
+ if (!ctx.canonicalUrl)
195
+ return null;
196
+ try {
197
+ const url = new URL(ctx.canonicalUrl);
198
+ if (url.search && url.search.length > 1) {
199
+ return createResult({ id: 'canonical-parameters', name: 'Canonical Query Parameters', category: 'canonicalization', severity: 'info' }, 'info', 'Canonical contains query parameters', {
200
+ value: ctx.canonicalUrl,
201
+ recommendation: 'Verify query parameters in canonical are intentional',
202
+ evidence: {
203
+ found: url.search,
204
+ impact: 'Query parameters in canonicals may indicate tracking or filtering'
205
+ }
206
+ });
207
+ }
208
+ }
209
+ catch {
210
+ }
211
+ return null;
212
+ },
213
+ },
214
+ {
215
+ id: 'canonical-noindex-conflict',
216
+ name: 'Canonical + Noindex Conflict',
217
+ category: 'canonicalization',
218
+ severity: 'warning',
219
+ description: 'Pages with noindex should not have canonical to indexed page',
220
+ check: (ctx) => {
221
+ if (!ctx.hasCanonical || ctx.metaRobots === undefined)
222
+ return null;
223
+ const robots = Array.isArray(ctx.metaRobots) ? ctx.metaRobots : [ctx.metaRobots];
224
+ const hasNoindex = robots.some(r => r.toLowerCase().includes('noindex'));
225
+ if (hasNoindex && ctx.canonicalUrl) {
226
+ const isSelfReferencing = ctx.url && normalizeUrl(ctx.canonicalUrl) === normalizeUrl(ctx.url);
227
+ if (isSelfReferencing) {
228
+ return createResult({ id: 'canonical-noindex-conflict', name: 'Canonical + Noindex Conflict', category: 'canonicalization', severity: 'warning' }, 'warn', 'Page has both noindex and self-referencing canonical', {
229
+ recommendation: 'Remove canonical or noindex - conflicting signals',
230
+ evidence: {
231
+ found: `noindex + canonical to self (${ctx.canonicalUrl})`,
232
+ impact: 'Google ignores noindex if page has canonical to itself'
233
+ }
234
+ });
235
+ }
236
+ }
237
+ return null;
238
+ },
239
+ },
240
+ ];
241
+ function normalizeUrl(url) {
242
+ try {
243
+ const u = new URL(url);
244
+ return `${u.protocol}//${u.hostname}${u.pathname.replace(/\/$/, '')}${u.search}`.toLowerCase();
245
+ }
246
+ catch {
247
+ return url.toLowerCase();
248
+ }
249
+ }
@@ -304,4 +304,117 @@ export const crawlRules = [
304
304
  return null;
305
305
  },
306
306
  },
307
+ {
308
+ id: 'robots-txt-exists',
309
+ name: 'robots.txt Exists',
310
+ category: 'crawlability',
311
+ severity: 'info',
312
+ description: 'Website should have a robots.txt file',
313
+ check: (ctx) => {
314
+ if (ctx.robotsTxtExists === undefined)
315
+ return null;
316
+ if (!ctx.robotsTxtExists) {
317
+ return createResult({ id: 'robots-txt-exists', name: 'robots.txt Exists', category: 'crawlability', severity: 'info' }, 'info', 'No robots.txt file found', {
318
+ recommendation: 'Create a robots.txt file to control search engine crawling',
319
+ evidence: {
320
+ expected: '/robots.txt',
321
+ impact: 'Without robots.txt, search engines will crawl everything by default',
322
+ learnMore: 'https://developers.google.com/search/docs/crawling-indexing/robots/intro'
323
+ }
324
+ });
325
+ }
326
+ return createResult({ id: 'robots-txt-exists', name: 'robots.txt Exists', category: 'crawlability', severity: 'info' }, 'pass', 'robots.txt file exists');
327
+ },
328
+ },
329
+ {
330
+ id: 'sitemap-in-robots',
331
+ name: 'Sitemap Reference in robots.txt',
332
+ category: 'crawlability',
333
+ severity: 'warning',
334
+ description: 'robots.txt should reference sitemap.xml location',
335
+ check: (ctx) => {
336
+ if (ctx.robotsTxtHasSitemap === undefined)
337
+ return null;
338
+ if (!ctx.robotsTxtHasSitemap) {
339
+ return createResult({ id: 'sitemap-in-robots', name: 'Sitemap Reference in robots.txt', category: 'crawlability', severity: 'warning' }, 'warn', 'robots.txt does not reference sitemap.xml', {
340
+ recommendation: 'Add sitemap location to robots.txt',
341
+ evidence: {
342
+ expected: 'Sitemap: https://example.com/sitemap.xml',
343
+ impact: 'Search engines may not discover your sitemap automatically',
344
+ learnMore: 'https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap'
345
+ }
346
+ });
347
+ }
348
+ return createResult({ id: 'sitemap-in-robots', name: 'Sitemap Reference in robots.txt', category: 'crawlability', severity: 'warning' }, 'pass', 'Sitemap is referenced in robots.txt');
349
+ },
350
+ },
351
+ {
352
+ id: 'blocked-resources',
353
+ name: 'Blocked Resources',
354
+ category: 'crawlability',
355
+ severity: 'warning',
356
+ description: 'CSS/JS resources should not be blocked by robots.txt',
357
+ check: (ctx) => {
358
+ if (ctx.blockedResources === undefined)
359
+ return null;
360
+ if (ctx.blockedResources > 0) {
361
+ return createResult({ id: 'blocked-resources', name: 'Blocked Resources', category: 'crawlability', severity: 'warning' }, 'warn', `${ctx.blockedResources} resources blocked by robots.txt`, {
362
+ value: ctx.blockedResources,
363
+ recommendation: 'Update robots.txt to allow crawling of CSS and JS files',
364
+ evidence: {
365
+ found: `${ctx.blockedResources} blocked resources`,
366
+ expected: 'CSS, JS, and image files should be crawlable',
367
+ impact: 'Blocked resources prevent proper page rendering and indexing',
368
+ learnMore: 'https://developers.google.com/search/docs/crawling-indexing/robots/intro'
369
+ }
370
+ });
371
+ }
372
+ return null;
373
+ },
374
+ },
375
+ {
376
+ id: 'x-robots-tag-noindex',
377
+ name: 'X-Robots-Tag Noindex',
378
+ category: 'crawlability',
379
+ severity: 'warning',
380
+ description: 'X-Robots-Tag should not block important pages',
381
+ check: (ctx) => {
382
+ if (!ctx.xRobotsTag)
383
+ return null;
384
+ const tag = ctx.xRobotsTag.toLowerCase();
385
+ if (tag.includes('noindex')) {
386
+ return createResult({ id: 'x-robots-tag-noindex', name: 'X-Robots-Tag Noindex', category: 'crawlability', severity: 'warning' }, 'warn', 'Page blocked by X-Robots-Tag: noindex', {
387
+ value: ctx.xRobotsTag,
388
+ recommendation: 'Verify this page should not be indexed; remove X-Robots-Tag if unintentional',
389
+ evidence: {
390
+ found: `X-Robots-Tag: ${ctx.xRobotsTag}`,
391
+ impact: 'This page will not appear in search results'
392
+ }
393
+ });
394
+ }
395
+ return null;
396
+ },
397
+ },
398
+ {
399
+ id: 'blocked-external-resources',
400
+ name: 'Blocked External Resources',
401
+ category: 'crawlability',
402
+ severity: 'info',
403
+ description: 'External resources blocked by robots.txt may affect rendering',
404
+ check: (ctx) => {
405
+ if (ctx.blockedExternalResources === undefined)
406
+ return null;
407
+ if (ctx.blockedExternalResources > 0) {
408
+ return createResult({ id: 'blocked-external-resources', name: 'Blocked External Resources', category: 'crawlability', severity: 'info' }, 'info', `${ctx.blockedExternalResources} external resources blocked by robots.txt`, {
409
+ value: ctx.blockedExternalResources,
410
+ recommendation: 'Verify blocked resources are not critical for page rendering',
411
+ evidence: {
412
+ found: `${ctx.blockedExternalResources} blocked external resources`,
413
+ impact: 'If critical resources are blocked, search engines may not render pages correctly'
414
+ }
415
+ });
416
+ }
417
+ return null;
418
+ },
419
+ },
307
420
  ];
@@ -152,36 +152,6 @@ export const cwvRules = [
152
152
  return null;
153
153
  },
154
154
  },
155
- {
156
- id: 'cwv-cls-dynamic-content',
157
- name: 'Dynamic Content Space',
158
- category: 'performance',
159
- severity: 'info',
160
- description: 'Reserve space for dynamically injected content',
161
- check: (ctx) => {
162
- const potentialShifts = [];
163
- if (ctx.hasAdsWithoutReservedSpace) {
164
- potentialShifts.push('Ads without reserved space');
165
- }
166
- if (ctx.hasBannersWithoutMinHeight) {
167
- potentialShifts.push('Banners/alerts without min-height');
168
- }
169
- if (ctx.hasInfiniteScroll) {
170
- potentialShifts.push('Infinite scroll may cause shifts');
171
- }
172
- if (potentialShifts.length > 0) {
173
- return createResult({ id: 'cwv-cls-dynamic-content', name: 'Dynamic Content Space', category: 'performance', severity: 'info' }, 'info', 'Potential layout shift sources detected', {
174
- recommendation: 'Reserve space for dynamic content with min-height or skeleton loaders',
175
- evidence: {
176
- found: potentialShifts,
177
- example: '.ad-slot { min-height: 250px; }',
178
- impact: 'Dynamic content is a major source of CLS',
179
- },
180
- });
181
- }
182
- return null;
183
- },
184
- },
185
155
  {
186
156
  id: 'cwv-cls-font-fallback',
187
157
  name: 'Font Fallback Metrics',
@@ -210,71 +180,6 @@ export const cwvRules = [
210
180
  return null;
211
181
  },
212
182
  },
213
- {
214
- id: 'cwv-inp-main-thread',
215
- name: 'Main Thread Blocking',
216
- category: 'performance',
217
- severity: 'warning',
218
- description: 'Large inline scripts can block the main thread',
219
- check: (ctx) => {
220
- if (ctx.largeInlineScripts === undefined)
221
- return null;
222
- if (ctx.largeInlineScripts > 0) {
223
- return createResult({ id: 'cwv-inp-main-thread', name: 'Main Thread Blocking', category: 'performance', severity: 'warning' }, 'warn', `${ctx.largeInlineScripts} large inline script(s) may block main thread`, {
224
- recommendation: 'Move large scripts to external files with defer',
225
- evidence: {
226
- found: `${ctx.largeInlineScripts} inline scripts > 10KB`,
227
- expected: 'Small inline scripts only for critical initialization',
228
- impact: 'Large scripts block main thread and delay interactions',
229
- learnMore: 'https://web.dev/optimize-inp/',
230
- },
231
- });
232
- }
233
- return null;
234
- },
235
- },
236
- {
237
- id: 'cwv-inp-event-handlers',
238
- name: 'Inline Event Handlers',
239
- category: 'performance',
240
- severity: 'info',
241
- description: 'Avoid inline event handlers for better performance',
242
- check: (ctx) => {
243
- if (ctx.inlineEventHandlers === undefined)
244
- return null;
245
- if (ctx.inlineEventHandlers > 10) {
246
- return createResult({ id: 'cwv-inp-event-handlers', name: 'Inline Event Handlers', category: 'performance', severity: 'info' }, 'info', `${ctx.inlineEventHandlers} inline event handlers found`, {
247
- recommendation: 'Use event delegation instead of inline handlers',
248
- evidence: {
249
- found: ctx.inlineEventHandlers,
250
- expected: 'Minimal inline handlers, prefer addEventListener',
251
- example: 'document.addEventListener("click", handleClick)',
252
- impact: 'Many inline handlers increase parsing time',
253
- },
254
- });
255
- }
256
- return null;
257
- },
258
- },
259
- {
260
- id: 'cwv-inp-heavy-animations',
261
- name: 'Heavy Animations',
262
- category: 'performance',
263
- severity: 'info',
264
- description: 'Animations should use transform/opacity for best performance',
265
- check: (ctx) => {
266
- if (!ctx.hasHeavyAnimations)
267
- return null;
268
- return createResult({ id: 'cwv-inp-heavy-animations', name: 'Heavy Animations', category: 'performance', severity: 'info' }, 'info', 'Potentially expensive animations detected', {
269
- recommendation: 'Use transform and opacity for animations, avoid layout properties',
270
- evidence: {
271
- issue: 'Animating width, height, top, left triggers layout',
272
- expected: 'Animate transform: translate(), scale(), rotate() and opacity',
273
- learnMore: 'https://web.dev/animations-guide/',
274
- },
275
- });
276
- },
277
- },
278
183
  {
279
184
  id: 'cwv-resource-hints',
280
185
  name: 'Resource Hints',
@@ -219,4 +219,31 @@ export const i18nRules = [
219
219
  return null;
220
220
  },
221
221
  },
222
+ {
223
+ id: 'hreflang-language-mismatch',
224
+ name: 'Hreflang Language Mismatch',
225
+ category: 'technical',
226
+ severity: 'warning',
227
+ description: 'Hreflang language should match page content language',
228
+ check: (ctx) => {
229
+ if (!ctx.hreflangTags || !ctx.detectedLanguage)
230
+ return null;
231
+ const selfHreflang = ctx.hreflangTags.find(tag => tag.href === ctx.url || tag.href === ctx.canonicalUrl);
232
+ if (selfHreflang && ctx.detectedLanguage) {
233
+ const hreflangLang = selfHreflang.lang.split('-')[0].toLowerCase();
234
+ const detectedLang = ctx.detectedLanguage.toLowerCase();
235
+ if (hreflangLang !== detectedLang && hreflangLang !== 'x-default') {
236
+ return createResult({ id: 'hreflang-language-mismatch', name: 'Hreflang Language Mismatch', category: 'technical', severity: 'warning' }, 'warn', `Hreflang declares "${selfHreflang.lang}" but content appears to be "${ctx.detectedLanguage}"`, {
237
+ recommendation: 'Verify the hreflang attribute matches the actual page language',
238
+ evidence: {
239
+ found: `hreflang="${selfHreflang.lang}"`,
240
+ expected: `Content language: ${ctx.detectedLanguage}`,
241
+ impact: 'Language mismatch may confuse search engines and affect international SEO'
242
+ }
243
+ });
244
+ }
245
+ }
246
+ return null;
247
+ },
248
+ },
222
249
  ];
@@ -81,33 +81,6 @@ export const imageRules = [
81
81
  return null;
82
82
  },
83
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
84
  {
112
85
  id: 'images-alt-length',
113
86
  name: 'Alt Text Length',
@@ -177,4 +150,27 @@ export const imageRules = [
177
150
  return null;
178
151
  },
179
152
  },
153
+ {
154
+ id: 'broken-external-images',
155
+ name: 'Broken External Images',
156
+ category: 'images',
157
+ severity: 'warning',
158
+ description: 'External images should be accessible',
159
+ check: (ctx) => {
160
+ if (ctx.brokenExternalImages === undefined)
161
+ return null;
162
+ if (ctx.brokenExternalImages > 0) {
163
+ return createResult({ id: 'broken-external-images', name: 'Broken External Images', category: 'images', severity: 'warning' }, 'warn', `${ctx.brokenExternalImages} broken external images`, {
164
+ value: ctx.brokenExternalImages,
165
+ recommendation: 'Fix or remove broken external image references',
166
+ evidence: {
167
+ found: ctx.brokenExternalImageUrls?.slice(0, 5) || [],
168
+ expected: 'All images should load successfully',
169
+ impact: 'Broken images negatively affect user experience and may signal poor maintenance'
170
+ }
171
+ });
172
+ }
173
+ return null;
174
+ },
175
+ },
180
176
  ];
@@ -19,6 +19,11 @@ import { pwaRules } from './pwa.js';
19
19
  import { socialRules } from './social.js';
20
20
  import { internalLinkingRules } from './internal-linking.js';
21
21
  import { bestPracticesRules } from './best-practices.js';
22
+ import { aiSearchRules } from './ai-search.js';
23
+ import { resourceRules } from './resources.js';
24
+ import { technicalAdvancedRules } from './technical-advanced.js';
25
+ import { redirectRules } from './redirects.js';
26
+ import { canonicalRules } from './canonical.js';
22
27
  export * from './types.js';
23
28
  export * from './thresholds.js';
24
29
  export const ALL_SEO_RULES = [
@@ -43,6 +48,11 @@ export const ALL_SEO_RULES = [
43
48
  ...socialRules,
44
49
  ...internalLinkingRules,
45
50
  ...bestPracticesRules,
51
+ ...aiSearchRules,
52
+ ...resourceRules,
53
+ ...technicalAdvancedRules,
54
+ ...redirectRules,
55
+ ...canonicalRules,
46
56
  ];
47
57
  export const SCORING_WEIGHTS = {
48
58
  severity: {
@@ -65,6 +75,10 @@ export const SCORING_WEIGHTS = {
65
75
  'structured-data': 1.0,
66
76
  performance: 1.4,
67
77
  accessibility: 0.8,
78
+ 'ai-search': 0.7,
79
+ resources: 1.1,
80
+ crawlability: 1.3,
81
+ canonicalization: 1.2,
68
82
  },
69
83
  };
70
84
  export function calculateWeightedScore(results) {
@@ -286,11 +286,11 @@ export const internalLinkingRules = [
286
286
  check: (ctx) => {
287
287
  if (ctx.brokenInternalLinks === undefined)
288
288
  return null;
289
- if (ctx.brokenInternalLinks > 0) {
290
- return createResult({ id: 'linking-broken-internal', name: 'Broken Internal Links', category: 'links', severity: 'error' }, 'fail', `${ctx.brokenInternalLinks} broken internal link(s)`, {
289
+ if (ctx.brokenInternalLinks.length > 0) {
290
+ return createResult({ id: 'linking-broken-internal', name: 'Broken Internal Links', category: 'links', severity: 'error' }, 'fail', `${ctx.brokenInternalLinks.length} broken internal link(s)`, {
291
291
  recommendation: 'Fix or remove broken internal links',
292
292
  evidence: {
293
- found: ctx.brokenInternalLinks,
293
+ found: ctx.brokenInternalLinks.slice(0, 5),
294
294
  expected: '0 broken links',
295
295
  impact: 'Broken links waste crawl budget and harm user experience',
296
296
  },
@@ -308,11 +308,11 @@ export const internalLinkingRules = [
308
308
  check: (ctx) => {
309
309
  if (ctx.redirectChainLinks === undefined)
310
310
  return null;
311
- if (ctx.redirectChainLinks > 0) {
312
- return createResult({ id: 'linking-redirect-chains', name: 'Redirect Chains', category: 'links', severity: 'warning' }, 'warn', `${ctx.redirectChainLinks} link(s) go through redirects`, {
311
+ if (ctx.redirectChainLinks.length > 0) {
312
+ return createResult({ id: 'linking-redirect-chains', name: 'Redirect Chains', category: 'links', severity: 'warning' }, 'warn', `${ctx.redirectChainLinks.length} link(s) go through redirects`, {
313
313
  recommendation: 'Update links to point to final destination URLs',
314
314
  evidence: {
315
- found: ctx.redirectChainLinks,
315
+ found: ctx.redirectChainLinks.slice(0, 5).map(r => `${r.from} → ${r.to} (${r.hops} hops)`),
316
316
  expected: '0 redirect chain links',
317
317
  impact: 'Redirect chains slow down crawling and lose link equity',
318
318
  },