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
@@ -14,7 +14,7 @@ export const performanceRules = [
14
14
  if (ctx.preconnectCount && ctx.preconnectCount > 0) {
15
15
  return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preconnectCount} preconnect hint(s) found`, { value: ctx.preconnectCount });
16
16
  }
17
- return null;
17
+ return createResult({ id: 'perf-preconnect', name: 'Preconnect Hints', category: 'performance', severity: 'info' }, 'info', 'Not applicable (no external links or preconnect data unavailable)', { recommendation: 'This rule checks for preconnect hints when external resources are detected' });
18
18
  },
19
19
  },
20
20
  {
@@ -27,7 +27,7 @@ export const performanceRules = [
27
27
  if (ctx.dnsPrefetchCount && ctx.dnsPrefetchCount > 0) {
28
28
  return createResult({ id: 'perf-dns-prefetch', name: 'DNS Prefetch', category: 'performance', severity: 'info' }, 'pass', `${ctx.dnsPrefetchCount} dns-prefetch hint(s) found`, { value: ctx.dnsPrefetchCount });
29
29
  }
30
- return null;
30
+ return createResult({ id: 'perf-dns-prefetch', name: 'DNS Prefetch', category: 'performance', severity: 'info' }, 'pass', 'No dns-prefetch hints (not required if no external resources)');
31
31
  },
32
32
  },
33
33
  {
@@ -40,7 +40,7 @@ export const performanceRules = [
40
40
  if (ctx.preloadCount && ctx.preloadCount > 0) {
41
41
  return createResult({ id: 'perf-preload', name: 'Preload Hints', category: 'performance', severity: 'info' }, 'pass', `${ctx.preloadCount} preload hint(s) found`, { value: ctx.preloadCount });
42
42
  }
43
- return null;
43
+ return createResult({ id: 'perf-preload', name: 'Preload Hints', category: 'performance', severity: 'info' }, 'pass', 'No preload hints (consider adding for critical resources)');
44
44
  },
45
45
  },
46
46
  {
@@ -52,12 +52,22 @@ export const performanceRules = [
52
52
  check: (ctx) => {
53
53
  const blocking = ctx.renderBlockingResources ?? 0;
54
54
  if (blocking > 5) {
55
- return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'warn', `${blocking} render-blocking resources in <head>`, { value: blocking, recommendation: 'Use async/defer for scripts, preload for critical CSS' });
55
+ return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'warn', `${blocking} render-blocking resources in <head>`, {
56
+ value: blocking,
57
+ recommendation: 'Use async/defer for scripts, preload for critical CSS',
58
+ evidence: {
59
+ found: `${blocking} render-blocking resources`,
60
+ expected: '≤ 5 render-blocking resources',
61
+ impact: 'Render-blocking resources delay First Contentful Paint (FCP) and Largest Contentful Paint (LCP)',
62
+ example: '<script src="app.js" defer></script> or <link rel="preload" as="style" href="critical.css">',
63
+ learnMore: 'https://web.dev/render-blocking-resources/'
64
+ }
65
+ });
56
66
  }
57
67
  if (blocking > 0) {
58
68
  return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'info', `${blocking} render-blocking resource(s) in <head>`, { value: blocking });
59
69
  }
60
- return null;
70
+ return createResult({ id: 'perf-render-blocking', name: 'Render Blocking', category: 'performance', severity: 'warning' }, 'pass', 'No render-blocking resources detected in <head>');
61
71
  },
62
72
  },
63
73
  {
@@ -71,7 +81,7 @@ export const performanceRules = [
71
81
  if (inline > 10) {
72
82
  return createResult({ id: 'perf-inline-styles', name: 'Inline Styles', category: 'performance', severity: 'info' }, 'info', `${inline} inline style blocks found`, { value: inline, recommendation: 'Consider consolidating inline styles into external CSS' });
73
83
  }
74
- return null;
84
+ return createResult({ id: 'perf-inline-styles', name: 'Inline Styles', category: 'performance', severity: 'info' }, 'pass', `Inline styles count is acceptable (${inline} blocks)`);
75
85
  },
76
86
  },
77
87
  {
@@ -82,9 +92,18 @@ export const performanceRules = [
82
92
  description: 'Above-the-fold images should not use lazy loading',
83
93
  check: (ctx) => {
84
94
  if (ctx.lcpHints?.hasLazyLcp) {
85
- return createResult({ id: 'cwv-lcp-lazy', name: 'LCP Image Lazy', category: 'performance', severity: 'warning' }, 'warn', 'First large image uses lazy loading (may hurt LCP)', { recommendation: 'Remove loading="lazy" from above-the-fold images' });
95
+ return createResult({ id: 'cwv-lcp-lazy', name: 'LCP Image Lazy', category: 'performance', severity: 'warning' }, 'warn', 'First large image uses lazy loading (may hurt LCP)', {
96
+ recommendation: 'Remove loading="lazy" from above-the-fold images',
97
+ evidence: {
98
+ found: 'Above-the-fold image with loading="lazy"',
99
+ expected: 'LCP images should not use lazy loading',
100
+ impact: 'Lazy loading delays the Largest Contentful Paint (LCP), hurting Core Web Vitals',
101
+ example: '<img src="hero.jpg" alt="Hero"> <!-- Remove loading="lazy" from hero images -->',
102
+ learnMore: 'https://web.dev/lcp-lazy-loading/'
103
+ }
104
+ });
86
105
  }
87
- return null;
106
+ return createResult({ id: 'cwv-lcp-lazy', name: 'LCP Image Lazy', category: 'performance', severity: 'warning' }, 'pass', 'No lazy loading detected on LCP images');
88
107
  },
89
108
  },
90
109
  {
@@ -97,7 +116,7 @@ export const performanceRules = [
97
116
  if (ctx.lcpHints?.hasLargeImages && !ctx.lcpHints?.hasPriorityHints) {
98
117
  return createResult({ id: 'cwv-lcp-priority', name: 'LCP Priority Hints', category: 'performance', severity: 'info' }, 'info', 'No fetchpriority="high" found on large images', { recommendation: 'Add fetchpriority="high" to LCP candidate images' });
99
118
  }
100
- return null;
119
+ return createResult({ id: 'cwv-lcp-priority', name: 'LCP Priority Hints', category: 'performance', severity: 'info' }, 'pass', 'LCP images have proper priority hints or no large images detected');
101
120
  },
102
121
  },
103
122
  {
@@ -109,9 +128,19 @@ export const performanceRules = [
109
128
  check: (ctx) => {
110
129
  const missing = ctx.clsHints?.imagesWithoutDimensions ?? ctx.imagesMissingDimensions ?? 0;
111
130
  if (missing > 0) {
112
- return createResult({ id: 'cwv-cls-images', name: 'CLS Image Dimensions', category: 'performance', severity: 'warning' }, 'warn', `${missing} image(s) without width/height (causes CLS)`, { value: missing, recommendation: 'Add width and height attributes to all images' });
131
+ return createResult({ id: 'cwv-cls-images', name: 'CLS Image Dimensions', category: 'performance', severity: 'warning' }, 'warn', `${missing} image(s) without width/height (causes CLS)`, {
132
+ value: missing,
133
+ recommendation: 'Add width and height attributes to all images',
134
+ evidence: {
135
+ found: `${missing} images without dimensions`,
136
+ expected: 'All images should have width and height attributes',
137
+ impact: 'Images without dimensions cause layout shifts (CLS) when they load, hurting Core Web Vitals',
138
+ example: '<img src="photo.jpg" alt="Photo" width="800" height="600">',
139
+ learnMore: 'https://web.dev/optimize-cls/'
140
+ }
141
+ });
113
142
  }
114
- return null;
143
+ return createResult({ id: 'cwv-cls-images', name: 'CLS Image Dimensions', category: 'performance', severity: 'warning' }, 'pass', 'All images have proper dimensions');
115
144
  },
116
145
  },
117
146
  {
@@ -122,8 +151,9 @@ export const performanceRules = [
122
151
  description: 'TTFB should be under 600ms (ideally under 200ms)',
123
152
  check: (ctx) => {
124
153
  const ttfb = ctx.timings?.ttfb;
125
- if (ttfb === undefined)
126
- return null;
154
+ if (ttfb === undefined) {
155
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'info', 'Not applicable (TTFB timing data unavailable)', { recommendation: 'This rule checks Time to First Byte when timing data is available' });
156
+ }
127
157
  const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.ttfb;
128
158
  if (ttfb <= good) {
129
159
  return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'pass', `Excellent TTFB (${ttfb}ms)`, { value: ttfb });
@@ -132,9 +162,27 @@ export const performanceRules = [
132
162
  return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'info', `TTFB needs improvement (${ttfb}ms)`, { value: ttfb, recommendation: `Optimize server response time to under ${good}ms` });
133
163
  }
134
164
  if (ttfb <= poor) {
135
- return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'warn', `Slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Optimize server, use CDN, enable caching' });
165
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'warn', `Slow TTFB (${ttfb}ms)`, {
166
+ value: ttfb,
167
+ recommendation: 'Optimize server, use CDN, enable caching',
168
+ evidence: {
169
+ found: `${ttfb}ms TTFB`,
170
+ expected: `≤ ${needsImprovement}ms`,
171
+ impact: 'Slow server response delays page rendering and all subsequent resources',
172
+ learnMore: 'https://web.dev/ttfb/'
173
+ }
174
+ });
136
175
  }
