recker 1.0.42 → 1.0.43-next.7a08602

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 (458) hide show
  1. package/dist/bin/recker-linux-x64 +0 -0
  2. package/dist/bin/recker-macos-x64 +0 -0
  3. package/dist/bin/recker-win-x64.exe +0 -0
  4. package/dist/bin/rek.cjs +93958 -0
  5. package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
  6. package/dist/browser/ai/adaptive-timeout.js +208 -0
  7. package/dist/browser/ai/client.d.ts +22 -0
  8. package/dist/browser/ai/client.js +294 -0
  9. package/dist/browser/ai/index.d.ts +14 -0
  10. package/dist/browser/ai/index.js +11 -0
  11. package/dist/browser/ai/providers/anthropic.d.ts +63 -0
  12. package/dist/browser/ai/providers/anthropic.js +370 -0
  13. package/dist/browser/ai/providers/base.d.ts +48 -0
  14. package/dist/browser/ai/providers/base.js +150 -0
  15. package/dist/browser/ai/providers/google.d.ts +59 -0
  16. package/dist/browser/ai/providers/google.js +305 -0
  17. package/dist/browser/ai/providers/ollama.d.ts +44 -0
  18. package/dist/browser/ai/providers/ollama.js +240 -0
  19. package/dist/browser/ai/providers/openai.d.ts +64 -0
  20. package/dist/browser/ai/providers/openai.js +298 -0
  21. package/dist/browser/ai/rate-limiter.d.ts +43 -0
  22. package/dist/browser/ai/rate-limiter.js +215 -0
  23. package/dist/browser/ai/vector/index.d.ts +2 -0
  24. package/dist/browser/ai/vector/index.js +2 -0
  25. package/dist/browser/ai/vector/similarity.d.ts +2 -0
  26. package/dist/browser/ai/vector/similarity.js +27 -0
  27. package/dist/browser/ai/vector/store.d.ts +27 -0
  28. package/dist/browser/ai/vector/store.js +82 -0
  29. package/dist/browser/browser/cache.d.ts +2 -40
  30. package/dist/browser/browser/cache.js +2 -199
  31. package/dist/browser/browser/index.d.ts +8 -0
  32. package/dist/browser/browser/index.js +8 -0
  33. package/dist/browser/browser/recker.d.ts +8 -1
  34. package/dist/browser/browser/recker.js +8 -2
  35. package/dist/browser/cache/indexed-db.d.ts +10 -0
  36. package/dist/browser/cache/indexed-db.js +88 -0
  37. package/dist/browser/cache/service-worker-cache.d.ts +18 -0
  38. package/dist/browser/cache/service-worker-cache.js +103 -0
  39. package/dist/browser/cache.d.ts +2 -40
  40. package/dist/browser/cache.js +2 -199
  41. package/dist/browser/constants/user-agents.d.ts +7 -0
  42. package/dist/browser/constants/user-agents.js +7 -0
  43. package/dist/browser/core/client.d.ts +2 -0
  44. package/dist/browser/core/client.js +19 -1
  45. package/dist/browser/index.d.ts +8 -0
  46. package/dist/browser/index.js +8 -0
  47. package/dist/browser/plugins/har-recorder.d.ts +40 -0
  48. package/dist/browser/plugins/har-recorder.js +120 -0
  49. package/dist/browser/plugins/network-simulation.d.ts +7 -0
  50. package/dist/browser/plugins/network-simulation.js +13 -0
  51. package/dist/browser/presets/android.d.ts +2 -0
  52. package/dist/browser/presets/android.js +16 -0
  53. package/dist/browser/presets/anthropic.d.ts +8 -0
  54. package/dist/browser/presets/anthropic.js +27 -0
  55. package/dist/browser/presets/aws.d.ts +19 -0
  56. package/dist/browser/presets/aws.js +68 -0
  57. package/dist/browser/presets/azure-openai.d.ts +10 -0
  58. package/dist/browser/presets/azure-openai.js +35 -0
  59. package/dist/browser/presets/azure.d.ts +41 -0
  60. package/dist/browser/presets/azure.js +104 -0
  61. package/dist/browser/presets/chaturbate.d.ts +2 -0
  62. package/dist/browser/presets/chaturbate.js +17 -0
  63. package/dist/browser/presets/cloudflare.d.ts +12 -0
  64. package/dist/browser/presets/cloudflare.js +39 -0
  65. package/dist/browser/presets/cohere.d.ts +7 -0
  66. package/dist/browser/presets/cohere.js +22 -0
  67. package/dist/browser/presets/deepseek.d.ts +7 -0
  68. package/dist/browser/presets/deepseek.js +22 -0
  69. package/dist/browser/presets/digitalocean.d.ts +5 -0
  70. package/dist/browser/presets/digitalocean.js +16 -0
  71. package/dist/browser/presets/discord.d.ts +6 -0
  72. package/dist/browser/presets/discord.js +17 -0
  73. package/dist/browser/presets/elevenlabs.d.ts +6 -0
  74. package/dist/browser/presets/elevenlabs.js +20 -0
  75. package/dist/browser/presets/enhancers.d.ts +20 -0
  76. package/dist/browser/presets/enhancers.js +85 -0
  77. package/dist/browser/presets/fireworks.d.ts +7 -0
  78. package/dist/browser/presets/fireworks.js +22 -0
  79. package/dist/browser/presets/gcp.d.ts +34 -0
  80. package/dist/browser/presets/gcp.js +91 -0
  81. package/dist/browser/presets/gemini.d.ts +7 -0
  82. package/dist/browser/presets/gemini.js +23 -0
  83. package/dist/browser/presets/github.d.ts +6 -0
  84. package/dist/browser/presets/github.js +17 -0
  85. package/dist/browser/presets/gitlab.d.ts +6 -0
  86. package/dist/browser/presets/gitlab.js +16 -0
  87. package/dist/browser/presets/groq.d.ts +7 -0
  88. package/dist/browser/presets/groq.js +22 -0
  89. package/dist/browser/presets/hubspot.d.ts +9 -0
  90. package/dist/browser/presets/hubspot.js +28 -0
  91. package/dist/browser/presets/huggingface.d.ts +7 -0
  92. package/dist/browser/presets/huggingface.js +23 -0
  93. package/dist/browser/presets/index.d.ts +47 -0
  94. package/dist/browser/presets/index.js +47 -0
  95. package/dist/browser/presets/ios.d.ts +2 -0
  96. package/dist/browser/presets/ios.js +13 -0
  97. package/dist/browser/presets/linear.d.ts +5 -0
  98. package/dist/browser/presets/linear.js +16 -0
  99. package/dist/browser/presets/mailgun.d.ts +7 -0
  100. package/dist/browser/presets/mailgun.js +20 -0
  101. package/dist/browser/presets/meta.d.ts +10 -0
  102. package/dist/browser/presets/meta.js +33 -0
  103. package/dist/browser/presets/mistral.d.ts +7 -0
  104. package/dist/browser/presets/mistral.js +22 -0
  105. package/dist/browser/presets/notion.d.ts +6 -0
  106. package/dist/browser/presets/notion.js +17 -0
  107. package/dist/browser/presets/openai.d.ts +9 -0
  108. package/dist/browser/presets/openai.js +30 -0
  109. package/dist/browser/presets/oracle.d.ts +19 -0
  110. package/dist/browser/presets/oracle.js +117 -0
  111. package/dist/browser/presets/perplexity.d.ts +7 -0
  112. package/dist/browser/presets/perplexity.js +22 -0
  113. package/dist/browser/presets/pinecone.d.ts +8 -0
  114. package/dist/browser/presets/pinecone.js +42 -0
  115. package/dist/browser/presets/registry.d.ts +23 -0
  116. package/dist/browser/presets/registry.js +519 -0
  117. package/dist/browser/presets/replicate.d.ts +7 -0
  118. package/dist/browser/presets/replicate.js +23 -0
  119. package/dist/browser/presets/sendgrid.d.ts +6 -0
  120. package/dist/browser/presets/sendgrid.js +20 -0
  121. package/dist/browser/presets/sentry.d.ts +11 -0
  122. package/dist/browser/presets/sentry.js +48 -0
  123. package/dist/browser/presets/sinch.d.ts +9 -0
  124. package/dist/browser/presets/sinch.js +39 -0
  125. package/dist/browser/presets/slack.d.ts +5 -0
  126. package/dist/browser/presets/slack.js +16 -0
  127. package/dist/browser/presets/square.d.ts +10 -0
  128. package/dist/browser/presets/square.js +33 -0
  129. package/dist/browser/presets/stripe.d.ts +7 -0
  130. package/dist/browser/presets/stripe.js +23 -0
  131. package/dist/browser/presets/supabase.d.ts +6 -0
  132. package/dist/browser/presets/supabase.js +18 -0
  133. package/dist/browser/presets/tiktok.d.ts +10 -0
  134. package/dist/browser/presets/tiktok.js +38 -0
  135. package/dist/browser/presets/together.d.ts +7 -0
  136. package/dist/browser/presets/together.js +22 -0
  137. package/dist/browser/presets/twilio.d.ts +6 -0
  138. package/dist/browser/presets/twilio.js +17 -0
  139. package/dist/browser/presets/vercel.d.ts +6 -0
  140. package/dist/browser/presets/vercel.js +23 -0
  141. package/dist/browser/presets/vultr.d.ts +5 -0
  142. package/dist/browser/presets/vultr.js +16 -0
  143. package/dist/browser/presets/xai.d.ts +8 -0
  144. package/dist/browser/presets/xai.js +23 -0
  145. package/dist/browser/presets/youtube.d.ts +5 -0
  146. package/dist/browser/presets/youtube.js +20 -0
  147. package/dist/browser/recker.d.ts +8 -1
  148. package/dist/browser/recker.js +8 -2
  149. package/dist/browser/scrape/document.d.ts +5 -4
  150. package/dist/browser/scrape/document.js +89 -76
  151. package/dist/browser/scrape/element.d.ts +10 -8
  152. package/dist/browser/scrape/element.js +295 -81
  153. package/dist/browser/scrape/extractors.d.ts +11 -11
  154. package/dist/browser/scrape/extractors.js +145 -113
  155. package/dist/browser/scrape/parser/back.d.ts +1 -0
  156. package/dist/browser/scrape/parser/back.js +3 -0
  157. package/dist/browser/scrape/parser/index.d.ts +20 -0
  158. package/dist/browser/scrape/parser/index.js +19 -0
  159. package/dist/browser/scrape/parser/matcher.d.ts +30 -0
  160. package/dist/browser/scrape/parser/matcher.js +99 -0
  161. package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
  162. package/dist/browser/scrape/parser/nodes/comment.js +21 -0
  163. package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
  164. package/dist/browser/scrape/parser/nodes/html.js +978 -0
  165. package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
  166. package/dist/browser/scrape/parser/nodes/node.js +31 -0
  167. package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
  168. package/dist/browser/scrape/parser/nodes/text.js +30 -0
  169. package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
  170. package/dist/browser/scrape/parser/nodes/type.js +7 -0
  171. package/dist/browser/scrape/parser/parse.d.ts +1 -0
  172. package/dist/browser/scrape/parser/parse.js +1 -0
  173. package/dist/browser/scrape/parser/valid.d.ts +2 -0
  174. package/dist/browser/scrape/parser/valid.js +5 -0
  175. package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
  176. package/dist/browser/scrape/parser/void-tag.js +43 -0
  177. package/dist/browser/scrape/types.d.ts +7 -0
  178. package/dist/browser/seo/analyzer.d.ts +59 -0
  179. package/dist/browser/seo/analyzer.js +1399 -0
  180. package/dist/browser/seo/keywords.d.ts +16 -0
  181. package/dist/browser/seo/keywords.js +55 -0
  182. package/dist/browser/seo/rules/accessibility.d.ts +2 -0
  183. package/dist/browser/seo/rules/accessibility.js +722 -0
  184. package/dist/browser/seo/rules/ai-search.d.ts +2 -0
  185. package/dist/browser/seo/rules/ai-search.js +436 -0
  186. package/dist/browser/seo/rules/analytics.d.ts +2 -0
  187. package/dist/browser/seo/rules/analytics.js +306 -0
  188. package/dist/browser/seo/rules/best-practices.d.ts +2 -0
  189. package/dist/browser/seo/rules/best-practices.js +195 -0
  190. package/dist/browser/seo/rules/canonical.d.ts +12 -0
  191. package/dist/browser/seo/rules/canonical.js +258 -0
  192. package/dist/browser/seo/rules/content.d.ts +2 -0
  193. package/dist/browser/seo/rules/content.js +424 -0
  194. package/dist/browser/seo/rules/crawl.d.ts +2 -0
  195. package/dist/browser/seo/rules/crawl.js +435 -0
  196. package/dist/browser/seo/rules/cwv.d.ts +2 -0
  197. package/dist/browser/seo/rules/cwv.js +248 -0
  198. package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
  199. package/dist/browser/seo/rules/ecommerce.js +283 -0
  200. package/dist/browser/seo/rules/i18n.d.ts +2 -0
  201. package/dist/browser/seo/rules/i18n.js +277 -0
  202. package/dist/browser/seo/rules/images.d.ts +2 -0
  203. package/dist/browser/seo/rules/images.js +235 -0
  204. package/dist/browser/seo/rules/index.d.ts +52 -0
  205. package/dist/browser/seo/rules/index.js +159 -0
  206. package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
  207. package/dist/browser/seo/rules/internal-linking.js +394 -0
  208. package/dist/browser/seo/rules/links.d.ts +2 -0
  209. package/dist/browser/seo/rules/links.js +490 -0
  210. package/dist/browser/seo/rules/local.d.ts +2 -0
  211. package/dist/browser/seo/rules/local.js +289 -0
  212. package/dist/browser/seo/rules/meta.d.ts +2 -0
  213. package/dist/browser/seo/rules/meta.js +646 -0
  214. package/dist/browser/seo/rules/mobile.d.ts +2 -0
  215. package/dist/browser/seo/rules/mobile.js +161 -0
  216. package/dist/browser/seo/rules/performance.d.ts +2 -0
  217. package/dist/browser/seo/rules/performance.js +625 -0
  218. package/dist/browser/seo/rules/pwa.d.ts +2 -0
  219. package/dist/browser/seo/rules/pwa.js +299 -0
  220. package/dist/browser/seo/rules/readability.d.ts +2 -0
  221. package/dist/browser/seo/rules/readability.js +264 -0
  222. package/dist/browser/seo/rules/redirects.d.ts +16 -0
  223. package/dist/browser/seo/rules/redirects.js +199 -0
  224. package/dist/browser/seo/rules/resources.d.ts +2 -0
  225. package/dist/browser/seo/rules/resources.js +390 -0
  226. package/dist/browser/seo/rules/schema.d.ts +2 -0
  227. package/dist/browser/seo/rules/schema.js +379 -0
  228. package/dist/browser/seo/rules/security.d.ts +2 -0
  229. package/dist/browser/seo/rules/security.js +877 -0
  230. package/dist/browser/seo/rules/social.d.ts +2 -0
  231. package/dist/browser/seo/rules/social.js +603 -0
  232. package/dist/browser/seo/rules/structural.d.ts +2 -0
  233. package/dist/browser/seo/rules/structural.js +179 -0
  234. package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
  235. package/dist/browser/seo/rules/technical-advanced.js +288 -0
  236. package/dist/browser/seo/rules/technical.d.ts +2 -0
  237. package/dist/browser/seo/rules/technical.js +480 -0
  238. package/dist/browser/seo/rules/thresholds.d.ts +196 -0
  239. package/dist/browser/seo/rules/thresholds.js +118 -0
  240. package/dist/browser/seo/rules/types.d.ts +498 -0
  241. package/dist/browser/seo/rules/types.js +11 -0
  242. package/dist/browser/seo/types.d.ts +211 -0
  243. package/dist/browser/seo/types.js +1 -0
  244. package/dist/browser/transport/curl.d.ts +4 -0
  245. package/dist/browser/transport/curl.js +101 -0
  246. package/dist/browser/transport/undici.js +1 -2
  247. package/dist/browser/transport/worker.d.ts +18 -0
  248. package/dist/browser/transport/worker.js +278 -0
  249. package/dist/browser/types/index.d.ts +4 -1
  250. package/dist/browser/utils/binary-manager.d.ts +4 -0
  251. package/dist/browser/utils/binary-manager.js +72 -0
  252. package/dist/browser/utils/user-agent.js +2 -13
  253. package/dist/cache/indexed-db.d.ts +10 -0
  254. package/dist/cache/indexed-db.js +88 -0
  255. package/dist/cache/service-worker-cache.d.ts +18 -0
  256. package/dist/cache/service-worker-cache.js +103 -0
  257. package/dist/cli/commands/ai.d.ts +2 -0
  258. package/dist/cli/commands/ai.js +162 -0
  259. package/dist/cli/commands/bench.d.ts +2 -0
  260. package/dist/cli/commands/bench.js +51 -0
  261. package/dist/cli/commands/dns.d.ts +2 -0
  262. package/dist/cli/commands/dns.js +295 -0
  263. package/dist/cli/commands/har.d.ts +2 -0
  264. package/dist/cli/commands/har.js +171 -0
  265. package/dist/cli/commands/hls.d.ts +2 -0
  266. package/dist/cli/commands/hls.js +192 -0
  267. package/dist/cli/commands/network.d.ts +2 -0
  268. package/dist/cli/commands/network.js +288 -0
  269. package/dist/cli/commands/protocols.d.ts +2 -0
  270. package/dist/cli/commands/protocols.js +344 -0
  271. package/dist/cli/commands/scrape.d.ts +2 -0
  272. package/dist/cli/commands/scrape.js +176 -0
  273. package/dist/cli/commands/security.d.ts +2 -0
  274. package/dist/cli/commands/security.js +57 -0
  275. package/dist/cli/commands/seo.d.ts +2 -0
  276. package/dist/cli/commands/seo.js +125 -0
  277. package/dist/cli/commands/serve.d.ts +2 -0
  278. package/dist/cli/commands/serve.js +531 -0
  279. package/dist/cli/commands/spider.d.ts +3 -0
  280. package/dist/cli/commands/spider.js +456 -0
  281. package/dist/cli/commands/utils.d.ts +2 -0
  282. package/dist/cli/commands/utils.js +176 -0
  283. package/dist/cli/commands/vector.d.ts +2 -0
  284. package/dist/cli/commands/vector.js +158 -0
  285. package/dist/cli/handler.d.ts +2 -2
  286. package/dist/cli/handler.js +6 -6
  287. package/dist/cli/helpers.d.ts +7 -0
  288. package/dist/cli/helpers.js +128 -0
  289. package/dist/cli/index.js +96 -5228
  290. package/dist/cli/parser/help.d.ts +2 -0
  291. package/dist/cli/parser/help.js +52 -0
  292. package/dist/cli/parser/index.d.ts +3 -0
  293. package/dist/cli/parser/index.js +3 -0
  294. package/dist/cli/parser/parser.d.ts +4 -0
  295. package/dist/cli/parser/parser.js +146 -0
  296. package/dist/cli/parser/types.d.ts +41 -0
  297. package/dist/cli/parser/types.js +1 -0
  298. package/dist/cli/presets.d.ts +1 -1
  299. package/dist/cli/presets.js +1 -1
  300. package/dist/cli/router.d.ts +36 -0
  301. package/dist/cli/router.js +195 -0
  302. package/dist/cli/tui/ai-chat.js +1 -1
  303. package/dist/cli/tui/commands/context.d.ts +9 -0
  304. package/dist/cli/tui/commands/context.js +1 -0
  305. package/dist/cli/tui/commands/dns.d.ts +10 -0
  306. package/dist/cli/tui/commands/dns.js +461 -0
  307. package/dist/cli/tui/commands/hls.d.ts +2 -0
  308. package/dist/cli/tui/commands/hls.js +162 -0
  309. package/dist/cli/tui/commands/ip.d.ts +2 -0
  310. package/dist/cli/tui/commands/ip.js +45 -0
  311. package/dist/cli/tui/commands/network.d.ts +3 -0
  312. package/dist/cli/tui/commands/network.js +81 -0
  313. package/dist/cli/tui/commands/protocols.d.ts +6 -0
  314. package/dist/cli/tui/commands/protocols.js +531 -0
  315. package/dist/cli/tui/commands/security.d.ts +2 -0
  316. package/dist/cli/tui/commands/security.js +48 -0
  317. package/dist/cli/tui/commands/seo.d.ts +2 -0
  318. package/dist/cli/tui/commands/seo.js +74 -0
  319. package/dist/cli/tui/context.d.ts +12 -0
  320. package/dist/cli/tui/context.js +1 -0
  321. package/dist/cli/tui/shell.d.ts +11 -20
  322. package/dist/cli/tui/shell.js +216 -1873
  323. package/dist/constants/user-agents.d.ts +7 -0
  324. package/dist/constants/user-agents.js +7 -0
  325. package/dist/core/client.d.ts +2 -0
  326. package/dist/core/client.js +19 -1
  327. package/dist/index.d.ts +1 -0
  328. package/dist/index.js +1 -0
  329. package/dist/mcp/cli.js +2 -3
  330. package/dist/mcp/data/embeddings.json +1 -0
  331. package/dist/mcp/tools/network.js +298 -158
  332. package/dist/plugins/har-player.d.ts +23 -0
  333. package/dist/plugins/har-player.js +49 -0
  334. package/dist/plugins/har-recorder.d.ts +37 -3
  335. package/dist/plugins/har-recorder.js +116 -63
  336. package/dist/plugins/network-simulation.d.ts +7 -0
  337. package/dist/plugins/network-simulation.js +13 -0
  338. package/dist/presets/android.d.ts +2 -0
  339. package/dist/presets/android.js +16 -0
  340. package/dist/presets/chaturbate.d.ts +2 -0
  341. package/dist/presets/chaturbate.js +17 -0
  342. package/dist/presets/elevenlabs.d.ts +6 -0
  343. package/dist/presets/elevenlabs.js +20 -0
  344. package/dist/presets/enhancers.d.ts +20 -0
  345. package/dist/presets/enhancers.js +85 -0
  346. package/dist/presets/hubspot.d.ts +9 -0
  347. package/dist/presets/hubspot.js +28 -0
  348. package/dist/presets/index.d.ts +10 -0
  349. package/dist/presets/index.js +10 -0
  350. package/dist/presets/ios.d.ts +2 -0
  351. package/dist/presets/ios.js +13 -0
  352. package/dist/presets/pinecone.d.ts +8 -0
  353. package/dist/presets/pinecone.js +42 -0
  354. package/dist/presets/registry.js +60 -0
  355. package/dist/presets/sendgrid.d.ts +6 -0
  356. package/dist/presets/sendgrid.js +20 -0
  357. package/dist/presets/sentry.d.ts +11 -0
  358. package/dist/presets/sentry.js +48 -0
  359. package/dist/presets/square.d.ts +10 -0
  360. package/dist/presets/square.js +33 -0
  361. package/dist/recker.d.ts +3 -0
  362. package/dist/recker.js +4 -0
  363. package/dist/scrape/document.d.ts +5 -4
  364. package/dist/scrape/document.js +89 -76
  365. package/dist/scrape/element.d.ts +10 -8
  366. package/dist/scrape/element.js +295 -81
  367. package/dist/scrape/extractors.d.ts +11 -11
  368. package/dist/scrape/extractors.js +145 -113
  369. package/dist/scrape/index.d.ts +2 -0
  370. package/dist/scrape/index.js +1 -0
  371. package/dist/scrape/parser/back.d.ts +1 -0
  372. package/dist/scrape/parser/back.js +3 -0
  373. package/dist/scrape/parser/index.d.ts +20 -0
  374. package/dist/scrape/parser/index.js +19 -0
  375. package/dist/scrape/parser/matcher.d.ts +30 -0
  376. package/dist/scrape/parser/matcher.js +99 -0
  377. package/dist/scrape/parser/nodes/comment.d.ts +12 -0
  378. package/dist/scrape/parser/nodes/comment.js +21 -0
  379. package/dist/scrape/parser/nodes/html.d.ts +110 -0
  380. package/dist/scrape/parser/nodes/html.js +978 -0
  381. package/dist/scrape/parser/nodes/node.d.ts +18 -0
  382. package/dist/scrape/parser/nodes/node.js +31 -0
  383. package/dist/scrape/parser/nodes/text.d.ts +14 -0
  384. package/dist/scrape/parser/nodes/text.js +30 -0
  385. package/dist/scrape/parser/nodes/type.d.ts +6 -0
  386. package/dist/scrape/parser/nodes/type.js +7 -0
  387. package/dist/scrape/parser/parse.d.ts +1 -0
  388. package/dist/scrape/parser/parse.js +1 -0
  389. package/dist/scrape/parser/valid.d.ts +2 -0
  390. package/dist/scrape/parser/valid.js +5 -0
  391. package/dist/scrape/parser/void-tag.d.ts +7 -0
  392. package/dist/scrape/parser/void-tag.js +43 -0
  393. package/dist/scrape/spider.d.ts +19 -0
  394. package/dist/scrape/spider.js +28 -3
  395. package/dist/scrape/types.d.ts +7 -0
  396. package/dist/seo/analyzer.d.ts +15 -5
  397. package/dist/seo/analyzer.js +636 -175
  398. package/dist/seo/formatter.d.ts +16 -0
  399. package/dist/seo/formatter.js +228 -0
  400. package/dist/seo/index.d.ts +2 -0
  401. package/dist/seo/index.js +1 -0
  402. package/dist/seo/keywords.d.ts +16 -0
  403. package/dist/seo/keywords.js +55 -0
  404. package/dist/seo/rules/accessibility.js +85 -57
  405. package/dist/seo/rules/ai-search.js +44 -31
  406. package/dist/seo/rules/analytics.d.ts +2 -0
  407. package/dist/seo/rules/analytics.js +306 -0
  408. package/dist/seo/rules/best-practices.js +21 -14
  409. package/dist/seo/rules/canonical.js +31 -22
  410. package/dist/seo/rules/content.js +207 -19
  411. package/dist/seo/rules/crawl.js +55 -40
  412. package/dist/seo/rules/cwv.js +21 -15
  413. package/dist/seo/rules/ecommerce.js +51 -20
  414. package/dist/seo/rules/i18n.js +61 -33
  415. package/dist/seo/rules/images.js +87 -28
  416. package/dist/seo/rules/index.js +2 -0
  417. package/dist/seo/rules/internal-linking.js +58 -39
  418. package/dist/seo/rules/links.js +70 -51
  419. package/dist/seo/rules/local.js +49 -25
  420. package/dist/seo/rules/meta.js +161 -62
  421. package/dist/seo/rules/mobile.js +112 -2
  422. package/dist/seo/rules/performance.js +309 -54
  423. package/dist/seo/rules/pwa.js +36 -39
  424. package/dist/seo/rules/readability.js +31 -22
  425. package/dist/seo/rules/redirects.js +21 -15
  426. package/dist/seo/rules/resources.js +59 -42
  427. package/dist/seo/rules/schema.js +333 -8
  428. package/dist/seo/rules/security.js +142 -80
  429. package/dist/seo/rules/social.js +277 -47
  430. package/dist/seo/rules/structural.js +40 -16
  431. package/dist/seo/rules/technical-advanced.js +27 -22
  432. package/dist/seo/rules/technical.js +243 -42
  433. package/dist/seo/rules/types.d.ts +53 -1
  434. package/dist/seo/seo-spider.d.ts +22 -0
  435. package/dist/seo/seo-spider.js +77 -13
  436. package/dist/seo/types.d.ts +8 -1
  437. package/dist/seo/validators/llms-txt.js +19 -0
  438. package/dist/seo/validators/rss.d.ts +11 -0
  439. package/dist/seo/validators/rss.js +93 -0
  440. package/dist/seo/validators/sitemap.js +36 -26
  441. package/dist/transport/curl.d.ts +4 -0
  442. package/dist/transport/curl.js +101 -0
  443. package/dist/transport/udp.js +0 -1
  444. package/dist/transport/undici.js +1 -2
  445. package/dist/transport/worker.d.ts +18 -0
  446. package/dist/transport/worker.js +278 -0
  447. package/dist/types/index.d.ts +4 -1
  448. package/dist/utils/binary-manager.d.ts +4 -0
  449. package/dist/utils/binary-manager.js +72 -0
  450. package/dist/utils/optional-require.d.ts +7 -8
  451. package/dist/utils/optional-require.js +2 -21
  452. package/dist/utils/upload.d.ts +6 -0
  453. package/dist/utils/upload.js +11 -0
  454. package/dist/utils/user-agent.js +2 -13
  455. package/dist/version.js +1 -1
  456. package/package.json +14 -5
  457. package/dist/browser/utils/optional-require.d.ts +0 -19
  458. package/dist/browser/utils/optional-require.js +0 -105
