recker 1.0.43 → 1.0.44

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 (459) hide show
  1. package/README.md +47 -0
  2. package/dist/bin/recker-linux-x64 +0 -0
  3. package/dist/bin/recker-macos-x64 +0 -0
  4. package/dist/bin/recker-win-x64.exe +0 -0
  5. package/dist/bin/rek.cjs +85152 -100207
  6. package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
  7. package/dist/browser/ai/adaptive-timeout.js +208 -0
  8. package/dist/browser/ai/client.d.ts +22 -0
  9. package/dist/browser/ai/client.js +294 -0
  10. package/dist/browser/ai/index.d.ts +14 -0
  11. package/dist/browser/ai/index.js +11 -0
  12. package/dist/browser/ai/providers/anthropic.d.ts +63 -0
  13. package/dist/browser/ai/providers/anthropic.js +370 -0
  14. package/dist/browser/ai/providers/base.d.ts +48 -0
  15. package/dist/browser/ai/providers/base.js +150 -0
  16. package/dist/browser/ai/providers/google.d.ts +59 -0
  17. package/dist/browser/ai/providers/google.js +305 -0
  18. package/dist/browser/ai/providers/ollama.d.ts +44 -0
  19. package/dist/browser/ai/providers/ollama.js +240 -0
  20. package/dist/browser/ai/providers/openai.d.ts +64 -0
  21. package/dist/browser/ai/providers/openai.js +298 -0
  22. package/dist/browser/ai/rate-limiter.d.ts +43 -0
  23. package/dist/browser/ai/rate-limiter.js +215 -0
  24. package/dist/browser/ai/vector/index.d.ts +2 -0
  25. package/dist/browser/ai/vector/index.js +2 -0
  26. package/dist/browser/ai/vector/similarity.d.ts +2 -0
  27. package/dist/browser/ai/vector/similarity.js +27 -0
  28. package/dist/browser/ai/vector/store.d.ts +27 -0
  29. package/dist/browser/ai/vector/store.js +82 -0
  30. package/dist/browser/browser/cache.d.ts +2 -40
  31. package/dist/browser/browser/cache.js +2 -199
  32. package/dist/browser/browser/index.d.ts +8 -0
  33. package/dist/browser/browser/index.js +8 -0
  34. package/dist/browser/browser/recker.d.ts +8 -1
  35. package/dist/browser/browser/recker.js +8 -2
  36. package/dist/browser/cache/indexed-db.d.ts +10 -0
  37. package/dist/browser/cache/indexed-db.js +88 -0
  38. package/dist/browser/cache/service-worker-cache.d.ts +18 -0
  39. package/dist/browser/cache/service-worker-cache.js +103 -0
  40. package/dist/browser/cache.d.ts +2 -40
  41. package/dist/browser/cache.js +2 -199
  42. package/dist/browser/constants/user-agents.d.ts +7 -0
  43. package/dist/browser/constants/user-agents.js +7 -0
  44. package/dist/browser/core/client.d.ts +2 -0
  45. package/dist/browser/core/client.js +19 -1
  46. package/dist/browser/index.d.ts +8 -0
  47. package/dist/browser/index.js +8 -0
  48. package/dist/browser/plugins/har-recorder.d.ts +40 -0
  49. package/dist/browser/plugins/har-recorder.js +120 -0
  50. package/dist/browser/plugins/network-simulation.d.ts +7 -0
  51. package/dist/browser/plugins/network-simulation.js +13 -0
  52. package/dist/browser/presets/android.d.ts +2 -0
  53. package/dist/browser/presets/android.js +16 -0
  54. package/dist/browser/presets/anthropic.d.ts +8 -0
  55. package/dist/browser/presets/anthropic.js +27 -0
  56. package/dist/browser/presets/aws.d.ts +19 -0
  57. package/dist/browser/presets/aws.js +68 -0
  58. package/dist/browser/presets/azure-openai.d.ts +10 -0
  59. package/dist/browser/presets/azure-openai.js +35 -0
  60. package/dist/browser/presets/azure.d.ts +41 -0
  61. package/dist/browser/presets/azure.js +104 -0
  62. package/dist/browser/presets/chaturbate.d.ts +2 -0
  63. package/dist/browser/presets/chaturbate.js +17 -0
  64. package/dist/browser/presets/cloudflare.d.ts +12 -0
  65. package/dist/browser/presets/cloudflare.js +39 -0
  66. package/dist/browser/presets/cohere.d.ts +7 -0
  67. package/dist/browser/presets/cohere.js +22 -0
  68. package/dist/browser/presets/deepseek.d.ts +7 -0
  69. package/dist/browser/presets/deepseek.js +22 -0
  70. package/dist/browser/presets/digitalocean.d.ts +5 -0
  71. package/dist/browser/presets/digitalocean.js +16 -0
  72. package/dist/browser/presets/discord.d.ts +6 -0
  73. package/dist/browser/presets/discord.js +17 -0
  74. package/dist/browser/presets/elevenlabs.d.ts +6 -0
  75. package/dist/browser/presets/elevenlabs.js +20 -0
  76. package/dist/browser/presets/enhancers.d.ts +20 -0
  77. package/dist/browser/presets/enhancers.js +85 -0
  78. package/dist/browser/presets/fireworks.d.ts +7 -0
  79. package/dist/browser/presets/fireworks.js +22 -0
  80. package/dist/browser/presets/gcp.d.ts +34 -0
  81. package/dist/browser/presets/gcp.js +91 -0
  82. package/dist/browser/presets/gemini.d.ts +7 -0
  83. package/dist/browser/presets/gemini.js +23 -0
  84. package/dist/browser/presets/github.d.ts +6 -0
  85. package/dist/browser/presets/github.js +17 -0
  86. package/dist/browser/presets/gitlab.d.ts +6 -0
  87. package/dist/browser/presets/gitlab.js +16 -0
  88. package/dist/browser/presets/groq.d.ts +7 -0
  89. package/dist/browser/presets/groq.js +22 -0
  90. package/dist/browser/presets/hubspot.d.ts +9 -0
  91. package/dist/browser/presets/hubspot.js +28 -0
  92. package/dist/browser/presets/huggingface.d.ts +7 -0
  93. package/dist/browser/presets/huggingface.js +23 -0
  94. package/dist/browser/presets/index.d.ts +47 -0
  95. package/dist/browser/presets/index.js +47 -0
  96. package/dist/browser/presets/ios.d.ts +2 -0
  97. package/dist/browser/presets/ios.js +13 -0
  98. package/dist/browser/presets/linear.d.ts +5 -0
  99. package/dist/browser/presets/linear.js +16 -0
  100. package/dist/browser/presets/mailgun.d.ts +7 -0
  101. package/dist/browser/presets/mailgun.js +20 -0
  102. package/dist/browser/presets/meta.d.ts +10 -0
  103. package/dist/browser/presets/meta.js +33 -0
  104. package/dist/browser/presets/mistral.d.ts +7 -0
  105. package/dist/browser/presets/mistral.js +22 -0
  106. package/dist/browser/presets/notion.d.ts +6 -0
  107. package/dist/browser/presets/notion.js +17 -0
  108. package/dist/browser/presets/openai.d.ts +9 -0
  109. package/dist/browser/presets/openai.js +30 -0
  110. package/dist/browser/presets/oracle.d.ts +19 -0
  111. package/dist/browser/presets/oracle.js +117 -0
  112. package/dist/browser/presets/perplexity.d.ts +7 -0
  113. package/dist/browser/presets/perplexity.js +22 -0
  114. package/dist/browser/presets/pinecone.d.ts +8 -0
  115. package/dist/browser/presets/pinecone.js +42 -0
  116. package/dist/browser/presets/registry.d.ts +23 -0
  117. package/dist/browser/presets/registry.js +519 -0
  118. package/dist/browser/presets/replicate.d.ts +7 -0
  119. package/dist/browser/presets/replicate.js +23 -0
  120. package/dist/browser/presets/sendgrid.d.ts +6 -0
  121. package/dist/browser/presets/sendgrid.js +20 -0
  122. package/dist/browser/presets/sentry.d.ts +11 -0
  123. package/dist/browser/presets/sentry.js +48 -0
  124. package/dist/browser/presets/sinch.d.ts +9 -0
  125. package/dist/browser/presets/sinch.js +39 -0
  126. package/dist/browser/presets/slack.d.ts +5 -0
  127. package/dist/browser/presets/slack.js +16 -0
  128. package/dist/browser/presets/square.d.ts +10 -0
  129. package/dist/browser/presets/square.js +33 -0
  130. package/dist/browser/presets/stripe.d.ts +7 -0
  131. package/dist/browser/presets/stripe.js +23 -0
  132. package/dist/browser/presets/supabase.d.ts +6 -0
  133. package/dist/browser/presets/supabase.js +18 -0
  134. package/dist/browser/presets/tiktok.d.ts +10 -0
  135. package/dist/browser/presets/tiktok.js +38 -0
  136. package/dist/browser/presets/together.d.ts +7 -0
  137. package/dist/browser/presets/together.js +22 -0
  138. package/dist/browser/presets/twilio.d.ts +6 -0
  139. package/dist/browser/presets/twilio.js +17 -0
  140. package/dist/browser/presets/vercel.d.ts +6 -0
  141. package/dist/browser/presets/vercel.js +23 -0
  142. package/dist/browser/presets/vultr.d.ts +5 -0
  143. package/dist/browser/presets/vultr.js +16 -0
  144. package/dist/browser/presets/xai.d.ts +8 -0
  145. package/dist/browser/presets/xai.js +23 -0
  146. package/dist/browser/presets/youtube.d.ts +5 -0
  147. package/dist/browser/presets/youtube.js +20 -0
  148. package/dist/browser/recker.d.ts +8 -1
  149. package/dist/browser/recker.js +8 -2
  150. package/dist/browser/scrape/document.d.ts +5 -4
  151. package/dist/browser/scrape/document.js +89 -76
  152. package/dist/browser/scrape/element.d.ts +10 -8
  153. package/dist/browser/scrape/element.js +295 -81
  154. package/dist/browser/scrape/extractors.d.ts +11 -11
  155. package/dist/browser/scrape/extractors.js +145 -113
  156. package/dist/browser/scrape/parser/back.d.ts +1 -0
  157. package/dist/browser/scrape/parser/back.js +3 -0
  158. package/dist/browser/scrape/parser/index.d.ts +20 -0
  159. package/dist/browser/scrape/parser/index.js +19 -0
  160. package/dist/browser/scrape/parser/matcher.d.ts +30 -0
  161. package/dist/browser/scrape/parser/matcher.js +99 -0
  162. package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
  163. package/dist/browser/scrape/parser/nodes/comment.js +21 -0
  164. package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
  165. package/dist/browser/scrape/parser/nodes/html.js +978 -0
  166. package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
  167. package/dist/browser/scrape/parser/nodes/node.js +31 -0
  168. package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
  169. package/dist/browser/scrape/parser/nodes/text.js +30 -0
  170. package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
  171. package/dist/browser/scrape/parser/nodes/type.js +7 -0
  172. package/dist/browser/scrape/parser/parse.d.ts +1 -0
  173. package/dist/browser/scrape/parser/parse.js +1 -0
  174. package/dist/browser/scrape/parser/valid.d.ts +2 -0
  175. package/dist/browser/scrape/parser/valid.js +5 -0
  176. package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
  177. package/dist/browser/scrape/parser/void-tag.js +43 -0
  178. package/dist/browser/scrape/types.d.ts +7 -0
  179. package/dist/browser/seo/analyzer.d.ts +59 -0
  180. package/dist/browser/seo/analyzer.js +1399 -0
  181. package/dist/browser/seo/keywords.d.ts +16 -0
  182. package/dist/browser/seo/keywords.js +55 -0
  183. package/dist/browser/seo/rules/accessibility.d.ts +2 -0
  184. package/dist/browser/seo/rules/accessibility.js +733 -0
  185. package/dist/browser/seo/rules/ai-search.d.ts +2 -0
  186. package/dist/browser/seo/rules/ai-search.js +436 -0
  187. package/dist/browser/seo/rules/analytics.d.ts +2 -0
  188. package/dist/browser/seo/rules/analytics.js +306 -0
  189. package/dist/browser/seo/rules/best-practices.d.ts +2 -0
  190. package/dist/browser/seo/rules/best-practices.js +195 -0
  191. package/dist/browser/seo/rules/canonical.d.ts +12 -0
  192. package/dist/browser/seo/rules/canonical.js +270 -0
  193. package/dist/browser/seo/rules/content.d.ts +2 -0
  194. package/dist/browser/seo/rules/content.js +522 -0
  195. package/dist/browser/seo/rules/crawl.d.ts +2 -0
  196. package/dist/browser/seo/rules/crawl.js +435 -0
  197. package/dist/browser/seo/rules/cwv.d.ts +2 -0
  198. package/dist/browser/seo/rules/cwv.js +248 -0
  199. package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
  200. package/dist/browser/seo/rules/ecommerce.js +312 -0
  201. package/dist/browser/seo/rules/i18n.d.ts +2 -0
  202. package/dist/browser/seo/rules/i18n.js +288 -0
  203. package/dist/browser/seo/rules/images.d.ts +2 -0
  204. package/dist/browser/seo/rules/images.js +255 -0
  205. package/dist/browser/seo/rules/index.d.ts +52 -0
  206. package/dist/browser/seo/rules/index.js +159 -0
  207. package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
  208. package/dist/browser/seo/rules/internal-linking.js +394 -0
  209. package/dist/browser/seo/rules/links.d.ts +2 -0
  210. package/dist/browser/seo/rules/links.js +498 -0
  211. package/dist/browser/seo/rules/local.d.ts +2 -0
  212. package/dist/browser/seo/rules/local.js +289 -0
  213. package/dist/browser/seo/rules/meta.d.ts +2 -0
  214. package/dist/browser/seo/rules/meta.js +805 -0
  215. package/dist/browser/seo/rules/mobile.d.ts +2 -0
  216. package/dist/browser/seo/rules/mobile.js +161 -0
  217. package/dist/browser/seo/rules/performance.d.ts +2 -0
  218. package/dist/browser/seo/rules/performance.js +738 -0
  219. package/dist/browser/seo/rules/pwa.d.ts +2 -0
  220. package/dist/browser/seo/rules/pwa.js +299 -0
  221. package/dist/browser/seo/rules/readability.d.ts +2 -0
  222. package/dist/browser/seo/rules/readability.js +264 -0
  223. package/dist/browser/seo/rules/redirects.d.ts +16 -0
  224. package/dist/browser/seo/rules/redirects.js +199 -0
  225. package/dist/browser/seo/rules/resources.d.ts +2 -0
  226. package/dist/browser/seo/rules/resources.js +390 -0
  227. package/dist/browser/seo/rules/schema.d.ts +2 -0
  228. package/dist/browser/seo/rules/schema.js +379 -0
  229. package/dist/browser/seo/rules/security.d.ts +2 -0
  230. package/dist/browser/seo/rules/security.js +877 -0
  231. package/dist/browser/seo/rules/social.d.ts +2 -0
  232. package/dist/browser/seo/rules/social.js +603 -0
  233. package/dist/browser/seo/rules/structural.d.ts +2 -0
  234. package/dist/browser/seo/rules/structural.js +223 -0
  235. package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
  236. package/dist/browser/seo/rules/technical-advanced.js +289 -0
  237. package/dist/browser/seo/rules/technical.d.ts +2 -0
  238. package/dist/browser/seo/rules/technical.js +480 -0
  239. package/dist/browser/seo/rules/thresholds.d.ts +196 -0
  240. package/dist/browser/seo/rules/thresholds.js +118 -0
  241. package/dist/browser/seo/rules/types.d.ts +498 -0
  242. package/dist/browser/seo/rules/types.js +11 -0
  243. package/dist/browser/seo/types.d.ts +211 -0
  244. package/dist/browser/seo/types.js +1 -0
  245. package/dist/browser/transport/curl.d.ts +4 -0
  246. package/dist/browser/transport/curl.js +101 -0
  247. package/dist/browser/transport/undici.js +1 -2
  248. package/dist/browser/transport/worker.d.ts +18 -0
  249. package/dist/browser/transport/worker.js +278 -0
  250. package/dist/browser/types/index.d.ts +4 -1
  251. package/dist/browser/utils/binary-manager.d.ts +4 -0
  252. package/dist/browser/utils/binary-manager.js +72 -0
  253. package/dist/browser/utils/user-agent.js +2 -13
  254. package/dist/cache/indexed-db.d.ts +10 -0
  255. package/dist/cache/indexed-db.js +88 -0
  256. package/dist/cache/service-worker-cache.d.ts +18 -0
  257. package/dist/cache/service-worker-cache.js +103 -0
  258. package/dist/cli/commands/ai.d.ts +2 -0
  259. package/dist/cli/commands/ai.js +162 -0
  260. package/dist/cli/commands/bench.d.ts +2 -0
  261. package/dist/cli/commands/bench.js +51 -0
  262. package/dist/cli/commands/dns.d.ts +2 -0
  263. package/dist/cli/commands/dns.js +295 -0
  264. package/dist/cli/commands/har.d.ts +2 -0
  265. package/dist/cli/commands/har.js +171 -0
  266. package/dist/cli/commands/hls.d.ts +2 -0
  267. package/dist/cli/commands/hls.js +192 -0
  268. package/dist/cli/commands/network.d.ts +2 -0
  269. package/dist/cli/commands/network.js +288 -0
  270. package/dist/cli/commands/protocols.d.ts +2 -0
  271. package/dist/cli/commands/protocols.js +344 -0
  272. package/dist/cli/commands/scrape.d.ts +2 -0
  273. package/dist/cli/commands/scrape.js +176 -0
  274. package/dist/cli/commands/security.d.ts +2 -0
  275. package/dist/cli/commands/security.js +57 -0
  276. package/dist/cli/commands/seo.d.ts +2 -0
  277. package/dist/cli/commands/seo.js +125 -0
  278. package/dist/cli/commands/serve.d.ts +2 -0
  279. package/dist/cli/commands/serve.js +531 -0
  280. package/dist/cli/commands/spider.d.ts +3 -0
  281. package/dist/cli/commands/spider.js +456 -0
  282. package/dist/cli/commands/utils.d.ts +2 -0
  283. package/dist/cli/commands/utils.js +176 -0
  284. package/dist/cli/commands/vector.d.ts +2 -0
  285. package/dist/cli/commands/vector.js +158 -0
  286. package/dist/cli/handler.d.ts +2 -2
  287. package/dist/cli/handler.js +6 -6
  288. package/dist/cli/helpers.d.ts +7 -0
  289. package/dist/cli/helpers.js +128 -0
  290. package/dist/cli/index.js +96 -5228
  291. package/dist/cli/parser/help.d.ts +2 -0
  292. package/dist/cli/parser/help.js +52 -0
  293. package/dist/cli/parser/index.d.ts +3 -0
  294. package/dist/cli/parser/index.js +3 -0
  295. package/dist/cli/parser/parser.d.ts +4 -0
  296. package/dist/cli/parser/parser.js +146 -0
  297. package/dist/cli/parser/types.d.ts +41 -0
  298. package/dist/cli/parser/types.js +1 -0
  299. package/dist/cli/presets.d.ts +1 -1
  300. package/dist/cli/presets.js +1 -1
  301. package/dist/cli/router.d.ts +36 -0
  302. package/dist/cli/router.js +195 -0
  303. package/dist/cli/tui/ai-chat.js +1 -1
  304. package/dist/cli/tui/commands/context.d.ts +9 -0
  305. package/dist/cli/tui/commands/context.js +1 -0
  306. package/dist/cli/tui/commands/dns.d.ts +10 -0
  307. package/dist/cli/tui/commands/dns.js +461 -0
  308. package/dist/cli/tui/commands/hls.d.ts +2 -0
  309. package/dist/cli/tui/commands/hls.js +162 -0
  310. package/dist/cli/tui/commands/ip.d.ts +2 -0
  311. package/dist/cli/tui/commands/ip.js +45 -0
  312. package/dist/cli/tui/commands/network.d.ts +3 -0
  313. package/dist/cli/tui/commands/network.js +81 -0
  314. package/dist/cli/tui/commands/protocols.d.ts +6 -0
  315. package/dist/cli/tui/commands/protocols.js +531 -0
  316. package/dist/cli/tui/commands/security.d.ts +2 -0
  317. package/dist/cli/tui/commands/security.js +48 -0
  318. package/dist/cli/tui/commands/seo.d.ts +2 -0
  319. package/dist/cli/tui/commands/seo.js +74 -0
  320. package/dist/cli/tui/context.d.ts +12 -0
  321. package/dist/cli/tui/context.js +1 -0
  322. package/dist/cli/tui/shell.d.ts +11 -20
  323. package/dist/cli/tui/shell.js +216 -1873
  324. package/dist/constants/user-agents.d.ts +7 -0
  325. package/dist/constants/user-agents.js +7 -0
  326. package/dist/core/client.d.ts +2 -0
  327. package/dist/core/client.js +19 -1
  328. package/dist/index.d.ts +1 -0
  329. package/dist/index.js +1 -0
  330. package/dist/mcp/cli.js +2 -3
  331. package/dist/mcp/data/embeddings.json +1 -1
  332. package/dist/mcp/tools/network.js +298 -158
  333. package/dist/plugins/har-player.d.ts +23 -0
  334. package/dist/plugins/har-player.js +49 -0
  335. package/dist/plugins/har-recorder.d.ts +37 -3
  336. package/dist/plugins/har-recorder.js +116 -63
  337. package/dist/plugins/network-simulation.d.ts +7 -0
  338. package/dist/plugins/network-simulation.js +13 -0
  339. package/dist/presets/android.d.ts +2 -0
  340. package/dist/presets/android.js +16 -0
  341. package/dist/presets/chaturbate.d.ts +2 -0
  342. package/dist/presets/chaturbate.js +17 -0
  343. package/dist/presets/elevenlabs.d.ts +6 -0
  344. package/dist/presets/elevenlabs.js +20 -0
  345. package/dist/presets/enhancers.d.ts +20 -0
  346. package/dist/presets/enhancers.js +85 -0
  347. package/dist/presets/hubspot.d.ts +9 -0
  348. package/dist/presets/hubspot.js +28 -0
  349. package/dist/presets/index.d.ts +10 -0
  350. package/dist/presets/index.js +10 -0
  351. package/dist/presets/ios.d.ts +2 -0
  352. package/dist/presets/ios.js +13 -0
  353. package/dist/presets/pinecone.d.ts +8 -0
  354. package/dist/presets/pinecone.js +42 -0
  355. package/dist/presets/registry.js +60 -0
  356. package/dist/presets/sendgrid.d.ts +6 -0
  357. package/dist/presets/sendgrid.js +20 -0
  358. package/dist/presets/sentry.d.ts +11 -0
  359. package/dist/presets/sentry.js +48 -0
  360. package/dist/presets/square.d.ts +10 -0
  361. package/dist/presets/square.js +33 -0
  362. package/dist/recker.d.ts +3 -0
  363. package/dist/recker.js +4 -0
  364. package/dist/scrape/document.d.ts +5 -4
  365. package/dist/scrape/document.js +89 -76
  366. package/dist/scrape/element.d.ts +10 -8
  367. package/dist/scrape/element.js +295 -81
  368. package/dist/scrape/extractors.d.ts +11 -11
  369. package/dist/scrape/extractors.js +145 -113
  370. package/dist/scrape/index.d.ts +2 -0
  371. package/dist/scrape/index.js +1 -0
  372. package/dist/scrape/parser/back.d.ts +1 -0
  373. package/dist/scrape/parser/back.js +3 -0
  374. package/dist/scrape/parser/index.d.ts +20 -0
  375. package/dist/scrape/parser/index.js +19 -0
  376. package/dist/scrape/parser/matcher.d.ts +30 -0
  377. package/dist/scrape/parser/matcher.js +99 -0
  378. package/dist/scrape/parser/nodes/comment.d.ts +12 -0
  379. package/dist/scrape/parser/nodes/comment.js +21 -0
  380. package/dist/scrape/parser/nodes/html.d.ts +110 -0
  381. package/dist/scrape/parser/nodes/html.js +978 -0
  382. package/dist/scrape/parser/nodes/node.d.ts +18 -0
  383. package/dist/scrape/parser/nodes/node.js +31 -0
  384. package/dist/scrape/parser/nodes/text.d.ts +14 -0
  385. package/dist/scrape/parser/nodes/text.js +30 -0
  386. package/dist/scrape/parser/nodes/type.d.ts +6 -0
  387. package/dist/scrape/parser/nodes/type.js +7 -0
  388. package/dist/scrape/parser/parse.d.ts +1 -0
  389. package/dist/scrape/parser/parse.js +1 -0
  390. package/dist/scrape/parser/valid.d.ts +2 -0
  391. package/dist/scrape/parser/valid.js +5 -0
  392. package/dist/scrape/parser/void-tag.d.ts +7 -0
  393. package/dist/scrape/parser/void-tag.js +43 -0
  394. package/dist/scrape/spider.d.ts +19 -0
  395. package/dist/scrape/spider.js +28 -3
  396. package/dist/scrape/types.d.ts +7 -0
  397. package/dist/seo/analyzer.d.ts +15 -5
  398. package/dist/seo/analyzer.js +636 -175
  399. package/dist/seo/formatter.d.ts +16 -0
  400. package/dist/seo/formatter.js +228 -0
  401. package/dist/seo/index.d.ts +2 -0
  402. package/dist/seo/index.js +1 -0
  403. package/dist/seo/keywords.d.ts +16 -0
  404. package/dist/seo/keywords.js +55 -0
  405. package/dist/seo/rules/accessibility.js +96 -57
  406. package/dist/seo/rules/ai-search.js +44 -31
  407. package/dist/seo/rules/analytics.d.ts +2 -0
  408. package/dist/seo/rules/analytics.js +306 -0
  409. package/dist/seo/rules/best-practices.js +21 -14
  410. package/dist/seo/rules/canonical.js +53 -32
  411. package/dist/seo/rules/content.js +317 -31
  412. package/dist/seo/rules/crawl.js +55 -40
  413. package/dist/seo/rules/cwv.js +21 -15
  414. package/dist/seo/rules/ecommerce.js +82 -22
  415. package/dist/seo/rules/i18n.js +75 -36
  416. package/dist/seo/rules/images.js +109 -30
  417. package/dist/seo/rules/index.js +2 -0
  418. package/dist/seo/rules/internal-linking.js +58 -39
  419. package/dist/seo/rules/links.js +79 -52
  420. package/dist/seo/rules/local.js +49 -25
  421. package/dist/seo/rules/meta.js +339 -81
  422. package/dist/seo/rules/mobile.js +112 -2
  423. package/dist/seo/rules/performance.js +434 -66
  424. package/dist/seo/rules/pwa.js +36 -39
  425. package/dist/seo/rules/readability.js +31 -22
  426. package/dist/seo/rules/redirects.js +21 -15
  427. package/dist/seo/rules/resources.js +59 -42
  428. package/dist/seo/rules/schema.js +333 -8
  429. package/dist/seo/rules/security.js +142 -80
  430. package/dist/seo/rules/social.js +277 -47
  431. package/dist/seo/rules/structural.js +87 -19
  432. package/dist/seo/rules/technical-advanced.js +30 -24
  433. package/dist/seo/rules/technical.js +243 -42
  434. package/dist/seo/rules/types.d.ts +53 -1
  435. package/dist/seo/seo-spider.d.ts +22 -0
  436. package/dist/seo/seo-spider.js +77 -13
  437. package/dist/seo/types.d.ts +8 -1
  438. package/dist/seo/validators/llms-txt.js +19 -0
  439. package/dist/seo/validators/rss.d.ts +11 -0
  440. package/dist/seo/validators/rss.js +93 -0
  441. package/dist/seo/validators/sitemap.js +36 -26
  442. package/dist/transport/curl.d.ts +4 -0
  443. package/dist/transport/curl.js +101 -0
  444. package/dist/transport/udp.js +0 -1
  445. package/dist/transport/undici.js +1 -2
  446. package/dist/transport/worker.d.ts +18 -0
  447. package/dist/transport/worker.js +278 -0
  448. package/dist/types/index.d.ts +4 -1
  449. package/dist/utils/binary-manager.d.ts +4 -0
  450. package/dist/utils/binary-manager.js +72 -0
  451. package/dist/utils/optional-require.d.ts +7 -8
  452. package/dist/utils/optional-require.js +2 -21
  453. package/dist/utils/upload.d.ts +6 -0
  454. package/dist/utils/upload.js +11 -0
  455. package/dist/utils/user-agent.js +2 -13
  456. package/dist/version.js +1 -1
  457. package/package.json +12 -6
  458. package/dist/browser/utils/optional-require.d.ts +0 -19
  459. package/dist/browser/utils/optional-require.js +0 -105