137
- return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'fail', `Very slow TTFB (${ttfb}ms)`, { value: ttfb, recommendation: 'Critical: Server is too slow. Check server, database, and network' });
176
+ return createResult({ id: 'timing-ttfb', name: 'TTFB', category: 'performance', severity: 'error' }, 'fail', `Very slow TTFB (${ttfb}ms)`, {
177
+ value: ttfb,
178
+ recommendation: 'Critical: Server is too slow. Check server, database, and network',
179
+ evidence: {
180
+ found: `${ttfb}ms TTFB`,
181
+ expected: `≤ ${poor}ms (ideally ≤ ${good}ms)`,
182
+ impact: 'Critical server performance issue. Users will experience very slow page loads',
183
+ learnMore: 'https://web.dev/ttfb/'
184
+ }
185
+ });
138
186
  },
139
187
  },
140
188
  {
@@ -145,8 +193,9 @@ export const performanceRules = [
145
193
  description: 'Total page load should be under 2.5s',
146
194
  check: (ctx) => {
147
195
  const total = ctx.timings?.total;
148
- if (total === undefined)
149
- return null;
196
+ if (total === undefined) {
197
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (total load time data unavailable)', { recommendation: 'This rule checks total page load time when timing data is available' });
198
+ }
150
199
  const { good, needsImprovement, poor } = SEO_THRESHOLDS.timing.total;
151
200
  if (total <= good) {
152
201
  return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'pass', `Fast page load (${total}ms)`, { value: total });
@@ -155,9 +204,27 @@ export const performanceRules = [
155
204
  return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'info', `Page load time acceptable (${total}ms)`, { value: total, recommendation: `Aim for under ${good}ms for better user experience` });
156
205
  }
157
206
  if (total <= poor) {
158
- return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'warn', `Slow page load (${total}ms)`, { value: total, recommendation: 'Optimize assets, enable compression, use CDN' });
207
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'warn', `Slow page load (${total}ms)`, {
208
+ value: total,
209
+ recommendation: 'Optimize assets, enable compression, use CDN',
210
+ evidence: {
211
+ found: `${total}ms total load time`,
212
+ expected: `≤ ${needsImprovement}ms`,
213
+ impact: 'Slow page loads increase bounce rate and reduce user engagement',
214
+ learnMore: 'https://web.dev/performance-scoring/'
215
+ }
216
+ });
159
217
  }