@@ -19,7 +19,7 @@ export const metaRules = [
19
19
  },
20
20
  });
21
21
  }
22
- return null;
22
+ return createResult({ id: 'title-exists', name: 'Title Tag Exists', category: 'title', severity: 'error' }, 'info', 'Not applicable (page has title tag)', { recommendation: 'This rule checks for the presence of a title tag' });
23
23
  },
24
24
  },
25
25
  {
@@ -29,15 +29,32 @@ export const metaRules = [
29
29
  severity: 'warning',
30
30
  description: 'Title should be between 50-60 characters',
31
31
  check: (ctx) => {
32
- if (!ctx.title)
33
- return null;
32
+ if (!ctx.title) {
33
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks title length when a title tag is present' });
34
+ }
34
35
  const len = ctx.titleLength ?? ctx.title.length;
35
36
  const { min, ideal, max } = SEO_THRESHOLDS.title;
36
37
  if (len < min) {
37
- return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too short (${len} chars, min: ${min})`, { value: len, recommendation: `Expand title to ${ideal.min}-${ideal.max} characters` });
38
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too short (${len} chars, min: ${min})`, {
39
+ value: len,
40
+ recommendation: `Expand title to ${ideal.min}-${ideal.max} characters. Ensure it includes target keywords and encourages clicks.`,
41
+ evidence: {
42
+ found: `${len} characters`,
43
+ expected: `${ideal.min}-${ideal.max} characters`,
44
+ impact: 'Short titles limit keyword potential and may be replaced by Google.'
45
+ }
46
+ });
38
47
  }
39
48
  if (len > max) {
40
- return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too long (${len} chars, will be truncated after ~60)`, { value: len, recommendation: `Shorten title to under ${ideal.max} characters` });
49
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too long (${len} chars, will be truncated after ~60)`, {
50
+ value: len,
51
+ recommendation: `Shorten title to under ${ideal.max} characters to ensure visibility in SERPs.`,
52
+ evidence: {
53
+ found: `${len} characters`,
54
+ expected: `< ${max} characters`,
55
+ impact: 'Truncated titles may lose click-through rate if key information is hidden.'
56
+ }
57
+ });
41
58
  }
42
59
  if (len >= ideal.min && len <= ideal.max) {
43
60
  return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'pass', `Title length ideal (${len} chars)`, { value: len });
@@ -52,14 +69,23 @@ export const metaRules = [
52
69
  severity: 'warning',
53
70
  description: 'Title should not be ALL CAPS',
54
71
  check: (ctx) => {
55
- if (!ctx.title)
56
- return null;
72
+ if (!ctx.title) {
73
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks title capitalization when a title tag is present' });
74
+ }
57
75
  const words = ctx.title.split(/\s+/).filter((w) => w.length > 3);
58
76
  const allCapsWords = words.filter((w) => w === w.toUpperCase() && /[A-Z]/.test(w));
59
77
  if (allCapsWords.length > words.length / 2) {
60
- return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'warn', 'Title appears to be ALL CAPS', { recommendation: 'Use title case or sentence case for better readability' });
78
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'warn', 'Title appears to be ALL CAPS', {
79
+ recommendation: 'Use title case or sentence case for better readability and click-through rate.',
80
+ evidence: {
81
+ found: ctx.title,
82
+ expected: 'Normal capitalization (Title Case or Sentence case)',
83
+ impact: 'ALL CAPS titles look spammy and may be ignored by users. Google may also rewrite them.',
84
+ example: 'Instead of "BUY SHOES ONLINE NOW", use "Buy Shoes Online - Free Shipping"'
85
+ }
86
+ });
61
87
  }
62
- return null;
88
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title uses proper capitalization)', { recommendation: 'This rule checks for excessive ALL CAPS usage in title' });
63
89
  },
