recker 1.0.78 → 1.0.79-next.70f027c

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 (191) hide show
  1. package/dist/browser/browser/mini.d.ts +2 -2
  2. package/dist/browser/browser/recker-mini.d.ts +8 -8
  3. package/dist/browser/browser/recker.d.ts +11 -8
  4. package/dist/browser/browser/recker.js +54 -6
  5. package/dist/browser/core/client.d.ts +15 -10
  6. package/dist/browser/core/client.js +54 -38
  7. package/dist/browser/index.iife.min.js +129 -130
  8. package/dist/browser/index.min.js +129 -130
  9. package/dist/browser/index.mini.iife.js +5697 -636
  10. package/dist/browser/index.mini.iife.min.js +47 -48
  11. package/dist/browser/index.mini.min.js +47 -48
  12. package/dist/browser/index.mini.umd.js +5697 -636
  13. package/dist/browser/index.mini.umd.min.js +47 -48
  14. package/dist/browser/index.umd.min.js +129 -130
  15. package/dist/browser/mini.d.ts +2 -2
  16. package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
  17. package/dist/browser/plugins/proxy-rotator.js +6 -28
  18. package/dist/browser/recker-mini.d.ts +8 -8
  19. package/dist/browser/recker.d.ts +11 -8
  20. package/dist/browser/recker.js +54 -6
  21. package/dist/browser/scrape/document.js +2 -2
  22. package/dist/browser/scrape/element.js +7 -1
  23. package/dist/browser/scrape/parser/nodes/html.js +1 -1
  24. package/dist/browser/scrape/spider.d.ts +52 -0
  25. package/dist/browser/scrape/spider.js +620 -38
  26. package/dist/browser/scrape/types.d.ts +2 -0
  27. package/dist/browser/search/google.d.ts +26 -1
  28. package/dist/browser/search/google.js +427 -45
  29. package/dist/browser/seo/analyzer.d.ts +2 -0
  30. package/dist/browser/seo/analyzer.js +190 -5
  31. package/dist/browser/seo/formatter.js +20 -2
  32. package/dist/browser/seo/index.d.ts +1 -1
  33. package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
  34. package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
  35. package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  36. package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
  37. package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
  38. package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
  39. package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
  40. package/dist/browser/seo/keyword-campaign-shared.js +59 -0
  41. package/dist/browser/seo/keyword-campaign.d.ts +4 -107
  42. package/dist/browser/seo/keyword-campaign.js +2 -380
  43. package/dist/browser/seo/keywords.js +5 -22
  44. package/dist/browser/seo/types.d.ts +26 -0
  45. package/dist/browser/transport/curl.d.ts +5 -1
  46. package/dist/browser/transport/curl.js +207 -50
  47. package/dist/browser/transport/undici.d.ts +4 -3
  48. package/dist/browser/transport/undici.js +123 -49
  49. package/dist/browser/types/index.d.ts +9 -3
  50. package/dist/browser/utils/binary-manager.js +26 -3
  51. package/dist/browser/utils/block-detector.d.ts +8 -0
  52. package/dist/browser/utils/block-detector.js +542 -7
  53. package/dist/cli/commands/hls-runner.js +5 -4
  54. package/dist/cli/commands/live-runner.js +5 -4
  55. package/dist/cli/commands/loadtest-runner.js +3 -2
  56. package/dist/cli/commands/search.d.ts +2 -0
  57. package/dist/cli/commands/search.js +105 -0
  58. package/dist/cli/commands/seo-runner.d.ts +2 -0
  59. package/dist/cli/commands/seo-runner.js +13 -7
  60. package/dist/cli/commands/seo.js +140 -1
  61. package/dist/cli/commands/serve.js +75 -131
  62. package/dist/cli/commands/server-runner.js +59 -82
  63. package/dist/cli/commands/spider-runner.d.ts +37 -1
  64. package/dist/cli/commands/spider-runner.js +134 -10
  65. package/dist/cli/commands/spider.d.ts +18 -1
  66. package/dist/cli/commands/spider.js +457 -27
  67. package/dist/cli/events/handlers/cli.js +30 -1
  68. package/dist/cli/events/handlers/tui.js +26 -0
  69. package/dist/cli/events/types.d.ts +27 -0
  70. package/dist/cli/handler.d.ts +2 -12
  71. package/dist/cli/handler.js +20 -15
  72. package/dist/cli/handlers/protocols.js +39 -12
  73. package/dist/cli/handlers/search.d.ts +2 -0
  74. package/dist/cli/handlers/search.js +171 -0
  75. package/dist/cli/handlers/seo-analyze.d.ts +1 -0
  76. package/dist/cli/handlers/seo-analyze.js +666 -0
  77. package/dist/cli/handlers/seo-robots.d.ts +1 -0
  78. package/dist/cli/handlers/seo-robots.js +76 -0
  79. package/dist/cli/handlers/seo-serp.d.ts +54 -0
  80. package/dist/cli/handlers/seo-serp.js +243 -0
  81. package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
  82. package/dist/cli/handlers/seo-sitemap.js +55 -0
  83. package/dist/cli/handlers/seo-spider.d.ts +1 -0
  84. package/dist/cli/handlers/seo-spider.js +334 -0
  85. package/dist/cli/handlers/seo.d.ts +8 -4
  86. package/dist/cli/handlers/seo.js +294 -436
  87. package/dist/cli/handlers/spider.js +94 -17
  88. package/dist/cli/handlers/streaming.js +5 -1
  89. package/dist/cli/index.js +11 -2
  90. package/dist/cli/presets.d.ts +1 -1
  91. package/dist/cli/presets.js +15 -4
  92. package/dist/cli/tui/app.js +6 -1
  93. package/dist/cli/tui/components/rich-response.d.ts +74 -0
  94. package/dist/cli/tui/components/rich-response.js +118 -1
  95. package/dist/cli/tui/executor-commands/background.js +30 -24
  96. package/dist/cli/tui/executor-commands/testing.js +3 -2
  97. package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
  98. package/dist/cli/tui/hooks/useHelp.js +15 -2
  99. package/dist/cli/tui/job-manager.d.ts +4 -4
  100. package/dist/cli/tui/job-manager.js +5 -1
  101. package/dist/cli/tui/spider-tui.d.ts +63 -0
  102. package/dist/cli/tui/spider-tui.js +120 -2
  103. package/dist/cli/types.d.ts +12 -0
  104. package/dist/cli/types.js +1 -0
  105. package/dist/cli/utils/option-helpers.d.ts +10 -0
  106. package/dist/cli/utils/option-helpers.js +63 -0
  107. package/dist/cli/utils/score-color.d.ts +11 -0
  108. package/dist/cli/utils/score-color.js +11 -0
  109. package/dist/cli/utils/serp-campaign.d.ts +53 -0
  110. package/dist/cli/utils/serp-campaign.js +53 -0
  111. package/dist/cli/utils/serp-config.d.ts +26 -0
  112. package/dist/cli/utils/serp-config.js +125 -0
  113. package/dist/core/client.d.ts +15 -10
  114. package/dist/core/client.js +54 -38
  115. package/dist/mcp/cli.js +35 -34
  116. package/dist/mcp/client.d.ts +2 -2
  117. package/dist/mcp/client.js +20 -2
  118. package/dist/mcp/contract.d.ts +1 -1
  119. package/dist/mcp/profiles.js +5 -1
  120. package/dist/mcp/prompts/index.js +8 -4
  121. package/dist/mcp/resources/index.js +46 -23
  122. package/dist/mcp/server.js +9 -6
  123. package/dist/mcp/tools/protocols.js +9 -2
  124. package/dist/mcp/tools/registry.js +13 -3
  125. package/dist/mcp/tools/seo.js +427 -2
  126. package/dist/mcp/types.d.ts +5 -1
  127. package/dist/plugins/proxy-rotator.d.ts +2 -2
  128. package/dist/plugins/proxy-rotator.js +6 -28
  129. package/dist/recker.d.ts +10 -7
  130. package/dist/recker.js +56 -6
  131. package/dist/scrape/document.js +2 -2
  132. package/dist/scrape/element.js +7 -1
  133. package/dist/scrape/parser/nodes/html.js +1 -1
  134. package/dist/scrape/spider.d.ts +52 -0
  135. package/dist/scrape/spider.js +620 -38
  136. package/dist/scrape/types.d.ts +2 -0
  137. package/dist/search/google.d.ts +26 -1
  138. package/dist/search/google.js +427 -45
  139. package/dist/search/index.d.ts +1 -1
  140. package/dist/seo/analyzer.d.ts +2 -0
  141. package/dist/seo/analyzer.js +190 -5
  142. package/dist/seo/formatter.js +20 -2
  143. package/dist/seo/index.d.ts +1 -1
  144. package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
  145. package/dist/seo/keyword-campaign-analyzer.js +538 -0
  146. package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  147. package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
  148. package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
  149. package/dist/seo/keyword-campaign-seed-core.js +525 -0
  150. package/dist/seo/keyword-campaign-shared.d.ts +165 -0
  151. package/dist/seo/keyword-campaign-shared.js +59 -0
  152. package/dist/seo/keyword-campaign.d.ts +4 -107
  153. package/dist/seo/keyword-campaign.js +2 -380
  154. package/dist/seo/keywords.js +5 -22
  155. package/dist/seo/types.d.ts +26 -0
  156. package/dist/template/index.d.ts +1 -1
  157. package/dist/template/types.d.ts +0 -2
  158. package/dist/testing/index.d.ts +1 -22
  159. package/dist/testing/index.js +1 -11
  160. package/dist/transport/curl.d.ts +5 -1
  161. package/dist/transport/curl.js +207 -50
  162. package/dist/transport/undici.d.ts +4 -3
  163. package/dist/transport/undici.js +123 -49
  164. package/dist/types/index.d.ts +9 -3
  165. package/dist/utils/binary-manager.js +26 -3
  166. package/dist/utils/block-detector.d.ts +8 -0
  167. package/dist/utils/block-detector.js +542 -7
  168. package/dist/version.js +1 -1
  169. package/package.json +8 -1
  170. package/dist/testing/mock-dns-server.d.ts +0 -69
  171. package/dist/testing/mock-dns-server.js +0 -269
  172. package/dist/testing/mock-ftp-server.d.ts +0 -89
  173. package/dist/testing/mock-ftp-server.js +0 -562
  174. package/dist/testing/mock-hls-server.d.ts +0 -80
  175. package/dist/testing/mock-hls-server.js +0 -381
  176. package/dist/testing/mock-http-server.d.ts +0 -124
  177. package/dist/testing/mock-http-server.js +0 -343
  178. package/dist/testing/mock-proxy-server.d.ts +0 -108
  179. package/dist/testing/mock-proxy-server.js +0 -615
  180. package/dist/testing/mock-sse-server.d.ts +0 -76
  181. package/dist/testing/mock-sse-server.js +0 -291
  182. package/dist/testing/mock-telnet-server.d.ts +0 -59
  183. package/dist/testing/mock-telnet-server.js +0 -274
  184. package/dist/testing/mock-udp-server.d.ts +0 -43
  185. package/dist/testing/mock-udp-server.js +0 -188
  186. package/dist/testing/mock-websocket-server.d.ts +0 -76
  187. package/dist/testing/mock-websocket-server.js +0 -334
  188. package/dist/testing/mock-whois-server.d.ts +0 -56
  189. package/dist/testing/mock-whois-server.js +0 -234
  190. package/dist/testing/proxy-certs.d.ts +0 -19
  191. package/dist/testing/proxy-certs.js +0 -208