160
- return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'fail', `Very slow page load (${total}ms)`, { value: total, recommendation: 'Critical performance issue. Full optimization needed.' });
218
+ return createResult({ id: 'timing-total', name: 'Load Time', category: 'performance', severity: 'warning' }, 'fail', `Very slow page load (${total}ms)`, {
219
+ value: total,
220
+ recommendation: 'Critical performance issue. Full optimization needed.',
221
+ evidence: {
222
+ found: `${total}ms total load time`,
223
+ expected: `≤ ${poor}ms (ideally ≤ ${good}ms)`,
224
+ impact: 'Critical performance issue. Very slow load times severely hurt SEO rankings and user experience',
225
+ learnMore: 'https://web.dev/performance-scoring/'
226
+ }
227
+ });
161
228
  },
162
229
  },
163
230
  {
@@ -168,8 +235,9 @@ export const performanceRules = [
168
235
  description: 'DNS lookup should be under 50ms',
169
236
  check: (ctx) => {
170
237
  const dns = ctx.timings?.dnsLookup;
171
- if (dns === undefined)
172
- return null;
238
+ if (dns === undefined) {
239
+ return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'info', 'Not applicable (DNS lookup timing data unavailable)', { recommendation: 'This rule checks DNS lookup time when timing data is available' });
240
+ }
173
241
  const { good, poor } = SEO_THRESHOLDS.timing.dnsLookup;
174
242
  if (dns <= good) {
175
243
  return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'pass', `Fast DNS lookup (${dns}ms)`, { value: dns });
@@ -177,7 +245,17 @@ export const performanceRules = [
177
245
  if (dns <= poor) {
178
246
  return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'info', `DNS lookup could be faster (${dns}ms)`, { value: dns, recommendation: 'Consider using faster DNS provider or dns-prefetch' });
179
247
  }
180
- return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'warn', `Slow DNS lookup (${dns}ms)`, { value: dns, recommendation: 'DNS is slow. Consider Cloudflare, Google DNS, or dns-prefetch' });
248
+ return createResult({ id: 'timing-dns', name: 'DNS Lookup', category: 'performance', severity: 'info' }, 'warn', `Slow DNS lookup (${dns}ms)`, {
249
+ value: dns,
250
+ recommendation: 'DNS is slow. Consider Cloudflare, Google DNS, or dns-prefetch',
251
+ evidence: {
252
+ found: `${dns}ms DNS lookup`,
253
+ expected: `≤ ${poor}ms (ideally ≤ ${good}ms)`,
254
+ impact: 'Slow DNS resolution delays connection establishment and increases page load time',
255
+ example: '<link rel="dns-prefetch" href="//api.example.com">',
256
+ learnMore: 'https://web.dev/dns-prefetch/'
257
+ }
258
+ });
181
259
  },
182
260
  },
183
261
  {
@@ -188,8 +266,9 @@ export const performanceRules = [
188
266
  description: 'TLS handshake should be under 100ms',
189
267
  check: (ctx) => {
190
268
  const tls = ctx.timings?.tlsHandshake;
191
- if (tls === undefined)
192
- return null;
269
+ if (tls === undefined) {
270
+ return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'info', 'Not applicable (TLS handshake timing data unavailable)', { recommendation: 'This rule checks TLS handshake time when timing data is available' });
271
+ }
193
272
  const { good, poor } = SEO_THRESHOLDS.timing.tlsHandshake;
194
273
  if (tls <= good) {
195
274
  return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'pass', `Fast TLS handshake (${tls}ms)`, { value: tls });
@@ -197,7 +276,16 @@ export const performanceRules = [
197
276
  if (tls <= poor) {
198
277
  return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'info', `TLS handshake could be faster (${tls}ms)`, { value: tls, recommendation: 'Consider TLS 1.3, HTTP/2, or preconnect hints' });
199
278
  }
200
- return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'warn', `Slow TLS handshake (${tls}ms)`, { value: tls, recommendation: 'TLS is slow. Check server configuration and certificate chain' });
279
+ return createResult({ id: 'timing-tls', name: 'TLS Handshake', category: 'performance', severity: 'info' }, 'warn', `Slow TLS handshake (${tls}ms)`, {
280
+ value: tls,
281
+ recommendation: 'TLS is slow. Check server configuration and certificate chain',
282
+ evidence: {
283
+ found: `${tls}ms TLS handshake`,
284
+ expected: `≤ ${poor}ms (ideally ≤ ${good}ms)`,
285
+ impact: 'Slow TLS handshake delays secure connection establishment, especially on mobile networks',
286
+ learnMore: 'https://web.dev/uses-http2/'
287
+ }
288
+ });
201
289
  },