64
90
  },
65
91
  {
@@ -69,14 +95,15 @@ export const metaRules = [
69
95
  severity: 'warning',
70
96
  description: 'Title and H1 should be similar but not identical',
71
97
  check: (ctx) => {
72
- if (!ctx.title || !ctx.h1Text)
73
- return null;
98
+ if (!ctx.title || !ctx.h1Text) {
99
+ return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'info', 'Not applicable (missing title or H1)', { recommendation: 'This rule compares title and H1 when both are present' });
100
+ }
74
101
  const titleNorm = ctx.title.toLowerCase().trim();
75
102
  const h1Norm = ctx.h1Text.toLowerCase().trim();
76
103
  if (titleNorm === h1Norm) {
77
104
  return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'warn', 'Title and H1 are identical', { recommendation: 'Consider making H1 slightly different from title for variety' });
78
105
  }
79
- return null;
106
+ return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title and H1 are different)', { recommendation: 'This rule checks if title and H1 are identical' });
80
107
  },
81
108
  },
82
109
  {
@@ -97,7 +124,7 @@ export const metaRules = [
97
124
  },
98
125
  });
99
126
  }
100
- return null;
127
+ return createResult({ id: 'meta-description-exists', name: 'Meta Description Exists', category: 'meta', severity: 'error' }, 'info', 'Not applicable (page has meta description)', { recommendation: 'This rule checks for the presence of a meta description' });
101
128
  },