@@ -39,6 +39,7 @@ export class SeoAnalyzer {
39
39
  const socialLinkDetails = this.analyzeSocialLinks(links, socialDomains);
40
40
  const headings = this.analyzeHeadings();
41
41
  const content = this.analyzeContent(headings);
42
+ const contentSections = this.extractContentSectionSignals(headings);
42
43
  const linkAnalysis = this.buildLinkAnalysis(links);
43
44
  const imageAnalysis = this.buildImageAnalysis(images);
44
45
  const social = this.buildSocialAnalysis(og, twitter);
@@ -48,6 +49,16 @@ export class SeoAnalyzer {
48
49
  const feeds = this.analyzeFeeds();
49
50
  const conversion = this.analyzeConversionElements(links, visibleText);
50
51
  const pageType = this.detectPageType(jsonLd);
52
+ const linkAnchorTexts = links
53
+ .map((link) => link.text?.trim())
54
+ .filter((text) => Boolean(text))
55
+ .filter((text) => text.length <= 140)
56
+ .filter((text) => !text.match(/^\s*$/))
57
+ .slice(0, 80);
58
+ const linkUrlSamples = links
59
+ .map((link) => extractReadablePathFromUrl(link.href))
60
+ .filter((sample) => sample.length > 0)
61
+ .slice(0, 80);
51
62
  const context = this.buildRuleContext({
52
63
  pageType,
53
64
  meta,
@@ -71,7 +82,7 @@ export class SeoAnalyzer {
71
82
  const ruleResults = this.rulesEngine.evaluate(context);
72
83
  const checks = this.convertToCheckResults(ruleResults);
73
84
  const { score, grade } = this.calculateScore(ruleResults);
74
- const summary = this.buildSummary(ruleResults, checks, {
85
+ const summary = this.buildSummary(checks, {
75
86
  content,
76
87
  imageAnalysis,
77
88
  linkAnalysis,
@@ -124,12 +135,15 @@ export class SeoAnalyzer {
124
135
  items: jsonLd,
125
136
  },
126
137
  headings: headings,
138
+ contentSections,
127
139
  content,
128
140
  keywords,
129
141
  links: linkAnalysis,
130
142
  images: imageAnalysis,
131
143
  social,
132
144
  technical,
145
+ linkAnchorTexts,
146
+ linkUrlSamples,
133
147
  };
134
148
  }
135
149
  getMainBody() {
@@ -181,6 +195,97 @@ export class SeoAnalyzer {
181
195
  return 'other';
182
196
  }
183
197
  }
198
+ extractContentSectionSignals(headings) {
199
+ const body = this.getMainBody();
200
+ if (!body)
201
+ return [];
202
+ const candidateTags = 'h1,h2,h3,h4,h5,h6,p,li,ol,ul,article,section,main,figure,figcaption,blockquote,table,th,td';
203
+ const nodes = body.querySelectorAll(candidateTags);
204
+ const normalizeSignalText = (value) => value
205
+ .replace(/\s+/g, ' ')
206
+ .replace(/[,\.;:!?()[\]{}"“”']/g, '')
207
+ .trim()
208
+ .toLowerCase();
209
+ const headingStack = [];
210
+ const seen = new Set();
211
+ const results = [];
212
+ const pushSectionSignal = (entry) => {
213
+ const fingerprint = `${entry.tagName}|${entry.headingPath.join(' > ')}|${entry.text.slice(0, 120)}`;
214
+ if (seen.has(fingerprint))
215
+ return;
216
+ seen.add(fingerprint);
217
+ results.push(entry);
218
+ };
219
+ for (const node of nodes) {
220
+ const tagName = node.tagName.toLowerCase();
221
+ if (/^h[1-6]$/.test(tagName)) {
222
+ const level = Number.parseInt(tagName.slice(1), 10);
223
+ const text = normalizeSignalText(node.text);
224
+ if (!text || text.length < 4) {
225
+ continue;
226
+ }
227
+ while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
228
+ headingStack.pop();
229
+ }
230
+ headingStack.push({ level, text });
231
+ const headingPath = headingStack.map((entry) => entry.text);
232
+ const source = {
233
+ headingPath,
234
+ source: 'heading',
235
+ tagName,
236
+ headingLevel: level,
237
+ text,
238
+ wordCount: text.split(/\s+/).length,
239
+ linkDensity: 0,
240
+ weight: 1.45,
241
+ };
242
+ pushSectionSignal(source);
243
+ continue;
244
+ }
245
+ if (tagName === 'ul' || tagName === 'ol') {
246
+ continue;
247
+ }
248
+ const text = normalizeSignalText(node.text);
249
+ if (!text || text.split(/\s+/).length < 4) {
250
+ continue;
251
+ }
252
+ const words = text.split(/\s+/);
253
+ const wordCount = words.length;
254
+ if (wordCount < 4)
255
+ continue;
256
+ const anchorCount = node.querySelectorAll('a').length;
257
+ const linkDensity = wordCount > 0
258
+ ? Math.min(1, anchorCount / Math.max(1, wordCount))
259
+ : 0;
260
+ if (linkDensity > 0.85)
261
+ continue;
262
+ const headingPath = headingStack.length > 0
263
+ ? headingStack.map((entry) => entry.text)
264
+ : headings.h1Count > 0
265
+ ? ['conteúdo']
266
+ : ['home'];
267
+ const sourceLabel = tagName === 'li' ? 'list-item'
268
+ : tagName === 'figure' || tagName === 'figcaption' ? 'figure'
269
+ : (tagName === 'table' || tagName === 'th' || tagName === 'td') ? 'table'
270
+ : 'paragraph';
271
+ const headingLevel = headingStack.length > 0 ? headingStack[headingStack.length - 1].level : 1;
272
+ const baseWeight = sourceLabel === 'paragraph' ? 1.25 : 1.05;
273
+ const adjustedWeight = baseWeight * (1 - (linkDensity * 0.4));
274
+ pushSectionSignal({
275
+ headingPath,
276
+ source: sourceLabel,
277
+ tagName,
278
+ headingLevel,
279
+ text,
280
+ wordCount,
281
+ linkDensity,
282
+ weight: Math.max(0.5, adjustedWeight),
283
+ });
284
+ }
285
+ return results
286
+ .filter((entry) => entry.text.length >= 20)
287
+ .slice(0, 160);
288
+ }
184
289
  getVisibleText() {
185
290
  const body = this.getMainBody();
186
291
  if (!body)
@@ -951,6 +1056,27 @@ export class SeoAnalyzer {
951
1056
  }
952
1057
  return undefined;
953
1058
  }
1059
+ classifyInfoCheck(result) {
1060
+ if (result.status !== 'info')
1061
+ return undefined;
1062
+ const message = (result.message || '').toLowerCase();
1063
+ const evidenceIssue = (result.evidence?.issue || '').toLowerCase();
1064
+ const recommendation = (result.recommendation || '').toLowerCase();
1065
+ const context = `${message} ${evidenceIssue} ${recommendation}`;
1066
+ if (/not\s+applicable/.test(context) ||
1067
+ /not\s+available/.test(context) ||
1068
+ /data\s+unavailable/.test(context) ||
1069
+ /context\s+unavailable/.test(context) ||
1070
+ /unable\s+to\s+check/.test(context) ||
1071
+ /cannot\s+verify/.test(context) ||
1072
+ /cannot\s+be\s+determined/.test(context) ||
1073
+ /\bunavailable\b/.test(context) ||
1074
+ /not\s+set/.test(context) ||
1075
+ /not\s+present/.test(context)) {
1076
+ return 'not_applicable';
1077
+ }
1078
+ return 'suggestion';
1079
+ }
954
1080
  convertToCheckResults(results) {
955
1081
  return results.map((r) => ({
956
1082
  id: r.id,
@@ -959,26 +1085,36 @@ export class SeoAnalyzer {
959
1085
  severity: r.severity,
960
1086
  status: r.status,
961
1087
  message: r.message,
1088
+ infoType: this.classifyInfoCheck(r),
962
1089
  value: r.value,
963
1090
  recommendation: r.recommendation,
964
1091
  evidence: r.evidence,
965
1092
  }));
966
1093
  }
967
- buildSummary(ruleResults, checks, data) {
1094
+ buildSummary(checks, data) {
968
1095
  const pageType = data.pageType;
969
1096
  const timings = data.timings;
970
1097
  const passed = checks.filter((c) => c.status === 'pass').length;
971
1098
  const warnings = checks.filter((c) => c.status === 'warn').length;
972
1099
  const errors = checks.filter((c) => c.status === 'fail').length;
973
1100
  const infos = checks.filter((c) => c.status === 'info').length;
1101
+ const notApplicable = checks.filter((c) => c.status === 'info' && c.infoType === 'not_applicable').length;
1102
+ const suggestions = checks.filter((c) => c.status === 'info' && c.infoType !== 'not_applicable').length;
974
1103
  const totalChecks = checks.length;
975
1104
  const scoringChecks = totalChecks - infos;
976
1105
  const passRate = scoringChecks > 0 ? Math.round((passed / scoringChecks) * 100) : 100;
977
1106
  const issuesByCategory = {};
978
- for (const result of ruleResults) {
1107
+ for (const result of checks) {
979
1108
  const cat = result.category;
980
1109
  if (!issuesByCategory[cat]) {
981
- issuesByCategory[cat] = { passed: 0, warnings: 0, errors: 0 };
1110
+ issuesByCategory[cat] = {
1111
+ passed: 0,
1112
+ warnings: 0,
1113
+ errors: 0,
1114
+ infos: 0,
1115
+ notApplicable: 0,
1116
+ suggestions: 0,
1117
+ };
982
1118
  }
983
1119
  if (result.status === 'pass')
984
1120
  issuesByCategory[cat].passed++;
@@ -986,6 +1122,15 @@ export class SeoAnalyzer {
986
1122
  issuesByCategory[cat].warnings++;
987
1123
  else if (result.status === 'fail')
988
1124
  issuesByCategory[cat].errors++;
1125
+ else if (result.status === 'info') {
1126
+ issuesByCategory[cat].infos++;
1127
+ if (result.infoType === 'not_applicable') {
1128
+ issuesByCategory[cat].notApplicable++;
1129
+ }
1130
+ else {
1131
+ issuesByCategory[cat].suggestions++;
1132
+ }
1133
+ }
989
1134
  }
990
1135
  const topIssues = checks
991
1136
  .filter((c) => c.status === 'fail' || c.status === 'warn')
@@ -1052,6 +1197,8 @@ export class SeoAnalyzer {
1052
1197
  warnings,
1053
1198
  errors,
1054
1199
  infos,
1200
+ notApplicable,
1201
+ suggestions,
1055
1202
  passRate,
1056
1203
  issuesByCategory,
1057
1204
  pageType: pageType,
@@ -1179,7 +1326,7 @@ export class SeoAnalyzer {
1179
1326
  const level = parseInt(tagName.substring(1), 10);
1180
1327
  const text = el.text.trim();
1181
1328
  counts[level] = (counts[level] || 0) + 1;
1182
- structure.push({ level, text: text.slice(0, 80), count: 1 });
1329
+ structure.push({ level, text: text.slice(0, 120), count: 1 });
1183
1330
  if (level === 2) {
1184
1331
  if (inSection) {
1185
1332
  sectionWordCounts.push(currentSectionWordCount);
@@ -1219,8 +1366,24 @@ export class SeoAnalyzer {
1219
1366
  else if (counts[1] > 1) {
1220
1367
  issues.push('Multiple H1 tags');
1221
1368
  }
1369
+ const tree = [];
1370
+ const stack = [];
1371
+ for (const heading of structure) {
1372
+ const node = { level: heading.level, text: heading.text, children: [] };
1373
+ while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
1374
+ stack.pop();
1375
+ }
1376
+ if (stack.length === 0) {
1377
+ tree.push(node);
1378
+ }
1379
+ else {
1380
+ stack[stack.length - 1].node.children.push(node);
1381
+ }
1382
+ stack.push({ node, level: heading.level });
1383
+ }
1222
1384
  return {
1223
1385
  structure,
1386
+ tree,
1224
1387
  h1Count: counts[1],
1225
1388
  hasProperHierarchy,
1226
1389
  issues,
@@ -1453,6 +1616,28 @@ export class SeoAnalyzer {
1453
1616
  return this.rulesEngine.getCategories();
1454
1617
  }
1455
1618
  }
1619
+ function extractReadablePathFromUrl(value) {
1620
+ try {
1621
+ const parsed = new URL(value);
1622
+ const fragments = parsed.pathname
1623
+ .split('/')
1624
+ .filter((segment) => segment.length > 0)
1625
+ .slice(0, 3)
1626
+ .map((segment) => segment.replace(/[-_]/g, ' '))
1627
+ .join(' ')
1628
+ .replace(/\d+/g, ' ')
1629
+ .replace(/\s+/g, ' ')
1630
+ .trim();
1631
+ const host = parsed.hostname.replace(/^www\./, '').replace(/\./g, ' ');
1632
+ if (fragments.length > 0) {
1633
+ return `${host} ${fragments}`.trim();
1634
+ }
1635
+ return host;
1636
+ }
1637
+ catch {
1638
+ return '';
1639
+ }
1640
+ }
1456
1641
  export async function analyzeSeo(html, options = {}) {
1457
1642
  const analyzer = await SeoAnalyzer.fromHtml(html, options);
1458
1643
  return analyzer.analyze();
@@ -29,7 +29,16 @@ export function statusIcon(status) {
29
29
  export function formatCheck(check, showEvidence = false) {
30
30
  const lines = [];
31
31
  const icon = statusIcon(check.status);
32
- lines.push(` ${icon} ${check.message}`);
32
+ let infoLabel = '';
33
+ if (check.status === 'info') {
34
+ if (check.infoType === 'not_applicable') {
35
+ infoLabel = colors.magenta(' [N/A]');
36
+ }
37
+ else if (check.infoType === 'suggestion') {
38
+ infoLabel = colors.cyan(' [SUGGESTION]');
39
+ }
40
+ }
41
+ lines.push(` ${icon} ${check.message}${infoLabel}`);
33
42
  if (check.value !== undefined) {
34
43
  lines.push(` ${colors.gray('Value:')} ${check.value}`);
35
44
  }
@@ -239,7 +248,10 @@ export function formatSeoReport(report, url, options = {}) {
239
248
  lines.push(colors.gray(' ──────────────────────────────────────────────────'));
240
249
  lines.push('');
241
250
  lines.push(` ${colors.bold('Summary')}`);
242
- lines.push(` ${colors.green('✔ Passed:')} ${s.passed} ${colors.yellow('⚠ Warnings:')} ${s.warnings} ${colors.red('✖ Errors:')} ${s.errors} ${colors.blue('ℹ Info:')} ${s.infos}`);
251
+ const infoSummary = (s.notApplicable !== undefined || s.suggestions !== undefined)
252
+ ? ` (${s.notApplicable ?? 0} not applicable, ${s.suggestions ?? 0} suggestions)`
253
+ : '';
254
+ lines.push(` ${colors.green('✔ Passed:')} ${s.passed} ${colors.yellow('⚠ Warnings:')} ${s.warnings} ${colors.red('✖ Errors:')} ${s.errors} ${colors.blue('ℹ Info:')} ${s.infos}${infoSummary}`);
243
255
  if (s.vitals) {
244
256
  lines.push(` ${colors.gray('Words:')} ${s.vitals.wordCount} | ${colors.gray('Images:')} ${s.vitals.imageCount} | ${colors.gray('Links:')} ${s.vitals.linkCount}`);
245
257
  }
@@ -381,12 +393,16 @@ function groupChecksByCategory(checks) {
381
393
  const warnings = catChecks.filter(c => c.status === 'warn').length;
382
394
  const errors = catChecks.filter(c => c.status === 'fail').length;
383
395
  const infos = catChecks.filter(c => c.status === 'info').length;
396
+ const notApplicable = catChecks.filter(c => c.status === 'info' && c.infoType === 'not_applicable').length;
397
+ const suggestions = catChecks.filter(c => c.status === 'info' && c.infoType !== 'not_applicable').length;
384
398
  const total = catChecks.length - infos;
385
399
  result[cat] = {
386
400
  passed,
387
401
  warnings,
388
402
  errors,
389
403
  infos,
404
+ notApplicable,
405
+ suggestions,
390
406
  passRate: total > 0 ? Math.round((passed / total) * 100) : 100,
391
407
  checks: catChecks,
392
408
  };
@@ -410,6 +426,8 @@ export function formatSeoReportJson(report, url, options) {
410
426
  warnings: report.checks.filter(c => c.status === 'warn').length,
411
427
  errors: report.checks.filter(c => c.status === 'fail').length,
412
428
  infos: report.checks.filter(c => c.status === 'info').length,
429
+ notApplicable: report.checks.filter(c => c.status === 'info' && c.infoType === 'not_applicable').length,
430
+ suggestions: report.checks.filter(c => c.status === 'info' && c.infoType !== 'not_applicable').length,
413
431
  passRate: report.summary.passRate,
414
432
  completeness: report.summary.completeness,
415
433
  topIssues: report.summary.topIssues,
@@ -6,7 +6,7 @@ export type { SeoSpiderOptions, SeoPageResult, SiteWideIssue, SeoSpiderResult, }
6
6
  export { SeoRulesEngine, createRulesEngine, SEO_THRESHOLDS, ALL_SEO_RULES, } from './rules/index.js';
7
7
  export { generateSeoFilename, resolveOutputPath, writeReport, formatReportForJson, } from './output.js';
8
8
  export type { SeoOutputType, OutputOptions, WriteOptions } from './output.js';
9
- export type { SeoReport, SeoCheckResult, SeoStatus, SeoPageType, SeoTiming, HeadingAnalysis, HeadingInfo, ContentMetrics, LinkAnalysis, ImageAnalysis, SocialMetaAnalysis, TechnicalSeo, SeoAnalyzerOptions, } from './types.js';
9
+ export type { SeoReport, SeoCheckResult, SeoStatus, SeoPageType, SeoTiming, HeadingAnalysis, HeadingInfo, HeadingNode, ContentMetrics, LinkAnalysis, ImageAnalysis, SocialMetaAnalysis, TechnicalSeo, SeoAnalyzerOptions, } from './types.js';
10
10
  export type { SeoRule, RuleContext, RuleResult, RuleEvidence, RuleCategory, RuleSeverity, RulesEngineOptions, } from './rules/index.js';
11
11
  export type { SeoAnalyzerFullOptions } from './analyzer.js';
12
12
  export { parseRobotsTxt, validateRobotsTxt, isPathAllowed, fetchAndValidateRobotsTxt, } from './validators/robots.js';
@@ -0,0 +1,2 @@
1
+ import { type KeywordCampaignOptions, type KeywordCampaignReport } from './keyword-campaign-shared.js';
2
+ export declare function analyzeKeywordCampaign(options: KeywordCampaignOptions): Promise<KeywordCampaignReport>;