202
290
  },
203
291
  {
@@ -208,8 +296,9 @@ export const performanceRules = [
208
296
  description: 'HTML should be under 500KB (ideally under 100KB)',
209
297
  check: (ctx) => {
210
298
  const size = ctx.htmlSize;
211
- if (size === undefined)
212
- return null;
299
+ if (size === undefined) {
300
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (HTML size data unavailable)', { recommendation: 'This rule checks HTML size when response size data is available' });
301
+ }
213
302
  const { good, warning, poor } = SEO_THRESHOLDS.responseSize.html;
214
303
  const sizeKb = Math.round(size / 1024);
215
304
  if (size <= good) {
@@ -219,9 +308,27 @@ export const performanceRules = [
219
308
  return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'info', `HTML size is acceptable (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Consider reducing HTML size for faster parsing' });
220
309
  }
221
310
  if (size <= poor) {
222
- return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'warn', `HTML is large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Reduce HTML size. Check for inline data, remove unused code' });
311
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'warn', `HTML is large (${sizeKb}KB)`, {
312
+ value: sizeKb,
313
+ recommendation: 'Reduce HTML size. Check for inline data, remove unused code',
314
+ evidence: {
315
+ found: `${sizeKb}KB HTML`,
316
+ expected: `≤ ${Math.round(warning / 1024)}KB (ideally ≤ ${Math.round(good / 1024)}KB)`,
317
+ impact: 'Large HTML increases parsing time and delays time to interactive',
318
+ learnMore: 'https://web.dev/dom-size/'
319
+ }
320
+ });
223
321
  }
224
- return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'fail', `HTML is very large (${sizeKb}KB)`, { value: sizeKb, recommendation: 'Critical: HTML too large. Use pagination, lazy loading, or split content' });
322
+ return createResult({ id: 'response-html-size', name: 'HTML Size', category: 'performance', severity: 'warning' }, 'fail', `HTML is very large (${sizeKb}KB)`, {
323
+ value: sizeKb,
324
+ recommendation: 'Critical: HTML too large. Use pagination, lazy loading, or split content',
325
+ evidence: {
326
+ found: `${sizeKb}KB HTML`,
327
+ expected: `≤ ${Math.round(poor / 1024)}KB (ideally ≤ ${Math.round(good / 1024)}KB)`,
328
+ impact: 'Very large HTML severely impacts page parsing performance and user experience',
329
+ learnMore: 'https://web.dev/dom-size/'
330
+ }
331
+ });
225
332
  },
226
333
  },
227
334
  {
@@ -231,16 +338,26 @@ export const performanceRules = [
231
338
  severity: 'warning',
232
339
  description: 'Response should be compressed (gzip/brotli)',
233
340
  check: (ctx) => {
234
- if (ctx.htmlSize === undefined)
235
- return null;
341
+ if (ctx.htmlSize === undefined) {
342
+ return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (HTML size data unavailable)', { recommendation: 'This rule checks for response compression when response data is available' });
343
+ }
236
344
  if (ctx.isCompressed === false && ctx.htmlSize > 1024) {
237
- return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'warn', 'Response is not compressed', { recommendation: 'Enable gzip or brotli compression on server' });
345
+ return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'warn', 'Response is not compressed', {
346
+ recommendation: 'Enable gzip or brotli compression on server',
347
+ evidence: {
348
+ found: 'No compression (Content-Encoding header missing)',
349
+ expected: 'Content-Encoding: gzip or Content-Encoding: br',
350
+ impact: 'Uncompressed responses are 60-80% larger, wasting bandwidth and slowing downloads',
351
+ example: '# Apache\nAddOutputFilterByType DEFLATE text/html text/css application/javascript\n\n# Nginx\ngzip on;\ngzip_types text/html text/css application/javascript;',
352
+ learnMore: 'https://web.dev/uses-text-compression/'
353
+ }
354
+ });
238
355
  }
239
356
  if (ctx.isCompressed === true) {
240
357
  const ratio = ctx.compressedSize && ctx.htmlSize ? Math.round((ctx.compressedSize / ctx.htmlSize) * 100) : undefined;
241
358
  return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'pass', ratio ? `Response compressed (${ratio}% of original)` : 'Response is compressed');
242
359
  }
243
- return null;
360
+ return createResult({ id: 'response-compression', name: 'Compression', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (small response or compression status unknown)');
244
361
  },
245
362
  },