102
129
  },
103
130
  {
@@ -107,15 +134,32 @@ export const metaRules = [
107
134
  severity: 'warning',
108
135
  description: 'Meta description should be 120-155 characters',
109
136
  check: (ctx) => {
110
- if (!ctx.metaDescription)
111
- return null;
137
+ if (!ctx.metaDescription) {
138
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'info', 'Not applicable (no meta description)', { recommendation: 'This rule checks meta description length when present' });
139
+ }
112
140
  const len = ctx.metaDescriptionLength ?? ctx.metaDescription.length;
113
141
  const { min, ideal, max } = SEO_THRESHOLDS.metaDescription;
114
142
  if (len < min) {
115
- return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description too short (${len} chars, min: ${min})`, { value: len, recommendation: `Expand to ${ideal.min}-${ideal.max} characters` });
143
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description too short (${len} chars, min: ${min})`, {
144
+ value: len,
145
+ recommendation: `Expand to ${ideal.min}-${ideal.max} characters. Summarize content and include keywords naturally.`,
146
+ evidence: {
147
+ found: `${len} characters`,
148
+ expected: `${ideal.min}-${ideal.max} characters`,
149
+ impact: 'Short descriptions may be ignored by search engines in favor of auto-generated snippets.'
150
+ }
151
+ });
116
152
  }
117
153
  if (len > max) {
118
- return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description may be truncated (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to under ${max} characters` });
154
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description may be truncated (${len} chars, max: ${max})`, {
155
+ value: len,
156
+ recommendation: `Shorten to under ${max} characters. Ensure the most important info is at the start.`,
157
+ evidence: {
158
+ found: `${len} characters`,
159
+ expected: `< ${max} characters`,
160
+ impact: 'Truncated descriptions look unprofessional and may lower CTR.'
161
+ }
162
+ });
119
163
  }
120
164
  if (len >= ideal.min && len <= ideal.max) {
121
165
  return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'pass', `Description length ideal (${len} chars)`, { value: len });
@@ -130,8 +174,9 @@ export const metaRules = [
130
174
  severity: 'info',
131
175
  description: 'Meta description should be unique and compelling',
132
176
  check: (ctx) => {
133
- if (!ctx.metaDescription)
134
- return null;
177
+ if (!ctx.metaDescription) {
178
+ return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'info', 'Not applicable (no meta description)', { recommendation: 'This rule checks meta description quality when present' });
179
+ }
135
180
  const desc = ctx.metaDescription.toLowerCase();
136
181
  const placeholders = ['lorem ipsum', 'description here', 'todo', 'placeholder', 'change this'];
137
182
  for (const placeholder of placeholders) {
@@ -139,7 +184,7 @@ export const metaRules = [
139
184
  return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'warn', 'Meta description appears to be a placeholder', { recommendation: 'Replace with a unique, compelling description for better CTR' });
140
185
  }
141
186
  }
142
- return null;
187
+ return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'info', 'Not applicable (description has good quality)', { recommendation: 'This rule checks for placeholder patterns in meta description' });
143
188
  },