@@ -7,10 +7,12 @@ export const socialRules = [
7
7
  severity: 'warning',
8
8
  description: 'Open Graph images should meet minimum size requirements',
9
9
  check: (ctx) => {
10
- if (!ctx.ogImage)
11
- return null;
12
- if (!ctx.ogImageDimensions)
13
- return null;
10
+ if (!ctx.ogImage) {
11
+ return createResult({ id: 'social-og-image-size', name: 'OG Image Size', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image dimensions when present' });
12
+ }
13
+ if (!ctx.ogImageDimensions) {
14
+ return createResult({ id: 'social-og-image-size', name: 'OG Image Size', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image dimensions not available)', { recommendation: 'This rule checks if og:image meets minimum size requirements' });
15
+ }
14
16
  const { width, height } = ctx.ogImageDimensions;
15
17
  const minWidth = 1200;
16
18
  const minHeight = 630;
@@ -35,8 +37,9 @@ export const socialRules = [
35
37
  severity: 'info',
36
38
  description: 'Open Graph images should use optimal aspect ratio',
37
39
  check: (ctx) => {
38
- if (!ctx.ogImageDimensions)
39
- return null;
40
+ if (!ctx.ogImageDimensions) {
41
+ return createResult({ id: 'social-og-image-aspect-ratio', name: 'OG Image Aspect Ratio', category: 'og', severity: 'info' }, 'info', 'Not applicable (og:image dimensions not available)', { recommendation: 'This rule checks og:image aspect ratio when dimensions are available' });
42
+ }
40
43
  const { width, height } = ctx.ogImageDimensions;
41
44
  const ratio = width / height;
42
45
  const optimalRatio = 1.91;
@@ -61,8 +64,9 @@ export const socialRules = [
61
64
  severity: 'info',
62
65
  description: 'Open Graph should specify content locale',
63
66
  check: (ctx) => {
64
- if (ctx.ogLocale === undefined)
65
- return null;
67
+ if (ctx.ogLocale === undefined) {
68
+ return createResult({ id: 'social-og-locale', name: 'OG Locale', category: 'og', severity: 'info' }, 'info', 'Not applicable (og:locale property not checked)', { recommendation: 'This rule checks og:locale when locale data is available' });
69
+ }
66
70
  if (!ctx.ogLocale) {
67
71
  return createResult({ id: 'social-og-locale', name: 'OG Locale', category: 'og', severity: 'info' }, 'info', 'Missing og:locale', {
68
72
  recommendation: 'Add og:locale for international content',
@@ -82,12 +86,15 @@ export const socialRules = [
82
86
  severity: 'info',
83
87
  description: 'Multi-language sites should specify alternate locales',
84
88
  check: (ctx) => {
85
- if (!ctx.ogLocale)
86
- return null;
87
- if (!ctx.hreflangTags || ctx.hreflangTags.length <= 1)
88
- return null;
89
- if (ctx.ogLocaleAlternate === undefined)
90
- return null;
89
+ if (!ctx.ogLocale) {
90
+ return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'info', 'Not applicable (no og:locale)', { recommendation: 'This rule checks for alternate locales when og:locale is present' });
91
+ }
92
+ if (!ctx.hreflangTags || ctx.hreflangTags.length <= 1) {
93
+ return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'info', 'Not applicable (single language site)', { recommendation: 'This rule checks for og:locale:alternate on multi-language sites' });
94
+ }
95
+ if (ctx.ogLocaleAlternate === undefined) {
96
+ return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'info', 'Not applicable (og:locale:alternate property not checked)', { recommendation: 'This rule checks og:locale:alternate when data is available' });
97
+ }
91
98
  if (!ctx.ogLocaleAlternate || ctx.ogLocaleAlternate.length === 0) {
92
99
  return createResult({ id: 'social-og-locale-alternate', name: 'OG Locale Alternates', category: 'og', severity: 'info' }, 'info', 'Missing og:locale:alternate for multi-language site', {
93
100
  recommendation: 'Add og:locale:alternate for other language versions',
@@ -108,10 +115,12 @@ export const socialRules = [
108
115
  severity: 'info',
109
116
  description: 'Article pages should include article-specific Open Graph tags',
110
117
  check: (ctx) => {
111
- if (ctx.ogType !== 'article')
112
- return null;
113
- if (ctx.ogArticleTags === undefined)
114
- return null;
118
+ if (ctx.ogType !== 'article') {
119
+ return createResult({ id: 'social-og-article-tags', name: 'OG Article Tags', category: 'og', severity: 'info' }, 'info', 'Not applicable (page is not an article)', { recommendation: 'This rule checks article-specific Open Graph tags for article pages' });
120
+ }
121
+ if (ctx.ogArticleTags === undefined) {
122
+ return createResult({ id: 'social-og-article-tags', name: 'OG Article Tags', category: 'og', severity: 'info' }, 'info', 'Not applicable (article tags property not checked)', { recommendation: 'This rule checks article-specific Open Graph tags when data is available' });
123
+ }
115
124
  const missing = [];
116
125
  if (!ctx.ogArticlePublishedTime)
117
126
  missing.push('article:published_time');
@@ -137,8 +146,9 @@ export const socialRules = [
137
146
  severity: 'info',
138
147
  description: 'Consider using summary_large_image for better visibility',
139
148
  check: (ctx) => {
140
- if (!ctx.twitterCard)
141
- return null;
149
+ if (!ctx.twitterCard) {
150
+ return createResult({ id: 'social-twitter-large-image', name: 'Twitter Large Image', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (no twitter:card)', { recommendation: 'This rule checks Twitter card type when present' });
151
+ }
142
152
  if (ctx.twitterCard === 'summary') {
143
153
  return createResult({ id: 'social-twitter-large-image', name: 'Twitter Large Image', category: 'twitter', severity: 'info' }, 'info', 'Using summary card (small image)', {
144
154
  recommendation: 'Consider summary_large_image for more visual impact',
@@ -159,12 +169,15 @@ export const socialRules = [
159
169
  severity: 'info',
160
170
  description: 'Article pages should include twitter:creator for attribution',
161
171
  check: (ctx) => {
162
- if (!ctx.twitterCard)
163
- return null;
164
- if (ctx.ogType !== 'article')
165
- return null;
166
- if (ctx.twitterCreator === undefined)
167
- return null;
172
+ if (!ctx.twitterCard) {
173
+ return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (no twitter:card)', { recommendation: 'This rule checks twitter:creator when Twitter Card is present' });
174
+ }
175
+ if (ctx.ogType !== 'article') {
176
+ return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (page is not an article)', { recommendation: 'This rule checks twitter:creator for article pages' });
177
+ }
178
+ if (ctx.twitterCreator === undefined) {
179
+ return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (twitter:creator property not checked)', { recommendation: 'This rule checks twitter:creator when data is available' });
180
+ }
168
181
  if (!ctx.twitterCreator) {
169
182
  return createResult({ id: 'social-twitter-creator', name: 'Twitter Creator', category: 'twitter', severity: 'info' }, 'info', 'Missing twitter:creator on article', {
170
183
  recommendation: 'Add twitter:creator for author attribution',
@@ -184,10 +197,12 @@ export const socialRules = [
184
197
  severity: 'info',
185
198
  description: 'Twitter images should have alt text for accessibility',
186
199
  check: (ctx) => {
187
- if (!ctx.twitterImage)
188
- return null;
189
- if (ctx.twitterImageAlt === undefined)
190
- return null;
200
+ if (!ctx.twitterImage) {
201
+ return createResult({ id: 'social-twitter-image-alt', name: 'Twitter Image Alt', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (no twitter:image)', { recommendation: 'This rule checks twitter:image:alt when Twitter image is present' });
202
+ }
203
+ if (ctx.twitterImageAlt === undefined) {
204
+ return createResult({ id: 'social-twitter-image-alt', name: 'Twitter Image Alt', category: 'twitter', severity: 'info' }, 'info', 'Not applicable (twitter:image:alt property not checked)', { recommendation: 'This rule checks twitter:image:alt when data is available' });
205
+ }
191
206
  if (!ctx.twitterImageAlt) {
192
207
  return createResult({ id: 'social-twitter-image-alt', name: 'Twitter Image Alt', category: 'twitter', severity: 'info' }, 'info', 'Missing twitter:image:alt', {
193
208
  recommendation: 'Add alt text for Twitter card image',
@@ -207,10 +222,12 @@ export const socialRules = [
207
222
  severity: 'info',
208
223
  description: 'Professional content should include author information',
209
224
  check: (ctx) => {
210
- if (ctx.ogType !== 'article')
211
- return null;
212
- if (ctx.linkedinAuthor === undefined && ctx.ogArticleAuthor === undefined)
213
- return null;
225
+ if (ctx.ogType !== 'article') {
226
+ return createResult({ id: 'social-linkedin-author', name: 'LinkedIn Author', category: 'og', severity: 'info' }, 'info', 'Not applicable (page is not an article)', { recommendation: 'This rule checks author information for article pages' });
227
+ }
228
+ if (ctx.linkedinAuthor === undefined && ctx.ogArticleAuthor === undefined) {
229
+ return createResult({ id: 'social-linkedin-author', name: 'LinkedIn Author', category: 'og', severity: 'info' }, 'info', 'Not applicable (author properties not checked)', { recommendation: 'This rule checks author information when data is available' });
230
+ }
214
231
  if (!ctx.linkedinAuthor && !ctx.ogArticleAuthor) {
215
232
  return createResult({ id: 'social-linkedin-author', name: 'LinkedIn Author', category: 'og', severity: 'info' }, 'info', 'No author specified for LinkedIn', {
216
233
  recommendation: 'Add author for professional network sharing',
@@ -230,10 +247,12 @@ export const socialRules = [
230
247
  severity: 'info',
231
248
  description: 'E-commerce and recipe sites should support Pinterest Rich Pins',
232
249
  check: (ctx) => {
233
- if (!ctx.isProductPage && ctx.ogType !== 'recipe')
234
- return null;
235
- if (ctx.pinterestRichPinSupport === undefined)
236
- return null;
250
+ if (!ctx.isProductPage && ctx.ogType !== 'recipe') {
251
+ return createResult({ id: 'social-pinterest-rich-pins', name: 'Pinterest Rich Pins', category: 'og', severity: 'info' }, 'info', 'Not applicable (page is not a product or recipe)', { recommendation: 'This rule checks Pinterest Rich Pin support for e-commerce and recipe sites' });
252
+ }
253
+ if (ctx.pinterestRichPinSupport === undefined) {
254
+ return createResult({ id: 'social-pinterest-rich-pins', name: 'Pinterest Rich Pins', category: 'og', severity: 'info' }, 'info', 'Not applicable (Pinterest Rich Pin support not checked)', { recommendation: 'This rule checks for structured data when data is available' });
255
+ }
237
256
  if (!ctx.pinterestRichPinSupport) {
238
257
  return createResult({ id: 'social-pinterest-rich-pins', name: 'Pinterest Rich Pins', category: 'og', severity: 'info' }, 'info', 'No Pinterest Rich Pin support detected', {
239
258
  recommendation: 'Add structured data for Pinterest Rich Pins',
@@ -254,8 +273,9 @@ export const socialRules = [
254
273
  severity: 'info',
255
274
  description: 'Check for intentional Pinterest blocking',
256
275
  check: (ctx) => {
257
- if (ctx.hasPinterestNopin === undefined)
258
- return null;
276
+ if (ctx.hasPinterestNopin === undefined) {
277
+ return createResult({ id: 'social-pinterest-nopin', name: 'Pinterest Nopin', category: 'og', severity: 'info' }, 'info', 'Not applicable (Pinterest nopin property not checked)', { recommendation: 'This rule checks for Pinterest blocking when data is available' });
278
+ }
259
279
  if (ctx.hasPinterestNopin) {
260
280
  return createResult({ id: 'social-pinterest-nopin', name: 'Pinterest Nopin', category: 'og', severity: 'info' }, 'info', 'Pinterest pinning is disabled', {
261
281
  evidence: {
@@ -264,7 +284,7 @@ export const socialRules = [
264
284
  },
265
285
  });
266
286
  }
267
- return null;
287
+ return createResult({ id: 'social-pinterest-nopin', name: 'Pinterest Nopin', category: 'og', severity: 'info' }, 'info', 'Not applicable (Pinterest pinning is allowed)', { recommendation: 'This rule checks for intentional Pinterest blocking' });
268
288
  },
269
289
  },
270
290
  {
@@ -300,8 +320,9 @@ export const socialRules = [
300
320
  severity: 'info',
301
321
  description: 'Open Graph titles should be optimized for social platforms',
302
322
  check: (ctx) => {
303
- if (!ctx.ogTitle)
304
- return null;
323
+ if (!ctx.ogTitle) {
324
+ return createResult({ id: 'social-og-title-length', name: 'OG Title Length', category: 'og', severity: 'info' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks og:title length for social platforms when present' });
325
+ }
305
326
  const length = ctx.ogTitle.length;
306
327
  const maxLength = 60;
307
328
  if (length > maxLength) {
@@ -324,8 +345,9 @@ export const socialRules = [
324
345
  severity: 'info',
325
346
  description: 'Open Graph descriptions should be optimized for social platforms',
326
347
  check: (ctx) => {
327
- if (!ctx.ogDescription)
328
- return null;
348
+ if (!ctx.ogDescription) {
349
+ return createResult({ id: 'social-og-description-length', name: 'OG Description Length', category: 'og', severity: 'info' }, 'info', 'Not applicable (no og:description)', { recommendation: 'This rule checks og:description length for social platforms when present' });
350
+ }
329
351
  const length = ctx.ogDescription.length;
330
352
  const minLength = 55;
331
353
  const maxLength = 200;
@@ -356,8 +378,9 @@ export const socialRules = [
356
378
  severity: 'info',
357
379
  description: 'Facebook App ID enables Insights and domain verification',
358
380
  check: (ctx) => {
359
- if (ctx.fbAppId === undefined)
360
- return null;
381
+ if (ctx.fbAppId === undefined) {
382
+ return createResult({ id: 'social-fb-app-id', name: 'Facebook App ID', category: 'og', severity: 'info' }, 'info', 'Not applicable (Facebook App ID property not checked)', { recommendation: 'This rule checks for fb:app_id when data is available' });
383
+ }
361
384
  if (!ctx.fbAppId) {
362
385
  return createResult({ id: 'social-fb-app-id', name: 'Facebook App ID', category: 'og', severity: 'info' }, 'info', 'No Facebook App ID', {
363
386
  recommendation: 'Add fb:app_id for Facebook Insights',
@@ -370,4 +393,211 @@ export const socialRules = [
370
393
  return createResult({ id: 'social-fb-app-id', name: 'Facebook App ID', category: 'og', severity: 'info' }, 'pass', 'Facebook App ID present');
371
394
  },
372
395
  },
396
+ {
397
+ id: 'social-links-presence',
398
+ name: 'Social Media Links',
399
+ category: 'og',
400
+ severity: 'info',
401
+ description: 'Site should link to social media profiles',
402
+ check: (ctx) => {
403
+ if (ctx.socialLinksFound && ctx.socialLinksFound.length > 0) {
404
+ return createResult({ id: 'social-links-presence', name: 'Social Media Links', category: 'og', severity: 'info' }, 'pass', `Found ${ctx.socialLinksFound.length} social media profile link(s)`, { value: ctx.socialLinksFound.length, evidence: { found: ctx.socialLinksFound.slice(0, 5) } });
405
+ }
406
+ return createResult({ id: 'social-links-presence', name: 'Social Media Links', category: 'og', severity: 'info' }, 'info', 'No social media profile links found', { recommendation: 'Link to your Facebook, Twitter, Instagram, etc. profiles to build trust.' });
407
+ },
408
+ },
409
+ {
410
+ id: 'social-links-accessibility',
411
+ name: 'Social Links Accessibility',
412
+ category: 'accessibility',
413
+ severity: 'warning',
414
+ description: 'Social media links should have accessible labels for screen readers',
415
+ check: (ctx) => {
416
+ if (!ctx.totalSocialLinks || ctx.totalSocialLinks === 0) {
417
+ return createResult({ id: 'social-links-accessibility', name: 'Social Links Accessibility', category: 'accessibility', severity: 'warning' }, 'info', 'Not applicable (no social links found)', { recommendation: 'This rule checks accessibility of social media links when present' });
418
+ }
419
+ if (ctx.socialLinksWithoutAccessibility === undefined) {
420
+ return createResult({ id: 'social-links-accessibility', name: 'Social Links Accessibility', category: 'accessibility', severity: 'warning' }, 'info', 'Not applicable (social link accessibility not checked)', { recommendation: 'This rule checks for accessible labels on social links when data is available' });
421
+ }
422
+ if (ctx.socialLinksWithoutAccessibility > 0) {
423
+ const inaccessibleLinks = ctx.socialLinkDetails
424
+ ?.filter(l => !l.hasAccessibility)
425
+ .map(l => l.platform)
426
+ .slice(0, 5);
427
+ return createResult({ id: 'social-links-accessibility', name: 'Social Links Accessibility', category: 'accessibility', severity: 'warning' }, 'warn', `${ctx.socialLinksWithoutAccessibility} social link(s) missing accessible labels`, {
428
+ recommendation: 'Add aria-label, title, or visible text to social media links',
429
+ evidence: {
430
+ found: ctx.socialLinksWithoutAccessibility,
431
+ expected: 'All social links should have accessible labels',
432
+ impact: 'Screen reader users cannot identify the purpose of icon-only social links',
433
+ example: '<a href="https://twitter.com/..." aria-label="Follow us on Twitter">',
434
+ issue: inaccessibleLinks?.length ? `Inaccessible: ${inaccessibleLinks.join(', ')}` : undefined,
435
+ },
436
+ });
437
+ }
438
+ return createResult({ id: 'social-links-accessibility', name: 'Social Links Accessibility', category: 'accessibility', severity: 'warning' }, 'pass', 'All social links have accessible labels');
439
+ },
440
+ },
441
+ {
442
+ id: 'social-links-security',
443
+ name: 'Social Links Security',
444
+ category: 'security',
445
+ severity: 'warning',
446
+ description: 'External social links should use rel="noopener" to prevent security vulnerabilities',
447
+ check: (ctx) => {
448
+ if (!ctx.totalSocialLinks || ctx.totalSocialLinks === 0) {
449
+ return createResult({ id: 'social-links-security', name: 'Social Links Security', category: 'security', severity: 'warning' }, 'info', 'Not applicable (no social links found)', { recommendation: 'This rule checks security attributes of social media links when present' });
450
+ }
451
+ if (ctx.socialLinksWithoutNoopener === undefined) {
452
+ return createResult({ id: 'social-links-security', name: 'Social Links Security', category: 'security', severity: 'warning' }, 'info', 'Not applicable (social link security not checked)', { recommendation: 'This rule checks for rel="noopener" on social links when data is available' });
453
+ }
454
+ const linksWithNewTab = ctx.socialLinkDetails?.filter(l => l.hasNewTab) || [];
455
+ if (linksWithNewTab.length === 0) {
456
+ return createResult({ id: 'social-links-security', name: 'Social Links Security', category: 'security', severity: 'warning' }, 'info', 'Not applicable (no social links with target="_blank")', { recommendation: 'This rule checks security for social links that open in new tabs' });
457
+ }
458
+ const insecureLinks = linksWithNewTab.filter(l => !l.hasNoopener);
459
+ if (insecureLinks.length > 0) {
460
+ return createResult({ id: 'social-links-security', name: 'Social Links Security', category: 'security', severity: 'warning' }, 'warn', `${insecureLinks.length} social link(s) with target="_blank" missing rel="noopener"`, {
461
+ recommendation: 'Add rel="noopener noreferrer" to external links with target="_blank"',
462
+ evidence: {
463
+ found: insecureLinks.length,
464
+ expected: 'All target="_blank" links should have rel="noopener"',
465
+ impact: 'Without noopener, the opened page can access window.opener and potentially redirect your page (tabnabbing attack)',
466
+ example: '<a href="https://twitter.com/..." target="_blank" rel="noopener noreferrer">',
467
+ issue: insecureLinks.slice(0, 3).map(l => l.platform).join(', '),
468
+ },
469
+ });
470
+ }
471
+ return createResult({ id: 'social-links-security', name: 'Social Links Security', category: 'security', severity: 'warning' }, 'pass', 'All social links opening in new tabs have proper security attributes');
472
+ },
473
+ },
474
+ {
475
+ id: 'social-links-new-tab',
476
+ name: 'Social Links New Tab',
477
+ category: 'og',
478
+ severity: 'info',
479
+ description: 'Social media links should open in a new tab to keep users on your site',
480
+ check: (ctx) => {
481
+ if (!ctx.totalSocialLinks || ctx.totalSocialLinks === 0) {
482
+ return createResult({ id: 'social-links-new-tab', name: 'Social Links New Tab', category: 'og', severity: 'info' }, 'info', 'Not applicable (no social links found)', { recommendation: 'This rule checks if social links open in new tabs when present' });
483
+ }
484
+ if (ctx.socialLinksWithoutNewTab === undefined) {
485
+ return createResult({ id: 'social-links-new-tab', name: 'Social Links New Tab', category: 'og', severity: 'info' }, 'info', 'Not applicable (social link new tab property not checked)', { recommendation: 'This rule checks target="_blank" on social links when data is available' });
486
+ }
487
+ if (ctx.socialLinksWithoutNewTab > 0) {
488
+ const linksWithoutNewTab = ctx.socialLinkDetails
489
+ ?.filter(l => !l.hasNewTab)
490
+ .map(l => l.platform)
491
+ .slice(0, 5);
492
+ return createResult({ id: 'social-links-new-tab', name: 'Social Links New Tab', category: 'og', severity: 'info' }, 'info', `${ctx.socialLinksWithoutNewTab} social link(s) don't open in new tab`, {
493
+ recommendation: 'Consider using target="_blank" for social links to keep users on your site',
494
+ evidence: {
495
+ found: ctx.socialLinksWithoutNewTab,
496
+ expected: 'Social links typically open in new tabs',
497
+ impact: 'Users leaving your site may not return; opening in new tab preserves their session',
498
+ example: '<a href="https://twitter.com/..." target="_blank" rel="noopener noreferrer">',
499
+ issue: linksWithoutNewTab?.length ? `Without new tab: ${linksWithoutNewTab.join(', ')}` : undefined,
500
+ },
501
+ });
502
+ }
503
+ return createResult({ id: 'social-links-new-tab', name: 'Social Links New Tab', category: 'og', severity: 'info' }, 'pass', 'All social links open in new tabs');
504
+ },
505
+ },
506
+ {
507
+ id: 'social-links-placement',
508
+ name: 'Social Links Placement',
509
+ category: 'og',
510
+ severity: 'info',
511
+ description: 'Social links should be placed in header or footer for easy discovery',
512
+ check: (ctx) => {
513
+ if (!ctx.totalSocialLinks || ctx.totalSocialLinks === 0) {
514
+ return createResult({ id: 'social-links-placement', name: 'Social Links Placement', category: 'og', severity: 'info' }, 'info', 'Not applicable (no social links found)', { recommendation: 'This rule checks social link placement when present' });
515
+ }
516
+ if (ctx.socialLinksInHeader === undefined && ctx.socialLinksInFooter === undefined) {
517
+ return createResult({ id: 'social-links-placement', name: 'Social Links Placement', category: 'og', severity: 'info' }, 'info', 'Not applicable (social link placement not checked)', { recommendation: 'This rule checks social link placement when data is available' });
518
+ }
519
+ const inHeaderOrFooter = (ctx.socialLinksInHeader || 0) + (ctx.socialLinksInFooter || 0);
520
+ if (inHeaderOrFooter === 0) {
521
+ return createResult({ id: 'social-links-placement', name: 'Social Links Placement', category: 'og', severity: 'info' }, 'info', 'Social links not found in header or footer', {
522
+ recommendation: 'Place social media links in header or footer for consistent visibility',
523
+ evidence: {
524
+ found: 'Social links only in body content',
525
+ expected: 'Social links in header and/or footer',
526
+ impact: 'Users expect to find social links in standard locations; hidden links reduce engagement',
527
+ },
528
+ });
529
+ }
530
+ const locations = [];
531
+ if (ctx.socialLinksInHeader && ctx.socialLinksInHeader > 0)
532
+ locations.push(`header (${ctx.socialLinksInHeader})`);
533
+ if (ctx.socialLinksInFooter && ctx.socialLinksInFooter > 0)
534
+ locations.push(`footer (${ctx.socialLinksInFooter})`);
535
+ return createResult({ id: 'social-links-placement', name: 'Social Links Placement', category: 'og', severity: 'info' }, 'pass', `Social links found in ${locations.join(' and ')}`);
536
+ },
537
+ },
538
+ {
539
+ id: 'social-links-diversity',
540
+ name: 'Social Platform Diversity',
541
+ category: 'og',
542
+ severity: 'info',
543
+ description: 'Consider linking to multiple social platforms for broader reach',
544
+ check: (ctx) => {
545
+ if (!ctx.platformsFound || ctx.platformsFound.length === 0) {
546
+ return createResult({ id: 'social-links-diversity', name: 'Social Platform Diversity', category: 'og', severity: 'info' }, 'info', 'Not applicable (no social platforms found)', { recommendation: 'This rule checks social platform diversity when social links are present' });
547
+ }
548
+ const majorPlatforms = ['facebook', 'twitter', 'instagram', 'linkedin', 'youtube'];
549
+ const foundMajor = ctx.platformsFound.filter(p => majorPlatforms.includes(p));
550
+ if (ctx.platformsFound.length === 1) {
551
+ return createResult({ id: 'social-links-diversity', name: 'Social Platform Diversity', category: 'og', severity: 'info' }, 'info', `Only 1 social platform linked: ${ctx.platformsFound[0]}`, {
552
+ recommendation: 'Consider linking to additional relevant social platforms',
553
+ evidence: {
554
+ found: ctx.platformsFound,
555
+ expected: 'Multiple social platforms for broader audience reach',
556
+ impact: 'Different audiences prefer different platforms; diversification increases reach',
557
+ },
558
+ });
559
+ }
560
+ if (foundMajor.length >= 2) {
561
+ return createResult({ id: 'social-links-diversity', name: 'Social Platform Diversity', category: 'og', severity: 'info' }, 'pass', `Found ${ctx.platformsFound.length} social platform(s): ${ctx.platformsFound.slice(0, 5).join(', ')}`, { evidence: { found: ctx.platformsFound } });
562
+ }
563
+ return createResult({ id: 'social-links-diversity', name: 'Social Platform Diversity', category: 'og', severity: 'info' }, 'info', `${ctx.platformsFound.length} social platform(s) linked`, {
564
+ evidence: { found: ctx.platformsFound },
565
+ recommendation: 'Consider adding major platforms like Facebook, Twitter/X, LinkedIn, or Instagram',
566
+ });
567
+ },
568
+ },
569
+ {
570
+ id: 'social-profile-consistency',
571
+ name: 'Social Profile Consistency',
572
+ category: 'og',
573
+ severity: 'info',
574
+ description: 'Social meta tags should be consistent with actual social profile links',
575
+ check: (ctx) => {
576
+ if (!ctx.twitterSite && !ctx.platformsFound?.includes('twitter')) {
577
+ return createResult({ id: 'social-profile-consistency', name: 'Social Profile Consistency', category: 'og', severity: 'info' }, 'info', 'Not applicable (no Twitter meta or links)', { recommendation: 'This rule checks consistency between social meta tags and profile links' });
578
+ }
579
+ const hasTwitterMeta = !!ctx.twitterSite;
580
+ const hasTwitterLink = ctx.platformsFound?.includes('twitter');
581
+ if (hasTwitterMeta && !hasTwitterLink) {
582
+ return createResult({ id: 'social-profile-consistency', name: 'Social Profile Consistency', category: 'og', severity: 'info' }, 'info', 'twitter:site is set but no Twitter profile link found', {
583
+ recommendation: 'Add a link to your Twitter profile for consistency',
584
+ evidence: {
585
+ found: `twitter:site: ${ctx.twitterSite}`,
586
+ expected: 'Matching Twitter profile link on page',
587
+ impact: 'Users may want to follow your Twitter account directly from your site',
588
+ },
589
+ });
590
+ }
591
+ if (!hasTwitterMeta && hasTwitterLink) {
592
+ return createResult({ id: 'social-profile-consistency', name: 'Social Profile Consistency', category: 'og', severity: 'info' }, 'info', 'Twitter profile linked but twitter:site meta tag is missing', {
593
+ recommendation: 'Add twitter:site meta tag for proper Twitter Card attribution',
594
+ evidence: {
595
+ expected: '<meta name="twitter:site" content="@yourusername">',
596
+ impact: 'Twitter Cards will not show your @username when shared',
597
+ },
598
+ });
599
+ }
600
+ return createResult({ id: 'social-profile-consistency', name: 'Social Profile Consistency', category: 'og', severity: 'info' }, 'pass', 'Social meta tags are consistent with profile links');
601
+ },
602
+ },
373
603
  ];
@@ -8,13 +8,31 @@ export const structuralRules = [
8
8
  severity: 'error',
9
9
  description: 'Page must have exactly one H1 heading',
10
10
  check: (ctx) => {
11
- if (ctx.h1Count === undefined)
12
- return null;
11
+ if (ctx.h1Count === undefined) {
12
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'info', 'Unable to check H1 (data unavailable)', { recommendation: 'Ensure HTML content is properly parsed' });
13
+ }
13
14
  if (ctx.h1Count === 0) {
14
- return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'fail', 'Missing H1 heading', { recommendation: 'Add a single H1 heading that describes the page content' });
15
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'fail', 'Missing H1 heading', {
16
+ recommendation: 'Add a single H1 heading. It should be the main headline and include your primary keyword.',
17
+ evidence: {
18
+ found: 'No H1 tags detected on the page',
19
+ expected: 'Exactly one <h1> tag',
20
+ impact: 'The H1 tag is the most important heading for SEO. It tells search engines and users what the main topic of the page is. Pages without an H1 have poor semantic structure and may rank lower.',
21
+ example: '<h1>Your Main Page Title Here</h1>'
22
+ }
23
+ });
15
24
  }
16
25
  if (ctx.h1Count > 1) {
17
- return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'warn', `Multiple H1 tags (${ctx.h1Count} found)`, { value: ctx.h1Count, recommendation: 'Use only one H1 per page' });
26
+ return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'warn', `Multiple H1 tags (${ctx.h1Count} found)`, {
27
+ value: ctx.h1Count,
28
+ recommendation: 'Use only one H1 per page to clearly signal the main topic.',
29
+ evidence: {
30
+ found: `${ctx.h1Count} H1 tags on the page`,
31
+ expected: 'Exactly one <h1> tag',
32
+ impact: 'Multiple H1 tags dilute the main topic signal and confuse search engines about which heading is the primary page title. This can hurt topical relevance and rankings.',
33
+ example: '<h1>Main Title</h1>\n<!-- Use H2 for subtopics instead -->\n<h2>Subtopic One</h2>\n<h2>Subtopic Two</h2>'
34
+ }
35
+ });
18
36
  }
19
37
  return createResult({ id: 'h1-exists', name: 'H1 Tag', category: 'headings', severity: 'error' }, 'pass', 'Single H1 tag present');
20
38
  },
@@ -26,15 +44,34 @@ export const structuralRules = [
26
44
  severity: 'warning',
27
45
  description: 'H1 should be 20-70 characters',
28
46
  check: (ctx) => {
29
- if (!ctx.h1Text)
30
- return null;
47
+ if (!ctx.h1Text) {
48
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'info', 'No H1 text to analyze', { recommendation: 'First add an H1 heading to the page' });
49
+ }
31
50
  const len = ctx.h1Length ?? ctx.h1Text.length;
32
51
  const { minLength, maxLength } = SEO_THRESHOLDS.headings.h1;
33
52
  if (len < minLength) {
34
- return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too short (${len} chars, min: ${minLength})`, { value: len, recommendation: `Expand H1 to at least ${minLength} characters` });
53
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too short (${len} chars, min: ${minLength})`, {
54
+ value: len,
55
+ recommendation: `Expand H1 to at least ${minLength} characters`,
56
+ evidence: {
57
+ found: `H1 with ${len} characters: "${ctx.h1Text}"`,
58
+ expected: `${minLength}-${maxLength} characters`,
59
+ impact: 'Very short H1 tags may not provide enough context for search engines and users to understand the page topic. Descriptive H1s improve relevance signals and click-through rates.',
60
+ example: '<h1>Complete Guide to SEO Best Practices</h1>'
61
+ }
62
+ });
35
63
  }
36
64
  if (len > maxLength) {
37
- return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too long (${len} chars, max: ${maxLength})`, { value: len, recommendation: `Shorten H1 to under ${maxLength} characters` });
65
+ return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'warn', `H1 too long (${len} chars, max: ${maxLength})`, {
66
+ value: len,
67
+ recommendation: `Shorten H1 to under ${maxLength} characters`,
68
+ evidence: {
69
+ found: `H1 with ${len} characters: "${ctx.h1Text}"`,
70
+ expected: `${minLength}-${maxLength} characters`,
71
+ impact: 'Overly long H1 tags can be truncated in search results and may appear unfocused. Concise H1s are more scannable and effective for both users and search engines.',
72
+ example: '<h1>SEO Guide: On-Page Optimization Tips</h1>'
73
+ }
74
+ });
38
75
  }
39
76
  return createResult({ id: 'h1-length', name: 'H1 Length', category: 'headings', severity: 'warning' }, 'pass', `H1 length OK (${len} chars)`, { value: len });
40
77
  },
@@ -46,11 +83,20 @@ export const structuralRules = [
46
83
  severity: 'warning',
47
84
  description: 'Headings should follow sequential order (H1 → H2 → H3)',
48
85
  check: (ctx) => {
49
- if (ctx.headingHierarchyValid === undefined)
50
- return null;
86
+ if (ctx.headingHierarchyValid === undefined) {
87
+ return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'info', 'Unable to check heading hierarchy (data unavailable)', { recommendation: 'Ensure heading analysis completed' });
88
+ }
51
89
  if (!ctx.headingHierarchyValid) {
52
90
  const skipped = ctx.headingSkippedLevels?.join(', ') || 'unknown';
53
- return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'warn', `Heading levels are skipped (${skipped})`, { recommendation: 'Use sequential heading levels (H1 → H2 → H3)' });
91
+ return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'warn', `Heading levels are skipped (${skipped})`, {
92
+ recommendation: 'Use sequential heading levels (H1 → H2 → H3)',
93
+ evidence: {
94
+ found: `Skipped heading levels: ${skipped}`,
95
+ expected: 'Sequential heading structure (H1 → H2 → H3 → H4, etc.)',
96
+ impact: 'Skipping heading levels disrupts the document outline and can confuse screen readers and search engine crawlers trying to understand content hierarchy. This weakens accessibility and semantic structure.',
97
+ example: '<h1>Main Title</h1>\n<h2>Section Title</h2>\n<h3>Subsection Title</h3>\n<!-- Do NOT skip from H1 to H3 -->'
98
+ }
99
+ });
54
100
  }
55
101
  return createResult({ id: 'heading-hierarchy', name: 'Heading Hierarchy', category: 'headings', severity: 'warning' }, 'pass', 'Heading structure is correct');
56
102
  },
@@ -62,8 +108,9 @@ export const structuralRules = [
62
108
  severity: 'info',
63
109
  description: 'Page should have 2-8 H2 headings for good structure',
64
110
  check: (ctx) => {
65
- if (ctx.h2Count === undefined)
66
- return null;
111
+ if (ctx.h2Count === undefined) {
112
+ return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', 'Unable to check H2 count (data unavailable)', { recommendation: 'Ensure heading analysis completed' });
113
+ }
67
114
  const { min, max } = SEO_THRESHOLDS.headings.h2;
68
115
  if (ctx.h2Count < min) {
69
116
  return createResult({ id: 'h2-count', name: 'H2 Count', category: 'headings', severity: 'info' }, 'info', `Few H2 headings (${ctx.h2Count})`, { value: ctx.h2Count, recommendation: `Consider adding more H2 headings for better structure (${min}-${max} ideal)` });
@@ -84,7 +131,7 @@ export const structuralRules = [
84
131
  if (!ctx.hasHeader) {
85
132
  return createResult({ id: 'html5-header-exists', name: 'HTML5 Header', category: 'technical', severity: 'info' }, 'info', 'Missing <header> element', { recommendation: 'Use <header> for site-wide or page-specific introductory content' });
86
133
  }
87
- return null;
134
+ return createResult({ id: 'html5-header-exists', name: 'HTML5 Header', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <header> element present');
88
135
  },
89
136
  },
90
137
  {
@@ -97,7 +144,7 @@ export const structuralRules = [
97
144
  if (!ctx.hasNav) {
98
145
  return createResult({ id: 'html5-nav-exists', name: 'HTML5 Navigation', category: 'technical', severity: 'info' }, 'info', 'Missing <nav> element', { recommendation: 'Use <nav> to define a block of navigation links' });
99
146
  }
100
- return null;
147
+ return createResult({ id: 'html5-nav-exists', name: 'HTML5 Navigation', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <nav> element present');
101
148
  },
102
149
  },
103
150
  {
@@ -110,7 +157,7 @@ export const structuralRules = [
110
157
  if (!ctx.hasMain) {
111
158
  return createResult({ id: 'html5-main-exists', name: 'HTML5 Main Content', category: 'technical', severity: 'info' }, 'info', 'Missing <main> element', { recommendation: 'Use <main> to enclose the dominant content of the <body>' });
112
159
  }
113
- return null;
160
+ return createResult({ id: 'html5-main-exists', name: 'HTML5 Main Content', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <main> element present');
114
161
  },
115
162
  },
116
163
  {
@@ -123,7 +170,7 @@ export const structuralRules = [
123
170
  if (!ctx.hasArticle && ctx.wordCount && ctx.wordCount > 500) {
124
171
  return createResult({ id: 'html5-article-exists', name: 'HTML5 Article', category: 'technical', severity: 'info' }, 'info', 'Missing <article> element for substantial content', { recommendation: 'Consider using <article> for blog posts, news articles, etc.' });
125
172
  }
126
- return null;
173
+ return createResult({ id: 'html5-article-exists', name: 'HTML5 Article', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <article> element present or not required');
127
174
  },
128
175
  },
129
176
  {
@@ -136,7 +183,7 @@ export const structuralRules = [
136
183
  if (!ctx.hasSection && (ctx.h2Count && ctx.h2Count > 1)) {
137
184
  return createResult({ id: 'html5-section-exists', name: 'HTML5 Section', category: 'technical', severity: 'info' }, 'info', 'Missing <section> element for content grouping', { recommendation: 'Consider using <section> to group related content, usually with a heading' });
138
185
  }
139
- return null;
186
+ return createResult({ id: 'html5-section-exists', name: 'HTML5 Section', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <section> element present or not required');
140
187
  },
141
188
  },
142
189
  {
@@ -149,7 +196,28 @@ export const structuralRules = [
149
196
  if (!ctx.hasFooter) {
150
197
  return createResult({ id: 'html5-footer-exists', name: 'HTML5 Footer', category: 'technical', severity: 'info' }, 'info', 'Missing <footer> element', { recommendation: 'Use <footer> for site-wide or page-specific footer content' });
151
198
  }
152
- return null;
199
+ return createResult({ id: 'html5-footer-exists', name: 'HTML5 Footer', category: 'technical', severity: 'info' }, 'pass', 'HTML5 <footer> element present');
200
+ },
201
+ },
202
+ {
203
+ id: 'keywords-in-h1',
204
+ name: 'Keywords in H1',
205
+ category: 'headings',
206
+ severity: 'warning',
207
+ description: 'H1 should contain main keywords',
208
+ check: (ctx) => {
209
+ if (ctx.keywordsInH1 === false && ctx.topKeywords && ctx.topKeywords.length > 0) {
210
+ return createResult({ id: 'keywords-in-h1', name: 'Keywords in H1', category: 'headings', severity: 'warning' }, 'warn', 'H1 does not appear to contain top keywords', {
211
+ recommendation: 'Ensure your H1 tag includes your main target keyword naturally.',
212
+ evidence: {
213
+ found: `H1: "${ctx.h1Text}"`,
214
+ expected: `Should contain one of: ${ctx.topKeywords.join(', ')}`,
215
+ impact: 'Keywords in H1 confirm relevance to the user and search engines. The H1 should naturally include your primary target keyword to reinforce the page topic and improve rankings for that term.',
216
+ example: `<h1>Complete Guide to ${ctx.topKeywords?.[0] || 'Your Primary Keyword'}</h1>`
217
+ }
218
+ });
219
+ }
220
+ return createResult({ id: 'keywords-in-h1', name: 'Keywords in H1', category: 'headings', severity: 'warning' }, 'pass', 'Keywords present in H1');
153
221
  },
154
222
  },
155
223
  ];