246
363
  {
@@ -250,8 +367,9 @@ export const performanceRules = [
250
367
  severity: 'warning',
251
368
  description: 'Static resources should have browser caching enabled',
252
369
  check: (ctx) => {
253
- if (ctx.resourcesWithoutCaching === undefined)
254
- return null;
370
+ if (ctx.resourcesWithoutCaching === undefined) {
371
+ return createResult({ id: 'browser-caching', name: 'Browser Caching', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (resource caching data unavailable)', { recommendation: 'This rule checks for browser caching headers on static resources when resource data is available' });
372
+ }
255
373
  if (ctx.resourcesWithoutCaching > 0) {
256
374
  return createResult({ id: 'browser-caching', name: 'Browser Caching', category: 'performance', severity: 'warning' }, 'warn', `${ctx.resourcesWithoutCaching} resources without cache headers`, {
257
375
  value: ctx.resourcesWithoutCaching,
@@ -268,42 +386,43 @@ export const performanceRules = [
268
386
  },
269
387
  },
270
388
  {
271
- id: 'js-css-total-size',
272
- name: 'JS/CSS Total Size',
389
+ id: 'js-total-size',
390
+ name: 'JS Total Size',
273
391
  category: 'performance',
274
392
  severity: 'warning',
275
- description: 'Total JS and CSS size should not exceed 2MB',
393
+ description: 'Total JavaScript size should be minimized for mobile performance (Ideal < 150KB)',
276
394
  check: (ctx) => {
277
- const jsSize = ctx.jsTotalSize || 0;
278
- const cssSize = ctx.cssTotalSize || 0;
279
- const totalSize = jsSize + cssSize;
280
- if (totalSize === 0)
281
- return null;
282
- const sizeMb = totalSize / (1024 * 1024);
283
- if (sizeMb > 2) {
284
- return createResult({ id: 'js-css-total-size', name: 'JS/CSS Total Size', category: 'performance', severity: 'warning' }, 'fail', `JS/CSS total ${sizeMb.toFixed(2)}MB exceeds 2MB limit`, {
285
- value: totalSize,
286
- recommendation: 'Reduce JS/CSS file sizes through minification and code splitting',
395
+ const jsSize = ctx.jsTotalSize;
396
+ if (jsSize === undefined || jsSize === 0) {
397
+ if (ctx.jsFilesCount && ctx.jsFilesCount > 20) {
398
+ return createResult({ id: 'js-total-size', name: 'JS Total Size', category: 'performance', severity: 'info' }, 'info', `Checking ${ctx.jsFilesCount} JS files (size unknown)`, { recommendation: 'Enable deep analysis to measure transfer size. Aim for < 150KB total JS (gzip) for mobile.' });
399
+ }
400
+ return createResult({ id: 'js-total-size', name: 'JS Total Size', category: 'performance', severity: 'info' }, 'info', 'Not applicable (JS size data unavailable)', { recommendation: 'This rule checks total JavaScript size when resource size data is available' });
401
+ }
402
+ const sizeKb = jsSize / 1024;
403
+ if (sizeKb > 500) {
404
+ return createResult({ id: 'js-total-size', name: 'JS Total Size', category: 'performance', severity: 'error' }, 'fail', `Critical: JS size ${sizeKb.toFixed(0)}KB exceeds 500KB limit`, {
405
+ value: sizeKb,
406
+ recommendation: 'Reduce JS bundle size immediately. 500KB+ significantly hurts INP and LCP on mobile.',
287
407
  evidence: {
288
- found: `${sizeMb.toFixed(2)}MB (JS: ${(jsSize / 1024).toFixed(0)}KB, CSS: ${(cssSize / 1024).toFixed(0)}KB)`,
289
- expected: '<2MB total',
290
- impact: 'Large JS/CSS files significantly increase page load time',
291
- learnMore: 'https://web.dev/total-byte-weight/'
408
+ found: `${sizeKb.toFixed(0)}KB`,
409
+ expected: '< 150KB (Ideal), < 300KB (Max)',
410
+ impact: 'High JS execution time blocks the main thread, causing poor responsiveness.'
292
411
  }
293
412
  });
294
413
  }
295
- if (sizeMb > 1) {
296
- return createResult({ id: 'js-css-total-size', name: 'JS/CSS Total Size', category: 'performance', severity: 'warning' }, 'warn', `JS/CSS total ${sizeMb.toFixed(2)}MB is large`, {
297
- value: totalSize,
298
- recommendation: 'Consider reducing JS/CSS bundle sizes',
414
+ if (sizeKb > 300) {
415
+ return createResult({ id: 'js-total-size', name: 'JS Total Size', category: 'performance', severity: 'warning' }, 'warn', `Risk: JS size ${sizeKb.toFixed(0)}KB (> 300KB)`, {
416
+ value: sizeKb,
417
+ recommendation: 'Optimize JS bundles. Code split routes and defer non-critical scripts.',
299
418
  evidence: {
300
- found: `${sizeMb.toFixed(2)}MB`,
301
- expected: '<1MB recommended',
302
- impact: 'Large bundles increase load time, especially on mobile'
419
+ found: `${sizeKb.toFixed(0)}KB`,
420
+ expected: '< 300KB',
421
+ impact: 'Mobile devices struggle with parsing/executing large JS bundles.'
303
422
  }
304
423
  });
305
424
  }
306
- return null;
425
+ return createResult({ id: 'js-total-size', name: 'JS Total Size', category: 'performance', severity: 'info' }, 'pass', `Excellent: JS size ${sizeKb.toFixed(0)}KB (< 300KB)`, { value: sizeKb });
307
426
  },
308
427
  },
309
428
  {
@@ -316,8 +435,9 @@ export const performanceRules = [
316
435
  const jsCount = ctx.jsFilesCount || 0;
317
436
  const cssCount = ctx.cssFilesCount || 0;
318
437
  const totalFiles = jsCount + cssCount;
319
- if (totalFiles === 0)
320
- return null;
438
+ if (totalFiles === 0) {
439
+ return createResult({ id: 'excessive-js-css-files', name: 'Excessive JS/CSS Files', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (no JS/CSS files detected or file count data unavailable)', { recommendation: 'This rule checks for excessive JS/CSS file count when resource data is available' });
440
+ }
321
441
  if (totalFiles > 100) {
322
442
  return createResult({ id: 'excessive-js-css-files', name: 'Excessive JS/CSS Files', category: 'performance', severity: 'warning' }, 'fail', `Page loads ${totalFiles} JS/CSS files (exceeds 100)`, {
323
443
  value: totalFiles,
@@ -340,7 +460,7 @@ export const performanceRules = [
340
460
  }
341
461
  });
342
462
  }
343
- return null;
463
+ return createResult({ id: 'excessive-js-css-files', name: 'Excessive JS/CSS Files', category: 'performance', severity: 'warning' }, 'pass', `JS/CSS file count is acceptable (${totalFiles} files)`, { value: totalFiles });
344
464
  },
345
465
  },
346
466
  {
@@ -350,8 +470,9 @@ export const performanceRules = [
350
470
  severity: 'warning',
351
471
  description: 'JS and CSS files should be minified',
352
472
  check: (ctx) => {
353
- if (ctx.unminifiedResources === undefined)
354
- return null;
473
+ if (ctx.unminifiedResources === undefined) {
474
+ return createResult({ id: 'unminified-resources', name: 'Unminified Resources', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (minification data unavailable)', { recommendation: 'This rule checks for unminified JS/CSS files when resource analysis data is available' });
475
+ }
355
476
  if (ctx.unminifiedResources > 0) {
356
477
  return createResult({ id: 'unminified-resources', name: 'Unminified Resources', category: 'performance', severity: 'warning' }, 'warn', `${ctx.unminifiedResources} unminified JS/CSS files detected`, {
357
478
  value: ctx.unminifiedResources,
@@ -364,7 +485,254 @@ export const performanceRules = [
364
485
  }
365
486
  });
366
487
  }
367
- return null;
488
+ return createResult({ id: 'unminified-resources', name: 'Unminified Resources', category: 'performance', severity: 'warning' }, 'pass', 'All resources are minified');
489
+ },
490
+ },
491
+ {
492
+ id: 'page-weight',
493
+ name: 'Total Page Weight',
494
+ category: 'performance',
495
+ severity: 'warning',
496
+ description: 'Total page size should be under 500KB for optimal performance (Google benchmark)',
497
+ check: (ctx) => {
498
+ const totalSize = ctx.totalPageSize;
499
+ if (totalSize === undefined) {
500
+ return createResult({ id: 'page-weight', name: 'Total Page Weight', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (total page size data unavailable)', { recommendation: 'This rule checks total page weight when resource size data is available' });
501
+ }
502
+ const sizeKb = Math.round(totalSize / 1024);
503
+ const sizeMb = (totalSize / (1024 * 1024)).toFixed(2);
504
+ if (totalSize <= 500 * 1024) {
505
+ return createResult({ id: 'page-weight', name: 'Total Page Weight', category: 'performance', severity: 'warning' }, 'pass', `Page weight is excellent (${sizeKb}KB)`, { value: sizeKb });
506
+ }
507
+ if (totalSize <= 1024 * 1024) {
508
+ return createResult({ id: 'page-weight', name: 'Total Page Weight', category: 'performance', severity: 'warning' }, 'warn', `Page weight exceeds Google benchmark (${sizeKb}KB)`, {
509
+ value: sizeKb,
510
+ recommendation: 'Reduce page size to under 500KB for faster loading',
511
+ evidence: {
512
+ found: `${sizeKb}KB`,
513
+ expected: '< 500KB (Google benchmark)',
514
+ impact: 'Larger pages take longer to download, especially on mobile networks',
515
+ learnMore: 'https://web.dev/total-byte-weight/'
516
+ }
517
+ });
518
+ }
519
+ if (totalSize <= 2 * 1024 * 1024) {
520
+ return createResult({ id: 'page-weight', name: 'Total Page Weight', category: 'performance', severity: 'warning' }, 'fail', `Page weight is heavy (${sizeMb}MB)`, {
521
+ value: sizeKb,
522
+ recommendation: 'Critical: Page is too heavy. Compress images, remove unused code',
523
+ evidence: {
524
+ found: `${sizeMb}MB`,
525
+ expected: '< 500KB',
526
+ impact: 'Heavy pages cause slow loading and high data costs for users'
527
+ }
528
+ });
529
+ }
530
+ return createResult({ id: 'page-weight', name: 'Total Page Weight', category: 'performance', severity: 'warning' }, 'fail', `Page weight is critical (${sizeMb}MB)`, {
531
+ value: sizeKb,
532
+ recommendation: 'Urgent: Drastically reduce page weight. Consider lazy loading, pagination, or removing heavy assets',
533
+ evidence: {
534
+ found: `${sizeMb}MB`,
535
+ expected: '< 500KB',
536
+ impact: 'Severely impacts mobile users and SEO rankings'
537
+ }
538
+ });
539
+ },
540
+ },
541
+ {
542
+ id: 'request-count',
543
+ name: 'Total HTTP Requests',
544
+ category: 'performance',
545
+ severity: 'warning',
546
+ description: 'Total requests should be under 50 for optimal performance (Google benchmark)',
547
+ check: (ctx) => {
548
+ const totalRequests = ctx.totalRequests;
549
+ if (totalRequests === undefined) {
550
+ return createResult({ id: 'request-count', name: 'Total HTTP Requests', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (total requests data unavailable)', { recommendation: 'This rule checks total HTTP request count when resource data is available' });
551
+ }
552
+ if (totalRequests <= 50) {
553
+ return createResult({ id: 'request-count', name: 'Total HTTP Requests', category: 'performance', severity: 'warning' }, 'pass', `Request count is good (${totalRequests} requests)`, { value: totalRequests });
554
+ }
555
+ if (totalRequests <= 100) {
556
+ return createResult({ id: 'request-count', name: 'Total HTTP Requests', category: 'performance', severity: 'warning' }, 'warn', `Request count exceeds Google benchmark (${totalRequests} requests)`, {
557
+ value: totalRequests,
558
+ recommendation: 'Reduce requests by bundling assets, using CSS sprites, or lazy loading',
559
+ evidence: {
560
+ found: `${totalRequests} requests`,
561
+ expected: '< 50 requests (Google benchmark)',
562
+ impact: 'Each HTTP request adds latency, especially on mobile networks'
563
+ }
564
+ });
565
+ }
566
+ return createResult({ id: 'request-count', name: 'Total HTTP Requests', category: 'performance', severity: 'warning' }, 'fail', `Too many HTTP requests (${totalRequests} requests)`, {
567
+ value: totalRequests,
568
+ recommendation: 'Critical: Reduce requests significantly. Bundle JS/CSS, use image sprites, implement lazy loading',
569
+ evidence: {
570
+ found: `${totalRequests} requests`,
571
+ expected: '< 50 requests',
572
+ impact: 'Excessive requests severely impact page load time'
573
+ }
574
+ });
575
+ },
576
+ },
577
+ {
578
+ id: 'font-files-count',
579
+ name: 'Font Files Count',
580
+ category: 'performance',
581
+ severity: 'info',
582
+ description: 'Limit custom font files to reduce page weight (fonts avg ~123KB)',
583
+ check: (ctx) => {
584
+ const fontCount = ctx.fontFilesCount;
585
+ if (fontCount === undefined) {
586
+ return createResult({ id: 'font-files-count', name: 'Font Files Count', category: 'performance', severity: 'info' }, 'info', 'Not applicable (font file count data unavailable)', { recommendation: 'This rule checks for excessive custom font files when resource data is available' });
587
+ }
588
+ if (fontCount === 0) {
589
+ return createResult({ id: 'font-files-count', name: 'Font Files Count', category: 'performance', severity: 'info' }, 'pass', 'No custom fonts detected (using system fonts)');
590
+ }
591
+ if (fontCount <= 3) {
592
+ return createResult({ id: 'font-files-count', name: 'Font Files Count', category: 'performance', severity: 'info' }, 'pass', `Font files count is acceptable (${fontCount} fonts)`, { value: fontCount });
593
+ }
594
+ if (fontCount <= 6) {
595
+ return createResult({ id: 'font-files-count', name: 'Font Files Count', category: 'performance', severity: 'info' }, 'info', `Multiple font files detected (${fontCount} fonts)`, {
596
+ value: fontCount,
597
+ recommendation: 'Consider reducing font variants or using system fonts',
598
+ evidence: {
599
+ found: `${fontCount} font files`,
600
+ expected: '1-3 font files',
601
+ impact: 'Custom fonts add ~40-100KB each to page weight'
602
+ }
603
+ });
604
+ }
605
+ return createResult({ id: 'font-files-count', name: 'Font Files Count', category: 'performance', severity: 'info' }, 'warn', `Too many font files (${fontCount} fonts)`, {
606
+ value: fontCount,
607
+ recommendation: 'Reduce font files. Use variable fonts or limit to essential weights/styles',
608
+ evidence: {
609
+ found: `${fontCount} font files`,
610
+ expected: '< 4 font files',
611
+ impact: 'Excessive fonts significantly increase page weight and FOIT/FOUT issues'
612
+ }
613
+ });
614
+ },
615
+ },
616
+ {
617
+ id: 'font-display-swap',
618
+ name: 'Font Display Swap',
619
+ category: 'performance',
620
+ severity: 'warning',
621
+ description: 'Custom fonts should use font-display: swap to prevent invisible text',
622
+ check: (ctx) => {
623
+ if (!ctx.fontFilesCount || ctx.fontFilesCount === 0) {
624
+ return createResult({ id: 'font-display-swap', name: 'Font Display Swap', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (no custom fonts detected)', { recommendation: 'This rule checks for font-display: swap when custom fonts are present' });
625
+ }
626
+ if (ctx.hasFontDisplaySwap === undefined) {
627
+ return createResult({ id: 'font-display-swap', name: 'Font Display Swap', category: 'performance', severity: 'warning' }, 'info', 'Not applicable (font display swap data unavailable)', { recommendation: 'This rule checks for font-display: swap in @font-face declarations' });
628
+ }
629
+ if (!ctx.hasFontDisplaySwap) {
630
+ return createResult({ id: 'font-display-swap', name: 'Font Display Swap', category: 'performance', severity: 'warning' }, 'warn', 'Custom fonts may block text rendering', {
631
+ recommendation: 'Add font-display: swap to @font-face declarations',
632
+ evidence: {
633
+ found: 'Custom fonts without font-display: swap',
634
+ expected: '@font-face { font-display: swap; }',
635
+ impact: 'Without font-display, text may be invisible until fonts load (FOIT), hurting perceived performance',
636
+ example: '@font-face {\n font-family: "Open Sans";\n src: url("/fonts/OpenSans.woff2");\n font-display: swap; /* Add this */\n}',
637
+ learnMore: 'https://web.dev/font-display/'
638
+ }
639
+ });
640
+ }
641
+ return createResult({ id: 'font-display-swap', name: 'Font Display Swap', category: 'performance', severity: 'warning' }, 'pass', 'Font display swap is enabled');
642
+ },
643
+ },
644
+ {
645
+ id: 'external-origins-count',
646
+ name: 'External Origins',
647
+ category: 'performance',
648
+ severity: 'info',
649
+ description: 'Limit external origins to reduce DNS lookups and connection overhead',
650
+ check: (ctx) => {
651
+ const externalOrigins = ctx.externalOrigins;
652
+ if (externalOrigins === undefined) {
653
+ return createResult({ id: 'external-origins-count', name: 'External Origins', category: 'performance', severity: 'info' }, 'info', 'Not applicable (external origins data unavailable)', { recommendation: 'This rule checks for excessive external origins when resource data is available' });
654
+ }
655
+ if (externalOrigins <= 5) {
656
+ return createResult({ id: 'external-origins-count', name: 'External Origins', category: 'performance', severity: 'info' }, 'pass', `External origins count is good (${externalOrigins} origins)`, { value: externalOrigins });
657
+ }
658
+ if (externalOrigins <= 10) {
659
+ return createResult({ id: 'external-origins-count', name: 'External Origins', category: 'performance', severity: 'info' }, 'info', `Multiple external origins (${externalOrigins} origins)`, {
660
+ value: externalOrigins,
661
+ recommendation: 'Use preconnect hints for critical external domains',
662
+ evidence: {
663
+ found: `${externalOrigins} external origins`,
664
+ expected: '< 6 external origins',
665
+ impact: 'Each origin requires DNS lookup and connection setup'
666
+ }
667
+ });
668
+ }
669
+ return createResult({ id: 'external-origins-count', name: 'External Origins', category: 'performance', severity: 'info' }, 'warn', `Too many external origins (${externalOrigins} origins)`, {
670
+ value: externalOrigins,
671
+ recommendation: 'Reduce third-party dependencies or consolidate to fewer providers',
672
+ evidence: {
673
+ found: `${externalOrigins} external origins`,
674
+ expected: '< 10 external origins',
675
+ impact: 'Excessive external origins increase DNS lookups and connection overhead significantly'
676
+ }
677
+ });
678
+ },
679
+ },
680
+ {
681
+ id: 'iframe-count',
682
+ name: 'Iframe Count',
683
+ category: 'performance',
684
+ severity: 'info',
685
+ description: 'Excessive iframes (ads, widgets, embeds) impact page performance',
686
+ check: (ctx) => {
687
+ const iframeCount = ctx.iframeCount;
688
+ if (iframeCount === undefined || iframeCount === 0) {
689
+ return createResult({ id: 'iframe-count', name: 'Iframe Count', category: 'performance', severity: 'info' }, 'pass', 'No iframes detected');
690
+ }
691
+ if (iframeCount <= 3) {
692
+ return createResult({ id: 'iframe-count', name: 'Iframe Count', category: 'performance', severity: 'info' }, 'pass', `Iframe count is acceptable (${iframeCount} iframes)`, { value: iframeCount });
693
+ }
694
+ if (iframeCount <= 6) {
695
+ return createResult({ id: 'iframe-count', name: 'Iframe Count', category: 'performance', severity: 'info' }, 'info', `Multiple iframes detected (${iframeCount} iframes)`, {
696
+ value: iframeCount,
697
+ recommendation: 'Consider lazy loading iframes or reducing embedded content',
698
+ evidence: {
699
+ found: `${iframeCount} iframes`,
700
+ expected: '< 4 iframes',
701
+ impact: 'Each iframe loads a separate document, increasing page weight and requests'
702
+ }
703
+ });
704
+ }
705
+ return createResult({ id: 'iframe-count', name: 'Iframe Count', category: 'performance', severity: 'info' }, 'warn', `Too many iframes (${iframeCount} iframes)`, {
706
+ value: iframeCount,
707
+ recommendation: 'Reduce iframes. Consider removing non-essential ads or widgets',
708
+ evidence: {
709
+ found: `${iframeCount} iframes`,
710
+ expected: '< 4 iframes',
711
+ impact: 'Excessive iframes (often ads) significantly slow down page load'
712
+ }
713
+ });
714
+ },
715
+ },
716
+ {
717
+ id: 'large-base64-images',
718
+ name: 'Large Inline Images',
719
+ category: 'performance',
720
+ severity: 'warning',
721
+ description: 'Large base64 inline images bloat HTML and cannot be cached separately',
722
+ check: (ctx) => {
723
+ const largeBase64 = ctx.largeBase64ImagesCount;
724
+ if (largeBase64 === undefined || largeBase64 === 0) {
725
+ return createResult({ id: 'large-base64-images', name: 'Large Inline Images', category: 'performance', severity: 'warning' }, 'pass', 'No large base64 inline images detected');
726
+ }
727
+ return createResult({ id: 'large-base64-images', name: 'Large Inline Images', category: 'performance', severity: 'warning' }, 'warn', `${largeBase64} large base64 image(s) found (> 5KB each)`, {
728
+ value: largeBase64,
729
+ recommendation: 'Use external image files instead of large base64 strings',
730
+ evidence: {
731
+ found: `${largeBase64} large inline images`,
732
+ expected: 'Base64 images should be < 2KB (icons only)',
733
+ impact: 'Large base64 images increase HTML size and cannot be cached separately'
734
+ }
735
+ });
368
736
  },
369
737
  },
370
738
  ];