144
189
  },
145
190
  {
@@ -160,7 +205,7 @@ export const metaRules = [
160
205
  },
161
206
  });
162
207
  }
163
- return null;
208
+ return createResult({ id: 'og-title-exists', name: 'OG Title Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:title)', { recommendation: 'This rule checks for the presence of og:title meta tag' });
164
209
  },
165
210
  },
166
211
  {
@@ -170,8 +215,9 @@ export const metaRules = [
170
215
  severity: 'warning',
171
216
  description: 'og:title should be 60-70 characters (max 90)',
172
217
  check: (ctx) => {
173
- if (!ctx.ogTitle)
174
- return null;
218
+ if (!ctx.ogTitle) {
219
+ return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks og:title length when present' });
220
+ }
175
221
  const len = ctx.ogTitle.length;
176
222
  const { ideal, max } = SEO_THRESHOLDS.og.title;
177
223
  if (len > max) {
@@ -190,13 +236,14 @@ export const metaRules = [
190
236
  severity: 'warning',
191
237
  description: 'og:title should not contain emojis (some networks remove them)',
192
238
  check: (ctx) => {
193
- if (!ctx.ogTitle)
194
- return null;
239
+ if (!ctx.ogTitle) {
240
+ return createResult({ id: 'og-title-no-emoji', name: 'OG Title No Emoji', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks for emojis in og:title when present' });
241
+ }
195
242
  const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u;
196
243
  if (emojiRegex.test(ctx.ogTitle)) {
197
244
  return createResult({ id: 'og-title-no-emoji', name: 'OG Title Emoji', category: 'og', severity: 'warning' }, 'warn', 'og:title contains emojis (some networks remove them)', { recommendation: 'Remove emojis from og:title for consistent display' });
198
245
  }
199
- return null;
246
+ return createResult({ id: 'og-title-no-emoji', name: 'OG Title No Emoji', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title has no emojis)', { recommendation: 'This rule checks for emoji characters in og:title' });
200
247
  },
201
248
  },
202
249
  {
@@ -217,7 +264,7 @@ export const metaRules = [
217
264
  },
218
265
  });
219
266
  }
220
- return null;
267
+ return createResult({ id: 'og-description-exists', name: 'OG Description Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:description)', { recommendation: 'This rule checks for the presence of og:description meta tag' });
221
268
  },
222
269
  },
223
270
  {
@@ -227,8 +274,9 @@ export const metaRules = [
227
274
  severity: 'warning',
228
275
  description: 'og:description should be 110-155 characters (max 200)',
229
276
  check: (ctx) => {
230
- if (!ctx.ogDescription)
231
- return null;
277
+ if (!ctx.ogDescription) {
278
+ return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:description)', { recommendation: 'This rule checks og:description length when present' });
279
+ }
232
280
  const len = ctx.ogDescription.length;
233
281
  const { ideal, max } = SEO_THRESHOLDS.og.description;
234
282
  if (len > max) {
@@ -259,7 +307,7 @@ export const metaRules = [
259
307
  },
260
308
  });
261
309
  }
262
- return null;
310
+ return createResult({ id: 'og-image-exists', name: 'OG Image Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:image)', { recommendation: 'This rule checks for the presence of og:image meta tag' });
263
311
  },
264
312
  },
265
313
  {
@@ -269,8 +317,9 @@ export const metaRules = [
269
317
  severity: 'error',
270
318
  description: 'og:image URL must use HTTPS',
271
319
  check: (ctx) => {
272
- if (!ctx.ogImage)
273
- return null;
320
+ if (!ctx.ogImage) {
321
+ return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL protocol when present' });
322
+ }
274
323
  if (ctx.ogImage.startsWith('http://')) {
275
324
  return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'fail', 'og:image uses HTTP instead of HTTPS', { value: ctx.ogImage, recommendation: 'Always use HTTPS for og:image URLs' });
276
325
  }
@@ -310,13 +359,14 @@ export const metaRules = [
310
359
  severity: 'warning',
311
360
  description: 'og:image URL should be under 2000 characters',
312
361
  check: (ctx) => {
313
- if (!ctx.ogImage)
314
- return null;
362
+ if (!ctx.ogImage) {
363
+ return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL length when present' });
364
+ }
315
365
  const maxLen = SEO_THRESHOLDS.og.meta.maxUrlLength;
316
366
  if (ctx.ogImage.length > maxLen) {
317
367
  return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'warn', `og:image URL too long (${ctx.ogImage.length} chars, max: ${maxLen})`, { value: ctx.ogImage.length, recommendation: 'Shorten the image URL path' });
318
368
  }
319
- return null;
369
+ return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL length is acceptable)', { recommendation: 'This rule checks if og:image URL exceeds length limits' });
320
370
  },
321
371
  },
322
372
  {
@@ -326,8 +376,9 @@ export const metaRules = [
326
376
  severity: 'warning',
327
377
  description: 'og:image URL should not have expiring tokens or excessive query params',
328
378
  check: (ctx) => {
329
- if (!ctx.ogImage)
330
- return null;
379
+ if (!ctx.ogImage) {
380
+ return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL for expiring tokens when present' });
381
+ }
331
382
  try {
332
383
  const url = new URL(ctx.ogImage);
333
384
  const params = url.searchParams;
@@ -342,7 +393,7 @@ export const metaRules = [
342
393
  }
343
394
  catch {
344
395
  }
345
- return null;
396
+ return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL has good quality)', { recommendation: 'This rule checks for expiring tokens and excessive query parameters' });
346
397
  },
347
398
  },
348
399
  {
@@ -352,15 +403,16 @@ export const metaRules = [
352
403
  severity: 'info',
353
404
  description: 'og:description should not have excessive emojis',
354
405
  check: (ctx) => {
355
- if (!ctx.ogDescription)
356
- return null;
406
+ if (!ctx.ogDescription) {
407
+ return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', 'Not applicable (no og:description)', { recommendation: 'This rule checks emoji usage in og:description when present' });
408
+ }
357
409
  const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]/gu;
358
410
  const emojis = ctx.ogDescription.match(emojiRegex) || [];
359
411
  const maxEmojis = SEO_THRESHOLDS.og.meta.maxDescriptionEmojis;
360
412
  if (emojis.length > maxEmojis) {
361
413
  return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', `og:description has ${emojis.length} emojis (recommended max: ${maxEmojis})`, { value: emojis.length, recommendation: 'Reduce emojis in og:description for better compatibility' });
362
414
  }
363
- return null;
415
+ return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', 'Not applicable (og:description has acceptable emoji count)', { recommendation: 'This rule checks for excessive emoji usage in og:description' });
364
416
  },
365
417
  },
366
418
  {
@@ -370,18 +422,20 @@ export const metaRules = [
370
422
  severity: 'warning',
371
423
  description: 'og:title should not be mostly uppercase (Meta may flag as low quality)',
372
424
  check: (ctx) => {
373
- if (!ctx.ogTitle)
374
- return null;
425
+ if (!ctx.ogTitle) {
426
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks capitalization in og:title when present' });
427
+ }
375
428
  const letters = ctx.ogTitle.replace(/[^a-zA-Z]/g, '');
376
- if (letters.length < 5)
377
- return null;
429
+ if (letters.length < 5) {
430
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title has too few letters to evaluate)', { recommendation: 'This rule checks for excessive uppercase in og:title' });
431
+ }
378
432
  const uppercase = letters.replace(/[^A-Z]/g, '').length;
379
433
  const percentage = Math.round((uppercase / letters.length) * 100);
380
434
  const maxCaps = SEO_THRESHOLDS.og.meta.maxCapsPercentage;
381
435
  if (percentage > maxCaps) {
382
436
  return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'warn', `og:title has ${percentage}% uppercase (Meta may flag as low quality)`, { value: percentage, recommendation: 'Use normal capitalization in og:title' });
383
437
  }
384
- return null;
438
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title uses proper capitalization)', { recommendation: 'This rule checks for excessive uppercase in og:title' });
385
439
  },
386
440
  },
387
441
  {
@@ -417,7 +471,7 @@ export const metaRules = [
417
471
  if (ctx.ogTitle && !ctx.title) {
418
472
  return createResult({ id: 'og-fallback-meta-title', name: 'Fallback Meta Title', category: 'og', severity: 'info' }, 'info', 'No <title> tag found (og:title exists)', { recommendation: 'Add <title> tag as fallback for universal compatibility' });
419
473
  }
420
- return null;
474
+ return createResult({ id: 'og-fallback-meta-title', name: 'Fallback Meta Title', category: 'og', severity: 'info' }, 'info', 'Not applicable (page has title tag or no og:title)', { recommendation: 'This rule checks for title tag when og:title exists' });
421
475
  },
422
476
  },
423
477
  {
@@ -427,8 +481,9 @@ export const metaRules = [
427
481
  severity: 'warning',
428
482
  description: 'og:image should not have redirect chains (Meta blocks >2 redirects)',
429
483
  check: (ctx) => {
430
- if (!ctx.ogImage)
431
- return null;
484
+ if (!ctx.ogImage) {
485
+ return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL for redirect patterns when present' });
486
+ }
432
487
  try {
433
488
  const url = new URL(ctx.ogImage);
434
489
  const redirectPatterns = ['redirect', 'proxy', 'forward', 'goto', 'redir', 'bounce'];
@@ -439,7 +494,7 @@ export const metaRules = [
439
494
  }
440
495
  catch {
441
496
  }
442
- return null;
497
+ return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL has no redirect patterns)', { recommendation: 'This rule checks for redirect patterns in og:image URL' });
443
498
  },
444
499
  },
445
500
  {
@@ -449,8 +504,9 @@ export const metaRules = [
449
504
  severity: 'warning',
450
505
  description: 'og:image must be publicly accessible (no auth, no private URLs)',
451
506
  check: (ctx) => {
452
- if (!ctx.ogImage)
453
- return null;
507
+ if (!ctx.ogImage) {
508
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL accessibility when present' });
509
+ }
454
510
  try {
455
511
  const url = new URL(ctx.ogImage);
456
512
  if (url.username || url.password) {
@@ -464,7 +520,7 @@ export const metaRules = [
464
520
  }
465
521
  catch {
466
522
  }
467
- return null;
523
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL is publicly accessible)', { recommendation: 'This rule checks for authentication or private IPs in og:image URL' });
468
524
  },
469
525
  },
470
526
  {
@@ -492,14 +548,15 @@ export const metaRules = [
492
548
  description: 'twitter:title should be 55-70 characters',
493
549
  check: (ctx) => {
494
550
  const title = ctx.twitterTitle || ctx.ogTitle;
495
- if (!title)
496
- return null;
551
+ if (!title) {
552
+ return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (no twitter:title or og:title)', { recommendation: 'This rule checks Twitter title length when present' });
553
+ }
497
554
  const len = title.length;
498
555
  const { ideal, max } = SEO_THRESHOLDS.twitter.title;
499
556
  if (len > max) {
500
557
  return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:title too long (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to ${ideal.max} characters` });
501
558
  }
502
- return null;
559
+ return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (Twitter title length is acceptable)', { recommendation: 'This rule checks if Twitter title exceeds length limits' });
503
560
  },
504
561
  },
505
562
  {
@@ -510,14 +567,15 @@ export const metaRules = [
510
567
  description: 'twitter:description should be 125-200 characters',
511
568
  check: (ctx) => {
512
569
  const description = ctx.twitterDescription || ctx.ogDescription;
513
- if (!description)
514
- return null;
570
+ if (!description) {
571
+ return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (no twitter:description or og:description)', { recommendation: 'This rule checks Twitter description length when present' });
572
+ }
515
573
  const len = description.length;
516
574
  const { max } = SEO_THRESHOLDS.twitter.description;
517
575
  if (len > max) {
518
576
  return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:description too long (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to ${max} characters` });
519
577
  }
520
- return null;
578
+ return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (Twitter description length is acceptable)', { recommendation: 'This rule checks if Twitter description exceeds length limits' });
521
579
  },
522
580
  },
523
581
  {
@@ -527,8 +585,9 @@ export const metaRules = [
527
585
  severity: 'warning',
528
586
  description: 'Title should have at least 10 characters for SEO value',
529
587
  check: (ctx) => {
530
- if (!ctx.title)
531
- return null;
588
+ if (!ctx.title) {
589
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks if title is too short when present' });
590
+ }
532
591
  const len = ctx.titleLength ?? ctx.title.length;
533
592
  if (len <= 10) {
534
593
  return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'warn', `Title is very short (${len} chars)`, {
@@ -541,7 +600,47 @@ export const metaRules = [
541
600
  }
542
601
  });
543
602
  }
544
- return null;
603
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title has sufficient length)', { recommendation: 'This rule checks if title is shorter than 10 characters' });
604
+ },
605
+ },
606
+ {
607
+ id: 'keywords-in-title',
608
+ name: 'Keywords in Title',
609
+ category: 'title',
610
+ severity: 'warning',
611
+ description: 'Title should contain main keywords found in content',
612
+ check: (ctx) => {
613
+ if (ctx.keywordsInTitle === false && ctx.topKeywords && ctx.topKeywords.length > 0) {
614
+ return createResult({ id: 'keywords-in-title', name: 'Keywords in Title', category: 'title', severity: 'warning' }, 'warn', 'Title does not appear to contain top keywords', {
615
+ recommendation: 'Include your main target keywords in the page title.',
616
+ evidence: {
617
+ found: `Title: "${ctx.title}"`,
618
+ expected: `Should contain one of: ${ctx.topKeywords.join(', ')}`,
619
+ impact: 'Keywords in title are a strong ranking signal.'
620
+ }
621
+ });
622
+ }
623
+ return createResult({ id: 'keywords-in-title', name: 'Keywords in Title', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title contains keywords or no keyword data)', { recommendation: 'This rule checks if title contains main keywords from content' });
624
+ },
625
+ },
626
+ {
627
+ id: 'keywords-in-description',
628
+ name: 'Keywords in Description',
629
+ category: 'meta',
630
+ severity: 'info',
631
+ description: 'Meta description should contain main keywords',
632
+ check: (ctx) => {
633
+ if (ctx.keywordsInDescription === false && ctx.topKeywords && ctx.topKeywords.length > 0) {
634
+ return createResult({ id: 'keywords-in-description', name: 'Keywords in Description', category: 'meta', severity: 'info' }, 'info', 'Meta description does not appear to contain top keywords', {
635
+ recommendation: 'Include main keywords in the description to embolden them in search results.',
636
+ evidence: {
637
+ found: 'Description does not match top keywords',
638
+ expected: `Should contain one of: ${ctx.topKeywords.join(', ')}`,
639
+ impact: 'Keywords in description are bolded in SERPs, improving CTR.'
640
+ }
641
+ });
642
+ }
643
+ return createResult({ id: 'keywords-in-description', name: 'Keywords in Description', category: 'meta', severity: 'info' }, 'info', 'Not applicable (description contains keywords or no keyword data)', { recommendation: 'This rule checks if meta description contains main keywords' });
545
644
  },
546
645
  },
547
646
  ];
@@ -26,8 +26,9 @@ export const mobileRules = [
26
26
  severity: 'warning',
27
27
  description: 'Viewport should allow user scaling for accessibility',
28
28
  check: (ctx) => {
29
- if (!ctx.viewportContent)
30
- return null;
29
+ if (!ctx.viewportContent) {
30
+ return createResult({ id: 'viewport-scalable', name: 'Viewport Scalable', category: 'mobile', severity: 'warning' }, 'info', 'Not applicable (no viewport meta tag)', { recommendation: 'First add a viewport meta tag, then ensure it allows user scaling' });
31
+ }
31
32
  const content = ctx.viewportContent.toLowerCase();
32
33
  const hasUserScalableNo = /user-scalable\s*=\s*no/.test(content);
33
34
  const hasMaximumScale1 = /maximum-scale\s*=\s*1(\.0)?/.test(content);
@@ -48,4 +49,113 @@ export const mobileRules = [
48
49
  return createResult({ id: 'viewport-scalable', name: 'Viewport Scalable', category: 'mobile', severity: 'warning' }, 'pass', 'Viewport allows user scaling');
49
50
  },
50
51
  },
52
+ {
53
+ id: 'viewport-device-width',
54
+ name: 'Viewport Device Width',
55
+ category: 'mobile',
56
+ severity: 'warning',
57
+ description: 'Viewport should use width=device-width for responsive design',
58
+ check: (ctx) => {
59
+ if (!ctx.viewportContent) {
60
+ return createResult({ id: 'viewport-device-width', name: 'Viewport Device Width', category: 'mobile', severity: 'warning' }, 'info', 'Not applicable (no viewport meta tag)', { recommendation: 'First add a viewport meta tag with width=device-width' });
61
+ }
62
+ const content = ctx.viewportContent.toLowerCase();
63
+ const hasDeviceWidth = /width\s*=\s*device-width/.test(content);
64
+ const hasFixedWidth = /width\s*=\s*(\d+)/.test(content);
65
+ if (hasFixedWidth && !hasDeviceWidth) {
66
+ const match = content.match(/width\s*=\s*(\d+)/);
67
+ const fixedWidth = match ? match[1] : 'unknown';
68
+ return createResult({ id: 'viewport-device-width', name: 'Viewport Device Width', category: 'mobile', severity: 'warning' }, 'warn', `Viewport uses fixed width (${fixedWidth}px) instead of device-width`, {
69
+ recommendation: 'Use width=device-width to make your site responsive across all devices',
70
+ evidence: {
71
+ found: ctx.viewportContent,
72
+ expected: 'width=device-width',
73
+ impact: 'Fixed width viewports force users to scroll horizontally on different screen sizes',
74
+ example: '<meta name="viewport" content="width=device-width, initial-scale=1">',
75
+ },
76
+ });
77
+ }
78
+ if (!hasDeviceWidth) {
79
+ return createResult({ id: 'viewport-device-width', name: 'Viewport Device Width', category: 'mobile', severity: 'warning' }, 'info', 'Viewport missing width=device-width', {
80
+ recommendation: 'Add width=device-width to ensure proper responsive behavior',
81
+ evidence: {
82
+ found: ctx.viewportContent,
83
+ expected: 'width=device-width',
84
+ },
85
+ });
86
+ }
87
+ return createResult({ id: 'viewport-device-width', name: 'Viewport Device Width', category: 'mobile', severity: 'warning' }, 'pass', 'Viewport uses device-width correctly');
88
+ },
89
+ },
90
+ {
91
+ id: 'viewport-initial-scale',
92
+ name: 'Viewport Initial Scale',
93
+ category: 'mobile',
94
+ severity: 'info',
95
+ description: 'Viewport should include initial-scale=1 for consistent initial zoom',
96
+ check: (ctx) => {
97
+ if (!ctx.viewportContent) {
98
+ return createResult({ id: 'viewport-initial-scale', name: 'Viewport Initial Scale', category: 'mobile', severity: 'info' }, 'info', 'Not applicable (no viewport meta tag)', { recommendation: 'First add a viewport meta tag with initial-scale=1' });
99
+ }
100
+ const content = ctx.viewportContent.toLowerCase();
101
+ const hasInitialScale = /initial-scale\s*=\s*1(\.0)?/.test(content);
102
+ if (!hasInitialScale) {
103
+ return createResult({ id: 'viewport-initial-scale', name: 'Viewport Initial Scale', category: 'mobile', severity: 'info' }, 'info', 'Viewport missing initial-scale=1', {
104
+ recommendation: 'Add initial-scale=1 to set consistent initial zoom level',
105
+ evidence: {
106
+ found: ctx.viewportContent,
107
+ expected: 'initial-scale=1',
108
+ example: '<meta name="viewport" content="width=device-width, initial-scale=1">',
109
+ },
110
+ });
111
+ }
112
+ return createResult({ id: 'viewport-initial-scale', name: 'Viewport Initial Scale', category: 'mobile', severity: 'info' }, 'pass', 'Viewport has initial-scale=1');
113
+ },
114
+ },
115
+ {
116
+ id: 'viewport-complete',
117
+ name: 'Viewport Complete Configuration',
118
+ category: 'mobile',
119
+ severity: 'info',
120
+ description: 'Viewport should have the complete recommended configuration',
121
+ check: (ctx) => {
122
+ if (!ctx.viewportContent) {
123
+ return createResult({ id: 'viewport-complete', name: 'Viewport Complete', category: 'mobile', severity: 'info' }, 'info', 'Not applicable (no viewport meta tag)', { recommendation: 'Add a complete viewport meta tag: <meta name="viewport" content="width=device-width, initial-scale=1">' });
124
+ }
125
+ const content = ctx.viewportContent.toLowerCase();
126
+ const hasDeviceWidth = /width\s*=\s*device-width/.test(content);
127
+ const hasInitialScale = /initial-scale\s*=\s*1(\.0)?/.test(content);
128
+ const hasUserScalableNo = /user-scalable\s*=\s*no/.test(content);
129
+ const hasFixedWidth = /width\s*=\s*\d+/.test(content) && !hasDeviceWidth;
130
+ const issues = [];
131
+ if (!hasDeviceWidth)
132
+ issues.push('missing width=device-width');
133
+ if (!hasInitialScale)
134
+ issues.push('missing initial-scale=1');
135
+ if (hasUserScalableNo)
136
+ issues.push('has user-scalable=no');
137
+ if (hasFixedWidth)
138
+ issues.push('uses fixed width');
139
+ if (issues.length === 0) {
140
+ return createResult({ id: 'viewport-complete', name: 'Viewport Complete', category: 'mobile', severity: 'info' }, 'pass', 'Viewport has optimal configuration for mobile', { evidence: { found: ctx.viewportContent } });
141
+ }
142
+ if (issues.length >= 2 || hasFixedWidth) {
143
+ return createResult({ id: 'viewport-complete', name: 'Viewport Complete', category: 'mobile', severity: 'info' }, 'warn', `Viewport configuration has issues: ${issues.join(', ')}`, {
144
+ recommendation: 'Use the recommended viewport configuration for best mobile experience',
145
+ evidence: {
146
+ found: ctx.viewportContent,
147
+ expected: '<meta name="viewport" content="width=device-width, initial-scale=1">',
148
+ issue: issues.join('; '),
149
+ },
150
+ });
151
+ }
152
+ return createResult({ id: 'viewport-complete', name: 'Viewport Complete', category: 'mobile', severity: 'info' }, 'info', `Viewport configuration could be improved: ${issues.join(', ')}`, {
153
+ recommendation: 'Consider using the complete recommended viewport configuration',
154
+ evidence: {
155
+ found: ctx.viewportContent,
156
+ expected: '<meta name="viewport" content="width=device-width, initial-scale=1">',
157
+ },
158
+ });
159
+ },
160
+ },
51
161
  ];