recker 1.0.42 → 1.0.43-next.0b080a5

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 +94465 -0
  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 -0
  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 +14 -6
  458. package/dist/browser/utils/optional-require.d.ts +0 -19
  459. package/dist/browser/utils/optional-require.js +0 -105
@@ -0,0 +1,1399 @@
1
+ import { parse } from '../scrape/parser/index.js';
2
+ import { extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractLinks, extractImages, } from '../scrape/extractors.js';
3
+ import { generateKeywordCloud } from './keywords.js';
4
+ import { createRulesEngine, SEO_THRESHOLDS, } from './rules/index.js';
5
+ export class SeoAnalyzer {
6
+ root;
7
+ options;
8
+ rulesEngine;
9
+ constructor(root, options = {}) {
10
+ this.root = root;
11
+ this.options = options;
12
+ this.rulesEngine = createRulesEngine(options.rules);
13
+ }
14
+ static async fromHtml(html, options = {}) {
15
+ const root = parse(html);
16
+ return new SeoAnalyzer(root, options);
17
+ }
18
+ analyze() {
19
+ const url = this.options.baseUrl || '';
20
+ const meta = extractMeta(this.root);
21
+ const og = extractOpenGraph(this.root);
22
+ const twitter = extractTwitterCard(this.root);
23
+ const jsonLd = extractJsonLd(this.root);
24
+ const links = extractLinks(this.root, { baseUrl: this.options.baseUrl });
25
+ const images = extractImages(this.root, { baseUrl: this.options.baseUrl });
26
+ const visibleText = this.getVisibleText();
27
+ const keywords = generateKeywordCloud({
28
+ visibleText,
29
+ title: meta.title,
30
+ description: meta.description,
31
+ keywords: meta.keywords?.join(', ')
32
+ });
33
+ const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
34
+ const emailsFound = (visibleText.match(emailRegex) || []).filter(e => !e.endsWith('.png') && !e.endsWith('.jpg') && !e.endsWith('.webp'));
35
+ const socialDomains = ['facebook.com', 'twitter.com', 'x.com', 'instagram.com', 'linkedin.com', 'youtube.com', 'pinterest.com', 'tiktok.com', 'github.com', 'reddit.com', 'snapchat.com', 'whatsapp.com', 'telegram.org', 'discord.com', 'threads.net'];
36
+ const socialLinksFound = links
37
+ .map(l => l.href)
38
+ .filter(href => socialDomains.some(d => href.toLowerCase().includes(d)));
39
+ const socialLinkDetails = this.analyzeSocialLinks(links, socialDomains);
40
+ const headings = this.analyzeHeadings();
41
+ const content = this.analyzeContent(headings);
42
+ const linkAnalysis = this.buildLinkAnalysis(links);
43
+ const imageAnalysis = this.buildImageAnalysis(images);
44
+ const social = this.buildSocialAnalysis(og, twitter);
45
+ const technical = this.buildTechnicalAnalysis(meta);
46
+ const resources = this.analyzeResources();
47
+ const analytics = this.analyzeAnalytics();
48
+ const feeds = this.analyzeFeeds();
49
+ const conversion = this.analyzeConversionElements(links, visibleText);
50
+ const context = this.buildRuleContext({
51
+ meta,
52
+ og,
53
+ twitter,
54
+ jsonLd,
55
+ headings,
56
+ content,
57
+ linkAnalysis,
58
+ imageAnalysis,
59
+ links,
60
+ keywords,
61
+ resources,
62
+ emailsFound,
63
+ socialLinksFound,
64
+ socialLinkDetails,
65
+ analytics,
66
+ feeds,
67
+ conversion,
68
+ });
69
+ const ruleResults = this.rulesEngine.evaluate(context);
70
+ const checks = this.convertToCheckResults(ruleResults);
71
+ const { score, grade } = this.calculateScore(checks);
72
+ const summary = this.buildSummary(ruleResults, checks, {
73
+ content,
74
+ imageAnalysis,
75
+ linkAnalysis,
76
+ meta,
77
+ og,
78
+ twitter,
79
+ technical,
80
+ });
81
+ return {
82
+ url,
83
+ timestamp: new Date(),
84
+ grade,
85
+ score,
86
+ summary,
87
+ checks,
88
+ title: meta.title
89
+ ? { text: meta.title, length: meta.title.length }
90
+ : undefined,
91
+ metaDescription: meta.description
92
+ ? { text: meta.description, length: meta.description.length }
93
+ : undefined,
94
+ openGraph: Object.keys(og).length > 0
95
+ ? {
96
+ title: og.title,
97
+ description: og.description,
98
+ image: Array.isArray(og.image) ? og.image[0] : og.image,
99
+ url: og.url,
100
+ type: og.type,
101
+ siteName: og.siteName,
102
+ }
103
+ : undefined,
104
+ twitterCard: Object.keys(twitter).length > 0
105
+ ? {
106
+ card: twitter.card,
107
+ title: twitter.title,
108
+ description: twitter.description,
109
+ image: Array.isArray(twitter.image)
110
+ ? twitter.image[0]
111
+ : twitter.image,
112
+ site: twitter.site,
113
+ }
114
+ : undefined,
115
+ structuredData: {
116
+ count: jsonLd.length,
117
+ types: jsonLd.map((j) => j['@type']).filter(Boolean),
118
+ items: jsonLd,
119
+ },
120
+ headings: headings,
121
+ content,
122
+ keywords,
123
+ links: linkAnalysis,
124
+ images: imageAnalysis,
125
+ social,
126
+ technical,
127
+ };
128
+ }
129
+ getMainBody() {
130
+ const bodies = this.root.querySelectorAll('body');
131
+ if (bodies.length === 0)
132
+ return null;
133
+ if (bodies.length === 1)
134
+ return bodies[0];
135
+ return bodies.reduce((prev, curr) => curr.text.length > prev.text.length ? curr : prev);
136
+ }
137
+ getVisibleText() {
138
+ const body = this.getMainBody();
139
+ if (!body)
140
+ return '';
141
+ const clone = body.clone();
142
+ const tagsToRemove = ['script', 'style', 'noscript', 'iframe', 'svg', 'header', 'footer', 'nav'];
143
+ try {
144
+ const elements = clone.querySelectorAll(tagsToRemove.join(','));
145
+ elements.forEach(el => el.remove());
146
+ }
147
+ catch {
148
+ tagsToRemove.forEach(tag => {
149
+ clone.querySelectorAll(tag).forEach(el => el.remove());
150
+ });
151
+ }
152
+ return clone.text.replace(/\s+/g, ' ').trim();
153
+ }
154
+ buildRuleContext(data) {
155
+ const { meta, og, twitter, jsonLd, headings, content, linkAnalysis, imageAnalysis, links, keywords, resources, emailsFound, socialLinksFound, socialLinkDetails, analytics, feeds, conversion, } = data;
156
+ const html = this.root.querySelector('html');
157
+ const htmlLang = html ? html.getAttribute('lang') : undefined;
158
+ const hreflangTags = [];
159
+ this.root
160
+ .querySelectorAll('link[rel="alternate"][hreflang]')
161
+ .forEach((el) => {
162
+ const lang = el.getAttribute('hreflang');
163
+ const href = el.getAttribute('href');
164
+ if (lang && href) {
165
+ hreflangTags.push({ lang, href });
166
+ }
167
+ });
168
+ const ogLocaleEl = this.root.querySelector('meta[property="og:locale"]');
169
+ const ogLocale = ogLocaleEl ? ogLocaleEl.getAttribute('content') : undefined;
170
+ const genericTexts = SEO_THRESHOLDS.links.genericTexts;
171
+ const genericTextLinks = links.filter((l) => {
172
+ const text = l.text?.toLowerCase().trim();
173
+ return text && genericTexts.some((g) => text === g || text.includes(g));
174
+ });
175
+ const linksWithGenericText = genericTextLinks.length;
176
+ const linksWithoutTextArray = links.filter((l) => {
177
+ const hasText = l.text && l.text.trim() !== '';
178
+ const hasContent = l.hasImageWithAlt || l.hasSvgWithTitle;
179
+ const hasA11yLabel = l.ariaLabel || l.title;
180
+ return !hasText && !hasContent && !hasA11yLabel;
181
+ });
182
+ const externalBlankLinks = links.filter((l) => l.type === 'external' && l.target === '_blank');
183
+ const missingNoopenerLinks = externalBlankLinks.filter((l) => !l.rel?.includes('noopener'));
184
+ const missingNoreferrerLinks = externalBlankLinks.filter((l) => !l.rel?.includes('noreferrer'));
185
+ const problematicLinks = {
186
+ withoutText: linksWithoutTextArray,
187
+ genericText: genericTextLinks,
188
+ missingNoopener: missingNoopenerLinks,
189
+ missingNoreferrer: missingNoreferrerLinks,
190
+ };
191
+ const hasMixedContent = this.checkMixedContent();
192
+ const h1Elements = this.root.querySelectorAll('h1');
193
+ const h1Text = h1Elements.length > 0 ? h1Elements[0].text.trim() : '';
194
+ const iframeCount = this.root.querySelectorAll('iframe').length;
195
+ const topKeywords = keywords.topKeywords.slice(0, 5).map(k => k.word);
196
+ const mainKeyword = topKeywords.length > 0 ? topKeywords[0] : undefined;
197
+ const keywordsInTitle = topKeywords.some(kw => meta.title?.toLowerCase().includes(kw));
198
+ const keywordsInDescription = topKeywords.some(kw => meta.description?.toLowerCase().includes(kw));
199
+ const keywordsInH1 = topKeywords.some(kw => h1Text.toLowerCase().includes(kw));
200
+ const urlPath = this.options.baseUrl ? new URL(this.options.baseUrl).pathname.toLowerCase().replace(/[-_]/g, ' ') : '';
201
+ const keywordsInUrl = topKeywords.some(kw => urlPath.includes(kw));
202
+ const firstParagraph = this.root.querySelector('p')?.text?.toLowerCase() || '';
203
+ const keywordsInFirstParagraph = topKeywords.some(kw => firstParagraph.includes(kw));
204
+ const imageAlts = imageAnalysis.imageAltTexts || [];
205
+ const keywordsInAltText = imageAlts.some(alt => topKeywords.some(kw => alt.includes(kw)));
206
+ const keywordConsistencyDetails = mainKeyword ? {
207
+ inTitle: meta.title?.toLowerCase().includes(mainKeyword) || false,
208
+ inDescription: meta.description?.toLowerCase().includes(mainKeyword) || false,
209
+ inH1: h1Text.toLowerCase().includes(mainKeyword) || false,
210
+ inUrl: urlPath.includes(mainKeyword),
211
+ inFirstParagraph: firstParagraph.includes(mainKeyword),
212
+ inAltText: imageAlts.some(alt => alt.includes(mainKeyword)),
213
+ } : undefined;
214
+ const keywordConsistencyScore = keywordConsistencyDetails
215
+ ? Object.values(keywordConsistencyDetails).filter(Boolean).length
216
+ : undefined;
217
+ const viewportEl = this.root.querySelector('meta[name="viewport"]');
218
+ const viewportContent = viewportEl
219
+ ? viewportEl.getAttribute('content')
220
+ : undefined;
221
+ const a11yMetrics = this.analyzeAccessibility();
222
+ const imagesWithEmptyAlt = this.root.querySelectorAll('img[alt=""]').length;
223
+ const linkSecurityMetrics = this.analyzeLinkSecurity();
224
+ const faviconInfo = this.detectFavicon();
225
+ const perfHints = this.analyzePerformanceHints();
226
+ const cwvHints = this.analyzeCWVHints();
227
+ const structuralHtml = this.analyzeStructuralHtml();
228
+ const breadcrumbs = this.analyzeBreadcrumbs(jsonLd.map((j) => j['@type']).filter(Boolean));
229
+ const multimedia = this.analyzeMultimedia();
230
+ const advancedImages = this.analyzeAdvancedImages();
231
+ const responsiveImages = this.analyzeResponsiveImages();
232
+ const inlineImages = this.analyzeInlineImages();
233
+ const trustSignals = this.analyzeTrustSignals(links);
234
+ const totalSubheadings = (headings.structure.filter((h) => h.level === 2).length || 0) +
235
+ (headings.structure.filter((h) => h.level === 3).length || 0);
236
+ const subheadingFrequency = content.wordCount > 0
237
+ ? (totalSubheadings / content.wordCount) * 100
238
+ : 0;
239
+ const textHtmlRatio = this.calculateTextHtmlRatio(content.characterCount);
240
+ return {
241
+ jsFilesCount: resources.jsFilesCount,
242
+ cssFilesCount: resources.cssFilesCount,
243
+ unminifiedResources: resources.unminifiedResources,
244
+ unminifiedResourceUrls: resources.unminifiedResourceUrls,
245
+ emailsFound,
246
+ socialLinksFound,
247
+ ...socialLinkDetails,
248
+ keywordsInTitle,
249
+ keywordsInDescription,
250
+ keywordsInH1,
251
+ keywordsInUrl,
252
+ keywordsInFirstParagraph,
253
+ keywordsInAltText,
254
+ keywordConsistencyScore,
255
+ keywordConsistencyDetails,
256
+ topKeywords,
257
+ mainKeyword,
258
+ title: meta.title,
259
+ titleLength: meta.title?.length,
260
+ metaDescription: meta.description,
261
+ metaDescriptionLength: meta.description?.length,
262
+ metaKeywords: meta.keywords,
263
+ metaRobots: meta.robots,
264
+ ogTitle: og.title,
265
+ ogDescription: og.description,
266
+ ogImage: Array.isArray(og.image) ? og.image[0] : og.image,
267
+ ogUrl: og.url,
268
+ ogType: og.type,
269
+ ogSiteName: og.siteName,
270
+ twitterCard: twitter.card,
271
+ twitterTitle: twitter.title,
272
+ twitterDescription: twitter.description,
273
+ twitterImage: Array.isArray(twitter.image)
274
+ ? twitter.image[0]
275
+ : twitter.image,
276
+ twitterSite: twitter.site,
277
+ h1Count: headings.h1Count,
278
+ h1Text: h1Text || undefined,
279
+ h1Length: h1Text?.length,
280
+ h2Count: headings.structure.filter((h) => h.level === 2).length,
281
+ headingHierarchyValid: headings.hasProperHierarchy,
282
+ headingSkippedLevels: headings.issues.filter((i) => i.includes('Skipped')),
283
+ sectionWordCounts: headings.sectionWordCounts,
284
+ totalImages: imageAnalysis.total,
285
+ imagesWithAlt: imageAnalysis.withAlt,
286
+ imagesWithoutAlt: imageAnalysis.withoutAlt,
287
+ imagesWithLazyLoad: imageAnalysis.lazy,
288
+ imagesMissingDimensions: imageAnalysis.missingDimensions,
289
+ imagesWithEmptyAlt,
290
+ imagesUsingModernFormats: imageAnalysis.modernFormats,
291
+ altTextLengths: imageAnalysis.altTextLengths,
292
+ imageAltTexts: imageAnalysis.imageAltTexts,
293
+ imageFilenames: imageAnalysis.imageFilenames,
294
+ imagesWithAsyncDecoding: imageAnalysis.imagesWithAsyncDecoding,
295
+ imagesWithSrcset: responsiveImages.imagesWithSrcset,
296
+ largeBase64ImagesCount: inlineImages.largeBase64ImagesCount,
297
+ ...a11yMetrics,
298
+ allLinks: links,
299
+ totalLinks: linkAnalysis.total,
300
+ internalLinks: linkAnalysis.internal,
301
+ externalLinks: linkAnalysis.external,
302
+ internalHttpLinks: linkAnalysis.internalHttpLinks,
303
+ internalHttpLinkUrls: linkAnalysis.internalHttpLinkUrls,
304
+ linksWithoutText: linkAnalysis.withoutText,
305
+ nofollowLinks: linkAnalysis.nofollow,
306
+ sponsoredLinks: linkAnalysis.sponsoredLinks,
307
+ ugcLinks: linkAnalysis.ugcLinks,
308
+ linksWithGenericText,
309
+ ...linkSecurityMetrics,
310
+ problematicLinks,
311
+ wordCount: content.wordCount,
312
+ characterCount: content.characterCount,
313
+ sentenceCount: content.sentenceCount,
314
+ paragraphCount: content.paragraphCount,
315
+ avgWordsPerSentence: content.avgWordsPerSentence,
316
+ avgParagraphLength: content.avgParagraphLength,
317
+ listCount: content.listCount,
318
+ strongTagCount: content.strongTagCount,
319
+ emTagCount: content.emTagCount,
320
+ subheadingFrequency,
321
+ paragraphWordCounts: content.paragraphWordCounts,
322
+ avgSentenceLength: content.avgSentenceLength,
323
+ faqCount: content.faqCount,
324
+ imagePerWordRatio: content.imagePerWordRatio,
325
+ keywordDensity: content.keywordDensity,
326
+ fleschReadingEase: content.fleschReadingEase,
327
+ hasQuestionHeadings: content.hasQuestionHeadings,
328
+ ...structuralHtml,
329
+ ...trustSignals,
330
+ ...breadcrumbs,
331
+ ...multimedia,
332
+ hasCanonical: !!meta.canonical,
333
+ canonicalUrl: meta.canonical,
334
+ hasViewport: !!meta.viewport,
335
+ viewportContent,
336
+ hasCharset: !!meta.charset,
337
+ charset: meta.charset,
338
+ hasLang: !!htmlLang,
339
+ langValue: htmlLang,
340
+ isHttps: this.options.baseUrl?.startsWith('https://'),
341
+ hasMixedContent,
342
+ responseHeaders: this.options.responseHeaders,
343
+ textHtmlRatio,
344
+ hasFrameTags: this.root.querySelectorAll('frame, frameset').length > 0,
345
+ iframeCount: this.root.querySelectorAll('iframe').length,
346
+ hasDeprecatedPlugins: this.root.querySelectorAll('object, embed, applet').length > 0,
347
+ deprecatedTagsCount: this.root.querySelectorAll('center, font, strike, u, marquee, blink, big, tt').length,
348
+ deprecatedTagsFound: ['center', 'font', 'strike', 'u', 'marquee', 'blink', 'big', 'tt'].filter(t => this.root.querySelectorAll(t).length > 0),
349
+ hasAppleTouchIcon: this.root.querySelectorAll('link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]').length > 0,
350
+ ...faviconInfo,
351
+ ...perfHints,
352
+ lcpHints: cwvHints.lcpHints,
353
+ clsHints: cwvHints.clsHints,
354
+ jsonLdCount: jsonLd.length,
355
+ jsonLdTypes: jsonLd.map((j) => j['@type']).filter(Boolean),
356
+ url: this.options.baseUrl,
357
+ urlLength: this.options.baseUrl?.length,
358
+ titleMatchesH1: meta.title && h1Text
359
+ ? meta.title.toLowerCase().trim() === h1Text.toLowerCase().trim()
360
+ : undefined,
361
+ ...this.analyzeUrlQuality(),
362
+ ...this.analyzeJsRendering(content),
363
+ hreflangTags: hreflangTags.length > 0 ? hreflangTags : undefined,
364
+ ogLocale,
365
+ analyticsDetected: analytics.analyticsDetected,
366
+ analyticsProviders: analytics.analyticsProviders,
367
+ ...feeds,
368
+ ctaButtonsCount: conversion.ctaButtonsCount,
369
+ formCount: conversion.formCount,
370
+ hasWhatsAppLink: conversion.hasWhatsAppLink,
371
+ hasPhoneOnPage: conversion.hasPhoneOnPage,
372
+ };
373
+ }
374
+ analyzeUrlQuality() {
375
+ if (!this.options.baseUrl) {
376
+ return {
377
+ urlHasUppercase: false,
378
+ urlHasSpecialChars: false,
379
+ urlHasAccents: false,
380
+ };
381
+ }
382
+ try {
383
+ const url = new URL(this.options.baseUrl);
384
+ const path = url.pathname + url.search;
385
+ const urlHasUppercase = /[A-Z]/.test(path);
386
+ const urlHasAccents = /[àáâãäåæçèéêëìíîïñòóôõöùúûüýÿ]/i.test(path);
387
+ const urlHasSpecialChars = /[<>{}|\^`\[\]]/.test(path) || /%[0-9A-F]{2}/i.test(path);
388
+ return { urlHasUppercase, urlHasSpecialChars, urlHasAccents };
389
+ }
390
+ catch {
391
+ return {
392
+ urlHasUppercase: false,
393
+ urlHasSpecialChars: false,
394
+ urlHasAccents: false,
395
+ };
396
+ }
397
+ }
398
+ analyzeJsRendering(content) {
399
+ const bodyTextLength = content.characterCount;
400
+ const scriptCount = this.root.querySelectorAll('script').length;
401
+ const noscriptEl = this.root.querySelector('noscript');
402
+ const noscriptContent = noscriptEl ? noscriptEl.text.trim() : '';
403
+ const hasNoscriptContent = noscriptContent.length > 0;
404
+ return { bodyTextLength, scriptCount, hasNoscriptContent };
405
+ }
406
+ analyzeResponsiveImages() {
407
+ let imagesWithSrcset = 0;
408
+ this.root.querySelectorAll('img').forEach((img) => {
409
+ if (img.getAttribute('srcset') || (img.parentNode && img.parentNode.tagName === 'PICTURE')) {
410
+ imagesWithSrcset++;
411
+ }
412
+ });
413
+ return { imagesWithSrcset };
414
+ }
415
+ analyzeInlineImages() {
416
+ let largeBase64ImagesCount = 0;
417
+ this.root.querySelectorAll('img').forEach((img) => {
418
+ const src = img.getAttribute('src') || '';
419
+ if (src.startsWith('data:image') && src.length > 5 * 1024) {
420
+ largeBase64ImagesCount++;
421
+ }
422
+ });
423
+ return { largeBase64ImagesCount };
424
+ }
425
+ checkMixedContent() {
426
+ let hasMixed = false;
427
+ this.root.querySelectorAll('img[src^="http://"]').forEach(() => {
428
+ hasMixed = true;
429
+ });
430
+ this.root.querySelectorAll('script[src^="http://"]').forEach(() => {
431
+ hasMixed = true;
432
+ });
433
+ this.root.querySelectorAll('link[href^="http://"]').forEach(() => {
434
+ hasMixed = true;
435
+ });
436
+ return hasMixed;
437
+ }
438
+ analyzeLinkSecurity() {
439
+ let withoutNoopener = 0;
440
+ let withoutNoreferrer = 0;
441
+ this.root
442
+ .querySelectorAll('a[href^="http"][target="_blank"]')
443
+ .forEach((el) => {
444
+ const href = el.getAttribute('href') || '';
445
+ const rel = (el.getAttribute('rel') || '').toLowerCase();
446
+ if (this.options.baseUrl && href.startsWith(this.options.baseUrl)) {
447
+ return;
448
+ }
449
+ if (!rel.includes('noopener')) {
450
+ withoutNoopener++;
451
+ }
452
+ if (!rel.includes('noreferrer')) {
453
+ withoutNoreferrer++;
454
+ }
455
+ });
456
+ return {
457
+ externalLinksWithoutNoopener: withoutNoopener,
458
+ externalLinksWithoutNoreferrer: withoutNoreferrer,
459
+ };
460
+ }
461
+ detectFavicon() {
462
+ const faviconSelectors = [
463
+ 'link[rel="icon"]',
464
+ 'link[rel="shortcut icon"]',
465
+ 'link[rel="apple-touch-icon"]',
466
+ 'link[rel="apple-touch-icon-precomposed"]',
467
+ ];
468
+ for (const selector of faviconSelectors) {
469
+ const favicon = this.root.querySelector(selector);
470
+ if (favicon) {
471
+ return {
472
+ hasFavicon: true,
473
+ faviconUrl: favicon.getAttribute('href'),
474
+ };
475
+ }
476
+ }
477
+ return { hasFavicon: false };
478
+ }
479
+ analyzePerformanceHints() {
480
+ const preconnectLinks = this.root.querySelectorAll('link[rel="preconnect"]');
481
+ const preconnectCount = preconnectLinks.length;
482
+ const dnsPrefetchLinks = this.root.querySelectorAll('link[rel="dns-prefetch"]');
483
+ const dnsPrefetchCount = dnsPrefetchLinks.length;
484
+ const preloadLinks = this.root.querySelectorAll('link[rel="preload"]');
485
+ const preloadCount = preloadLinks.length;
486
+ let renderBlockingResources = 0;
487
+ this.root
488
+ .querySelectorAll('head script[src]:not([async]):not([defer])')
489
+ .forEach(() => {
490
+ renderBlockingResources++;
491
+ });
492
+ this.root.querySelectorAll('head link[rel="stylesheet"]').forEach((el) => {
493
+ const media = el.getAttribute('media');
494
+ if (!media || media === 'all' || media === 'screen') {
495
+ renderBlockingResources++;
496
+ }
497
+ });
498
+ const inlineScriptsCount = this.root
499
+ .querySelectorAll('script:not([src])')
500
+ .filter((el) => {
501
+ const content = el.innerHTML || '';
502
+ return content.trim().length > 0;
503
+ }).length;
504
+ const inlineStylesCount = this.root.querySelectorAll('style').length;
505
+ return {
506
+ hasPreconnect: preconnectCount > 0,
507
+ preconnectCount,
508
+ hasDnsPrefetch: dnsPrefetchCount > 0,
509
+ dnsPrefetchCount,
510
+ hasPreload: preloadCount > 0,
511
+ preloadCount,
512
+ renderBlockingResources,
513
+ inlineScriptsCount,
514
+ inlineStylesCount,
515
+ };
516
+ }
517
+ analyzeCWVHints() {
518
+ const images = this.root.querySelectorAll('img');
519
+ let hasLargeImages = false;
520
+ let hasLazyLcp = false;
521
+ let hasPriorityHints = false;
522
+ images.slice(0, 3).forEach((el, index) => {
523
+ const width = parseInt(el.getAttribute('width') || '0', 10);
524
+ const height = parseInt(el.getAttribute('height') || '0', 10);
525
+ const loading = el.getAttribute('loading');
526
+ const fetchPriority = el.getAttribute('fetchpriority');
527
+ if (width >= 400 || height >= 300) {
528
+ hasLargeImages = true;
529
+ }
530
+ if (index === 0 && loading === 'lazy') {
531
+ hasLazyLcp = true;
532
+ }
533
+ if (fetchPriority === 'high') {
534
+ hasPriorityHints = true;
535
+ }
536
+ });
537
+ this.root
538
+ .querySelectorAll('[style*="background-image"]')
539
+ .slice(0, 3)
540
+ .forEach(() => {
541
+ hasLargeImages = true;
542
+ });
543
+ const imagesWithoutDimensions = this.root.querySelectorAll('img:not([width]):not([height])').length +
544
+ this.root.querySelectorAll('img[width="auto"], img[height="auto"]')
545
+ .length;
546
+ return {
547
+ lcpHints: {
548
+ hasLargeImages,
549
+ hasLazyLcp,
550
+ hasPriorityHints,
551
+ },
552
+ clsHints: {
553
+ imagesWithoutDimensions,
554
+ },
555
+ };
556
+ }
557
+ analyzeAccessibility() {
558
+ let buttonsWithoutAriaLabel = 0;
559
+ this.root.querySelectorAll('button').forEach((el) => {
560
+ const text = el.text.trim();
561
+ const ariaLabel = el.getAttribute('aria-label');
562
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
563
+ const title = el.getAttribute('title');
564
+ if (!text && !ariaLabel && !ariaLabelledBy && !title) {
565
+ buttonsWithoutAriaLabel++;
566
+ }
567
+ });
568
+ let linksWithoutAriaLabel = 0;
569
+ this.root.querySelectorAll('a[href]').forEach((el) => {
570
+ const text = el.text.trim();
571
+ const ariaLabel = el.getAttribute('aria-label');
572
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
573
+ const title = el.getAttribute('title');
574
+ const imgs = el.querySelectorAll('img');
575
+ const hasImgWithAlt = imgs.filter((img) => !!img.getAttribute('alt')?.trim()).length > 0;
576
+ const svgs = el.querySelectorAll('svg');
577
+ const hasSvgWithTitle = svgs.filter((svg) => svg.querySelectorAll('title').length > 0 ||
578
+ !!svg.getAttribute('aria-label')).length > 0;
579
+ const hasContent = text || hasImgWithAlt || hasSvgWithTitle;
580
+ const hasLabel = ariaLabel || ariaLabelledBy || title;
581
+ if (!hasContent && !hasLabel) {
582
+ linksWithoutAriaLabel++;
583
+ }
584
+ });
585
+ let inputsWithoutLabel = 0;
586
+ this.root
587
+ .querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea')
588
+ .forEach((el) => {
589
+ const id = el.getAttribute('id');
590
+ const ariaLabel = el.getAttribute('aria-label');
591
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
592
+ const placeholder = el.getAttribute('placeholder');
593
+ const title = el.getAttribute('title');
594
+ const hasLabel = id
595
+ ? this.root.querySelectorAll(`label[for="${id}"]`).length > 0
596
+ : false;
597
+ const wrappedInLabel = el.closest('label') !== null;
598
+ if (!hasLabel &&
599
+ !wrappedInLabel &&
600
+ !ariaLabel &&
601
+ !ariaLabelledBy &&
602
+ !title &&
603
+ !placeholder) {
604
+ inputsWithoutLabel++;
605
+ }
606
+ });
607
+ const iframesWithoutTitle = this.root.querySelectorAll('iframe:not([title])').length;
608
+ let tablesWithoutCaption = 0;
609
+ this.root.querySelectorAll('table').forEach((el) => {
610
+ const hasCaption = el.querySelectorAll('caption').length > 0;
611
+ const ariaLabel = el.getAttribute('aria-label');
612
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
613
+ const role = el.getAttribute('role');
614
+ if (role === 'presentation' || role === 'none')
615
+ return;
616
+ const hasHeaders = el.querySelectorAll('th').length > 0;
617
+ if (hasHeaders && !hasCaption && !ariaLabel && !ariaLabelledBy) {
618
+ tablesWithoutCaption++;
619
+ }
620
+ });
621
+ let svgsWithoutTitle = 0;
622
+ this.root.querySelectorAll('svg').forEach((el) => {
623
+ const hasTitle = el.querySelectorAll('title').length > 0;
624
+ const ariaLabel = el.getAttribute('aria-label');
625
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
626
+ const ariaHidden = el.getAttribute('aria-hidden');
627
+ const role = el.getAttribute('role');
628
+ if (ariaHidden === 'true' || role === 'presentation' || role === 'none')
629
+ return;
630
+ if (!hasTitle && !ariaLabel && !ariaLabelledBy) {
631
+ svgsWithoutTitle++;
632
+ }
633
+ });
634
+ return {
635
+ buttonsWithoutAriaLabel,
636
+ linksWithoutAriaLabel,
637
+ inputsWithoutLabel,
638
+ iframesWithoutTitle,
639
+ tablesWithoutCaption,
640
+ svgsWithoutTitle,
641
+ };
642
+ }
643
+ analyzeStructuralHtml() {
644
+ return {
645
+ hasHeader: this.root.querySelectorAll('header').length > 0,
646
+ hasNav: this.root.querySelectorAll('nav').length > 0,
647
+ hasMain: this.root.querySelectorAll('main').length > 0,
648
+ hasArticle: this.root.querySelectorAll('article').length > 0,
649
+ hasSection: this.root.querySelectorAll('section').length > 0,
650
+ hasFooter: this.root.querySelectorAll('footer').length > 0,
651
+ };
652
+ }
653
+ analyzeBreadcrumbs(jsonLdTypes) {
654
+ const hasBreadcrumbsHtml = this.root.querySelectorAll('nav[aria-label="breadcrumb"], .breadcrumb, .breadcrumbs').length > 0;
655
+ const hasBreadcrumbsSchema = jsonLdTypes.includes('BreadcrumbList');
656
+ return { hasBreadcrumbsHtml, hasBreadcrumbsSchema };
657
+ }
658
+ analyzeMultimedia() {
659
+ const videos = this.root.querySelectorAll('video');
660
+ const audios = this.root.querySelectorAll('audio');
661
+ let hasAutoplay = false;
662
+ const checkAutoplay = (list) => {
663
+ list.forEach(el => {
664
+ if (el.getAttribute('autoplay') !== undefined || el.hasAttribute('autoplay')) {
665
+ hasAutoplay = true;
666
+ }
667
+ });
668
+ };
669
+ checkAutoplay(videos);
670
+ checkAutoplay(audios);
671
+ return {
672
+ videoCount: videos.length,
673
+ audioCount: audios.length,
674
+ hasAutoplay
675
+ };
676
+ }
677
+ analyzeSocialLinks(links, socialDomains) {
678
+ const socialLinks = links.filter(l => socialDomains.some(d => l.href.toLowerCase().includes(d)));
679
+ const platformMap = {
680
+ 'facebook.com': 'facebook',
681
+ 'twitter.com': 'twitter',
682
+ 'x.com': 'twitter',
683
+ 'instagram.com': 'instagram',
684
+ 'linkedin.com': 'linkedin',
685
+ 'youtube.com': 'youtube',
686
+ 'pinterest.com': 'pinterest',
687
+ 'tiktok.com': 'tiktok',
688
+ 'github.com': 'github',
689
+ 'reddit.com': 'reddit',
690
+ 'snapchat.com': 'snapchat',
691
+ 'whatsapp.com': 'whatsapp',
692
+ 'telegram.org': 'telegram',
693
+ 'discord.com': 'discord',
694
+ 'threads.net': 'threads',
695
+ };
696
+ const socialLinkDetails = [];
697
+ let socialLinksInHeader = 0;
698
+ let socialLinksInFooter = 0;
699
+ let socialLinksWithoutAccessibility = 0;
700
+ let socialLinksWithoutNewTab = 0;
701
+ let socialLinksWithoutNoopener = 0;
702
+ const platformsFound = new Set();
703
+ const headerLinks = this.root.querySelectorAll('header a[href]');
704
+ const footerLinks = this.root.querySelectorAll('footer a[href]');
705
+ const headerHrefs = new Set(headerLinks.map(l => l.getAttribute('href') || ''));
706
+ const footerHrefs = new Set(footerLinks.map(l => l.getAttribute('href') || ''));
707
+ for (const link of socialLinks) {
708
+ let platform = 'unknown';
709
+ for (const [domain, name] of Object.entries(platformMap)) {
710
+ if (link.href.toLowerCase().includes(domain)) {
711
+ platform = name;
712
+ platformsFound.add(name);
713
+ break;
714
+ }
715
+ }
716
+ let location = 'body';
717
+ if (headerHrefs.has(link.href)) {
718
+ location = 'header';
719
+ socialLinksInHeader++;
720
+ }
721
+ else if (footerHrefs.has(link.href)) {
722
+ location = 'footer';
723
+ socialLinksInFooter++;
724
+ }
725
+ const hasAccessibility = !!(link.ariaLabel || link.title || (link.text && link.text.trim()));
726
+ if (!hasAccessibility) {
727
+ socialLinksWithoutAccessibility++;
728
+ }
729
+ const hasNewTab = link.target === '_blank';
730
+ if (!hasNewTab) {
731
+ socialLinksWithoutNewTab++;
732
+ }
733
+ const hasNoopener = !!(link.rel && link.rel.includes('noopener'));
734
+ if (hasNewTab && !hasNoopener) {
735
+ socialLinksWithoutNoopener++;
736
+ }
737
+ socialLinkDetails.push({
738
+ href: link.href,
739
+ platform,
740
+ hasAccessibility,
741
+ hasNewTab,
742
+ hasNoopener,
743
+ location,
744
+ });
745
+ }
746
+ return {
747
+ totalSocialLinks: socialLinks.length,
748
+ socialLinksInHeader,
749
+ socialLinksInFooter,
750
+ socialLinksWithoutAccessibility,
751
+ socialLinksWithoutNewTab,
752
+ socialLinksWithoutNoopener,
753
+ platformsFound: Array.from(platformsFound),
754
+ socialLinkDetails,
755
+ };
756
+ }
757
+ analyzeTrustSignals(links) {
758
+ const linkHrefs = links.map((l) => l.href.toLowerCase());
759
+ return {
760
+ hasAboutPageLink: linkHrefs.some((href) => href.includes('about') || href.includes('quem-somos')),
761
+ hasContactPageLink: linkHrefs.some((href) => href.includes('contact') || href.includes('contato')),
762
+ hasPrivacyPolicyLink: linkHrefs.some((href) => href.includes('privacy') || href.includes('privacidade')),
763
+ hasTermsOfServiceLink: linkHrefs.some((href) => href.includes('terms') || href.includes('termos-de-uso')),
764
+ };
765
+ }
766
+ analyzeAnalytics() {
767
+ const providers = [];
768
+ const html = this.root.innerHTML || '';
769
+ const scripts = this.root.querySelectorAll('script');
770
+ const scriptSources = [];
771
+ const scriptContents = [];
772
+ scripts.forEach((s) => {
773
+ const src = s.getAttribute('src') || '';
774
+ const content = s.innerHTML || '';
775
+ if (src)
776
+ scriptSources.push(src.toLowerCase());
777
+ if (content)
778
+ scriptContents.push(content.toLowerCase());
779
+ });
780
+ const allScriptText = scriptSources.join(' ') + ' ' + scriptContents.join(' ');
781
+ if (allScriptText.includes('gtag') && allScriptText.includes('g-')) {
782
+ providers.push('Google Analytics 4 (GA4)');
783
+ }
784
+ if (allScriptText.includes('analytics.js') || allScriptText.includes('ga.js') ||
785
+ (allScriptText.includes('ua-') && !allScriptText.includes('g-'))) {
786
+ providers.push('Universal Analytics (UA)');
787
+ }
788
+ if (allScriptText.includes('googletagmanager.com/gtm') || allScriptText.includes('gtm-')) {
789
+ providers.push('Google Tag Manager');
790
+ }
791
+ if (allScriptText.includes('hotjar.com') || allScriptText.includes('hj(')) {
792
+ providers.push('Hotjar');
793
+ }
794
+ if (allScriptText.includes('clarity.ms') || allScriptText.includes('clarity(')) {
795
+ providers.push('Microsoft Clarity');
796
+ }
797
+ if (allScriptText.includes('fullstory.com') || allScriptText.includes('fs.identify')) {
798
+ providers.push('FullStory');
799
+ }
800
+ if (allScriptText.includes('luckyorange.com')) {
801
+ providers.push('Lucky Orange');
802
+ }
803
+ if (allScriptText.includes('crazyegg.com')) {
804
+ providers.push('Crazy Egg');
805
+ }
806
+ if (allScriptText.includes('mixpanel.com') || allScriptText.includes('mixpanel.init')) {
807
+ providers.push('Mixpanel');
808
+ }
809
+ if (allScriptText.includes('heap.io') || allScriptText.includes('heapanalytics')) {
810
+ providers.push('Heap');
811
+ }
812
+ if (allScriptText.includes('amplitude.com') || allScriptText.includes('amplitude.init')) {
813
+ providers.push('Amplitude');
814
+ }
815
+ if (allScriptText.includes('segment.com') || allScriptText.includes('analytics.load')) {
816
+ providers.push('Segment');
817
+ }
818
+ if (allScriptText.includes('plausible.io')) {
819
+ providers.push('Plausible');
820
+ }
821
+ if (allScriptText.includes('matomo') || allScriptText.includes('piwik')) {
822
+ providers.push('Matomo');
823
+ }
824
+ if (allScriptText.includes('posthog.com') || allScriptText.includes('posthog.init')) {
825
+ providers.push('PostHog');
826
+ }
827
+ if (allScriptText.includes('connect.facebook.net') || allScriptText.includes('fbq(')) {
828
+ providers.push('Facebook Pixel');
829
+ }
830
+ if (allScriptText.includes('snap.licdn.com') || allScriptText.includes('_linkedin_partner_id')) {
831
+ providers.push('LinkedIn Insight');
832
+ }
833
+ if (allScriptText.includes('static.ads-twitter.com') || allScriptText.includes('twq(')) {
834
+ providers.push('Twitter Pixel');
835
+ }
836
+ if (allScriptText.includes('pintrk(') || allScriptText.includes('pinterest.com/ct')) {
837
+ providers.push('Pinterest Tag');
838
+ }
839
+ return {
840
+ analyticsDetected: providers.length > 0,
841
+ analyticsProviders: providers,
842
+ };
843
+ }
844
+ analyzeFeeds() {
845
+ let hasRssFeed = false;
846
+ let rssFeedUrl;
847
+ let hasAtomFeed = false;
848
+ let atomFeedUrl;
849
+ const rssLink = this.root.querySelector('link[type="application/rss+xml"]');
850
+ if (rssLink) {
851
+ hasRssFeed = true;
852
+ rssFeedUrl = rssLink.getAttribute('href') || undefined;
853
+ }
854
+ const atomLink = this.root.querySelector('link[type="application/atom+xml"]');
855
+ if (atomLink) {
856
+ hasAtomFeed = true;
857
+ atomFeedUrl = atomLink.getAttribute('href') || undefined;
858
+ }
859
+ return { hasRssFeed, rssFeedUrl, hasAtomFeed, atomFeedUrl };
860
+ }
861
+ analyzeConversionElements(links, visibleText) {
862
+ const formCount = this.root.querySelectorAll('form').length;
863
+ const ctaPatterns = [
864
+ /^(get started|start|begin|try|sign up|register|subscribe|join|buy|purchase|order|add to cart|checkout|download|contact|request|schedule|book|reserve|learn more|read more|discover|explore|view|see|watch|listen|play|submit|send|apply|claim|grab|unlock|access)$/i,
865
+ /^(obter|começar|iniciar|experimentar|inscrever|registrar|assinar|entrar|comprar|pedir|adicionar|finalizar|baixar|contato|solicitar|agendar|reservar|saber mais|ler mais|descobrir|explorar|ver|assistir|ouvir|enviar|aplicar|reivindicar|acessar)$/i,
866
+ ];
867
+ let ctaButtonsCount = 0;
868
+ this.root.querySelectorAll('button').forEach((btn) => {
869
+ const text = btn.text.trim().toLowerCase();
870
+ if (ctaPatterns.some(p => p.test(text))) {
871
+ ctaButtonsCount++;
872
+ }
873
+ });
874
+ this.root.querySelectorAll('a[class*="btn"], a[class*="button"], a[role="button"]').forEach((link) => {
875
+ const text = link.text.trim().toLowerCase();
876
+ if (ctaPatterns.some(p => p.test(text))) {
877
+ ctaButtonsCount++;
878
+ }
879
+ });
880
+ const hasWhatsAppLink = links.some(l => l.href.includes('wa.me') ||
881
+ l.href.includes('whatsapp.com') ||
882
+ l.href.includes('api.whatsapp.com'));
883
+ const phoneRegex = /(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,4}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}/g;
884
+ const hasPhoneOnPage = phoneRegex.test(visibleText) ||
885
+ links.some(l => l.href.startsWith('tel:'));
886
+ return {
887
+ ctaButtonsCount,
888
+ formCount,
889
+ hasWhatsAppLink,
890
+ hasPhoneOnPage,
891
+ };
892
+ }
893
+ analyzeAdvancedImages() {
894
+ let imagesWithSrcset = 0;
895
+ let largeBase64ImagesCount = 0;
896
+ const imgs = this.root.querySelectorAll('img');
897
+ imgs.forEach((img) => {
898
+ if (img.getAttribute('srcset') || (img.parentNode && img.parentNode.tagName === 'PICTURE')) {
899
+ imagesWithSrcset++;
900
+ }
901
+ const src = img.getAttribute('src') || '';
902
+ if (src.startsWith('data:image') && src.length > 5 * 1024) {
903
+ largeBase64ImagesCount++;
904
+ }
905
+ });
906
+ return { imagesWithSrcset, largeBase64ImagesCount };
907
+ }
908
+ calculateTextHtmlRatio(bodyTextLength) {
909
+ const htmlSize = this.root.innerHTML?.length;
910
+ if (htmlSize && htmlSize > 0) {
911
+ return (bodyTextLength / htmlSize) * 100;
912
+ }
913
+ return undefined;
914
+ }
915
+ convertToCheckResults(results) {
916
+ return results.map((r) => ({
917
+ name: r.name,
918
+ category: r.category,
919
+ status: r.status,
920
+ message: r.message,
921
+ value: r.value,
922
+ recommendation: r.recommendation,
923
+ evidence: r.evidence,
924
+ }));
925
+ }
926
+ buildSummary(ruleResults, checks, data) {
927
+ const passed = checks.filter((c) => c.status === 'pass').length;
928
+ const warnings = checks.filter((c) => c.status === 'warn').length;
929
+ const errors = checks.filter((c) => c.status === 'fail').length;
930
+ const infos = checks.filter((c) => c.status === 'info').length;
931
+ const totalChecks = checks.length;
932
+ const scoringChecks = totalChecks - infos;
933
+ const passRate = scoringChecks > 0 ? Math.round((passed / scoringChecks) * 100) : 100;
934
+ const issuesByCategory = {};
935
+ for (const result of ruleResults) {
936
+ const cat = result.category;
937
+ if (!issuesByCategory[cat]) {
938
+ issuesByCategory[cat] = { passed: 0, warnings: 0, errors: 0 };
939
+ }
940
+ if (result.status === 'pass')
941
+ issuesByCategory[cat].passed++;
942
+ else if (result.status === 'warn')
943
+ issuesByCategory[cat].warnings++;
944
+ else if (result.status === 'fail')
945
+ issuesByCategory[cat].errors++;
946
+ }
947
+ const topIssues = ruleResults
948
+ .filter((r) => r.status === 'fail' || r.status === 'warn')
949
+ .sort((a, b) => {
950
+ if (a.status === 'fail' && b.status !== 'fail')
951
+ return -1;
952
+ if (a.status !== 'fail' && b.status === 'fail')
953
+ return 1;
954
+ return 0;
955
+ })
956
+ .slice(0, 5)
957
+ .map((r) => ({
958
+ name: r.name,
959
+ message: r.message,
960
+ category: r.category,
961
+ severity: (r.status === 'fail' ? 'error' : 'warning'),
962
+ }));
963
+ const quickWins = [];
964
+ if (!data.meta.title)
965
+ quickWins.push('Add a page title');
966
+ if (!data.meta.description)
967
+ quickWins.push('Add a meta description');
968
+ if (!data.og.title && !data.og.description)
969
+ quickWins.push('Add OpenGraph meta tags for social sharing');
970
+ if (!data.twitter.card)
971
+ quickWins.push('Add Twitter Card meta tags');
972
+ if (!data.technical.hasCanonical)
973
+ quickWins.push('Add a canonical URL');
974
+ if (!data.technical.hasLang)
975
+ quickWins.push('Add lang attribute to <html>');
976
+ if (data.imageAnalysis.withoutAlt > 0)
977
+ quickWins.push(`Add alt text to ${data.imageAnalysis.withoutAlt} image(s)`);
978
+ if (data.linkAnalysis.withoutText > 0)
979
+ quickWins.push(`Add text to ${data.linkAnalysis.withoutText} empty link(s)`);
980
+ const limitedQuickWins = quickWins.slice(0, 5);
981
+ const htmlSize = this.root.innerHTML?.length;
982
+ const domElements = this.root.querySelectorAll('*').length;
983
+ const vitals = {
984
+ htmlSize,
985
+ domElements,
986
+ ttfb: this.options.responseHeaders ? undefined : undefined,
987
+ totalTime: undefined,
988
+ wordCount: data.content.wordCount,
989
+ readingTime: data.content.readingTimeMinutes,
990
+ imageCount: data.imageAnalysis.total,
991
+ linkCount: data.linkAnalysis.total,
992
+ };
993
+ const completeness = {
994
+ meta: this.calculateMetaCompleteness(data.meta),
995
+ social: this.calculateSocialCompleteness(data.og, data.twitter),
996
+ technical: this.calculateTechnicalCompleteness(data.technical),
997
+ content: this.calculateContentCompleteness(data.content),
998
+ images: this.calculateImageCompleteness(data.imageAnalysis),
999
+ links: this.calculateLinkCompleteness(data.linkAnalysis),
1000
+ };
1001
+ return {
1002
+ totalChecks,
1003
+ passed,
1004
+ warnings,
1005
+ errors,
1006
+ infos,
1007
+ passRate,
1008
+ issuesByCategory,
1009
+ topIssues,
1010
+ quickWins: limitedQuickWins,
1011
+ vitals,
1012
+ completeness,
1013
+ };
1014
+ }
1015
+ calculateMetaCompleteness(meta) {
1016
+ let score = 0;
1017
+ const total = 5;
1018
+ if (meta.title)
1019
+ score++;
1020
+ if (meta.description)
1021
+ score++;
1022
+ if (meta.canonical)
1023
+ score++;
1024
+ if (meta.viewport)
1025
+ score++;
1026
+ if (meta.charset)
1027
+ score++;
1028
+ return Math.round((score / total) * 100);
1029
+ }
1030
+ calculateSocialCompleteness(og, twitter) {
1031
+ let score = 0;
1032
+ const total = 8;
1033
+ if (og.title)
1034
+ score++;
1035
+ if (og.description)
1036
+ score++;
1037
+ if (og.image)
1038
+ score++;
1039
+ if (og.url)
1040
+ score++;
1041
+ if (twitter.card)
1042
+ score++;
1043
+ if (twitter.title)
1044
+ score++;
1045
+ if (twitter.description)
1046
+ score++;
1047
+ if (twitter.image)
1048
+ score++;
1049
+ return Math.round((score / total) * 100);
1050
+ }
1051
+ calculateTechnicalCompleteness(technical) {
1052
+ let score = 0;
1053
+ const total = 5;
1054
+ if (technical.hasCanonical)
1055
+ score++;
1056
+ if (technical.hasViewport)
1057
+ score++;
1058
+ if (technical.hasCharset)
1059
+ score++;
1060
+ if (technical.hasLang)
1061
+ score++;
1062
+ if (technical.hasRobotsMeta)
1063
+ score++;
1064
+ return Math.round((score / total) * 100);
1065
+ }
1066
+ calculateContentCompleteness(content) {
1067
+ let score = 0;
1068
+ const total = 5;
1069
+ if (content.wordCount >= 300)
1070
+ score++;
1071
+ if (content.paragraphCount >= 3)
1072
+ score++;
1073
+ if (content.listCount > 0)
1074
+ score++;
1075
+ if (content.readingTimeMinutes >= 1)
1076
+ score++;
1077
+ if (content.strongTagCount > 0 || content.emTagCount > 0)
1078
+ score++;
1079
+ return Math.round((score / total) * 100);
1080
+ }
1081
+ calculateImageCompleteness(images) {
1082
+ if (images.total === 0)
1083
+ return 100;
1084
+ let score = 0;
1085
+ const total = 4;
1086
+ if (images.withoutAlt === 0)
1087
+ score++;
1088
+ if (images.lazy > 0)
1089
+ score++;
1090
+ if (images.missingDimensions === 0)
1091
+ score++;
1092
+ if (images.modernFormats > 0)
1093
+ score++;
1094
+ return Math.round((score / total) * 100);
1095
+ }
1096
+ calculateLinkCompleteness(links) {
1097
+ if (links.total === 0)
1098
+ return 100;
1099
+ let score = 0;
1100
+ const total = 4;
1101
+ if (links.internal > 0)
1102
+ score++;
1103
+ if (links.external > 0)
1104
+ score++;
1105
+ if (links.withoutText === 0)
1106
+ score++;
1107
+ if (links.broken === 0)
1108
+ score++;
1109
+ return Math.round((score / total) * 100);
1110
+ }
1111
+ analyzeHeadings() {
1112
+ const issues = [];
1113
+ const structure = [];
1114
+ const sectionWordCounts = [];
1115
+ const counts = {
1116
+ 1: 0,
1117
+ 2: 0,
1118
+ 3: 0,
1119
+ 4: 0,
1120
+ 5: 0,
1121
+ 6: 0,
1122
+ };
1123
+ let currentSectionWordCount = 0;
1124
+ let inSection = false;
1125
+ const body = this.getMainBody();
1126
+ const allElements = body ? body.querySelectorAll('*') : [];
1127
+ allElements.forEach((el) => {
1128
+ const tagName = el.tagName.toLowerCase();
1129
+ if (tagName.match(/^h[1-6]$/)) {
1130
+ const level = parseInt(tagName.substring(1), 10);
1131
+ const text = el.text.trim();
1132
+ counts[level] = (counts[level] || 0) + 1;
1133
+ structure.push({ level, text: text.slice(0, 80), count: 1 });
1134
+ if (level === 2) {
1135
+ if (inSection) {
1136
+ sectionWordCounts.push(currentSectionWordCount);
1137
+ }
1138
+ currentSectionWordCount = 0;
1139
+ inSection = true;
1140
+ }
1141
+ }
1142
+ else if (inSection &&
1143
+ ['p', 'ul', 'ol', 'div', 'article', 'section'].includes(tagName)) {
1144
+ let text = '';
1145
+ el.childNodes.forEach(n => {
1146
+ if (n.nodeType === 3)
1147
+ text += n.text;
1148
+ });
1149
+ text = text.trim();
1150
+ if (text.length > 0) {
1151
+ currentSectionWordCount += text.split(/\s+/).length;
1152
+ }
1153
+ }
1154
+ });
1155
+ if (inSection) {
1156
+ sectionWordCounts.push(currentSectionWordCount);
1157
+ }
1158
+ let hasProperHierarchy = true;
1159
+ let prevLevel = 0;
1160
+ for (const heading of structure) {
1161
+ if (heading.level > prevLevel + 1 && prevLevel !== 0) {
1162
+ hasProperHierarchy = false;
1163
+ issues.push(`Skipped heading level: H${prevLevel} to H${heading.level}`);
1164
+ }
1165
+ prevLevel = heading.level;
1166
+ }
1167
+ if (counts[1] === 0) {
1168
+ issues.push('No H1 tag found');
1169
+ }
1170
+ else if (counts[1] > 1) {
1171
+ issues.push('Multiple H1 tags');
1172
+ }
1173
+ return {
1174
+ structure,
1175
+ h1Count: counts[1],
1176
+ hasProperHierarchy,
1177
+ issues,
1178
+ sectionWordCounts,
1179
+ };
1180
+ }
1181
+ analyzeContent(headings) {
1182
+ const body = this.getMainBody();
1183
+ const bodyText = body ? body.text.replace(/\s+/g, ' ').trim() : '';
1184
+ const words = bodyText.split(/\s+/).filter((w) => w.length > 0);
1185
+ const sentences = bodyText
1186
+ .split(/[.!?]+/)
1187
+ .filter((s) => s.trim().length > 0);
1188
+ const paragraphs = this.root.querySelectorAll('p');
1189
+ let totalParagraphLength = 0;
1190
+ const paragraphWordCounts = [];
1191
+ paragraphs.forEach((el) => {
1192
+ const text = el.text.trim();
1193
+ totalParagraphLength += text.length;
1194
+ const pWords = text.split(/\s+/).filter((w) => w.length > 0).length;
1195
+ if (pWords > 0)
1196
+ paragraphWordCounts.push(pWords);
1197
+ });
1198
+ const wordCount = words.length;
1199
+ const readingTimeMinutes = Math.ceil(wordCount / 200);
1200
+ const avgWordsPerSentence = sentences.length > 0 ? Math.round(wordCount / sentences.length) : 0;
1201
+ let faqCount = 0;
1202
+ headings.structure.forEach((h) => {
1203
+ if (h.level === 3 &&
1204
+ /^(what|how|why|when|where|who|can|do|is|are)\b/i.test(h.text)) {
1205
+ faqCount++;
1206
+ }
1207
+ });
1208
+ const imageCount = this.root.querySelectorAll('img').length;
1209
+ const imagePerWordRatio = wordCount > 0 ? imageCount / wordCount : 0;
1210
+ const fleschReadingEase = undefined;
1211
+ const hasQuestionHeadings = headings.structure.some((h) => (h.level === 2 || h.level === 3) &&
1212
+ /^(what|how|why|when|where|who|can|do|is|are)\b/i.test(h.text));
1213
+ return {
1214
+ wordCount,
1215
+ characterCount: bodyText.length,
1216
+ sentenceCount: sentences.length,
1217
+ paragraphCount: paragraphs.length,
1218
+ readingTimeMinutes,
1219
+ avgWordsPerSentence,
1220
+ avgParagraphLength: paragraphs.length > 0
1221
+ ? Math.round(totalParagraphLength / paragraphs.length)
1222
+ : 0,
1223
+ listCount: this.root.querySelectorAll('ul, ol').length,
1224
+ strongTagCount: this.root.querySelectorAll('strong').length,
1225
+ emTagCount: this.root.querySelectorAll('em').length,
1226
+ paragraphWordCounts,
1227
+ avgSentenceLength: avgWordsPerSentence,
1228
+ faqCount,
1229
+ imagePerWordRatio,
1230
+ fleschReadingEase,
1231
+ hasQuestionHeadings,
1232
+ };
1233
+ }
1234
+ buildLinkAnalysis(links) {
1235
+ const internalHttpLinkUrls = links
1236
+ .filter((l) => l.type === 'internal' && l.href.startsWith('http://'))
1237
+ .map((l) => l.href);
1238
+ return {
1239
+ total: links.length,
1240
+ internal: links.filter((l) => l.type === 'internal').length,
1241
+ external: links.filter((l) => l.type === 'external').length,
1242
+ nofollow: links.filter((l) => l.rel?.includes('nofollow')).length,
1243
+ sponsoredLinks: links.filter((l) => l.rel?.includes('sponsored')).length,
1244
+ ugcLinks: links.filter((l) => l.rel?.includes('ugc')).length,
1245
+ broken: 0,
1246
+ withoutText: links.filter((l) => !l.text?.trim()).length,
1247
+ internalHttpLinks: internalHttpLinkUrls.length,
1248
+ internalHttpLinkUrls,
1249
+ };
1250
+ }
1251
+ buildImageAnalysis(images) {
1252
+ return {
1253
+ total: images.length,
1254
+ withAlt: images.filter((i) => i.alt && i.alt.trim().length > 0).length,
1255
+ withoutAlt: images.filter((i) => !i.alt || i.alt.trim().length === 0).length,
1256
+ lazy: images.filter((i) => i.loading === 'lazy').length,
1257
+ missingDimensions: images.filter((i) => !i.width || !i.height).length,
1258
+ modernFormats: images.filter((i) => /\.(webp|avif)$/i.test(i.src))
1259
+ .length,
1260
+ altTextLengths: images.filter((i) => i.alt).map((i) => i.alt.length),
1261
+ imageAltTexts: images.filter((i) => i.alt).map((i) => i.alt.toLowerCase()),
1262
+ imageFilenames: images
1263
+ .map((i) => {
1264
+ try {
1265
+ const url = new URL(i.src);
1266
+ return url.pathname.split('/').pop() || '';
1267
+ }
1268
+ catch {
1269
+ return '';
1270
+ }
1271
+ })
1272
+ .filter(Boolean),
1273
+ imagesWithAsyncDecoding: images.filter((i) => i.decoding === 'async')
1274
+ .length,
1275
+ };
1276
+ }
1277
+ buildSocialAnalysis(og, twitter) {
1278
+ const ogIssues = [];
1279
+ const twitterIssues = [];
1280
+ const hasOg = !!(og.title || og.description || og.image);
1281
+ if (!hasOg) {
1282
+ ogIssues.push('No OpenGraph meta tags found');
1283
+ }
1284
+ else {
1285
+ if (!og.title)
1286
+ ogIssues.push('Missing og:title');
1287
+ if (!og.description)
1288
+ ogIssues.push('Missing og:description');
1289
+ if (!og.image)
1290
+ ogIssues.push('Missing og:image');
1291
+ if (!og.url)
1292
+ ogIssues.push('Missing og:url');
1293
+ }
1294
+ const hasTwitter = !!(twitter.card || twitter.title || twitter.description);
1295
+ if (!hasTwitter) {
1296
+ twitterIssues.push('No Twitter Card meta tags found');
1297
+ }
1298
+ else {
1299
+ if (!twitter.card)
1300
+ twitterIssues.push('Missing twitter:card');
1301
+ if (!twitter.title)
1302
+ twitterIssues.push('Missing twitter:title');
1303
+ if (!twitter.description)
1304
+ twitterIssues.push('Missing twitter:description');
1305
+ }
1306
+ return {
1307
+ openGraph: {
1308
+ present: hasOg,
1309
+ hasTitle: !!og.title,
1310
+ hasDescription: !!og.description,
1311
+ hasImage: !!og.image,
1312
+ hasUrl: !!og.url,
1313
+ issues: ogIssues,
1314
+ },
1315
+ twitterCard: {
1316
+ present: hasTwitter,
1317
+ hasCard: !!twitter.card,
1318
+ hasTitle: !!twitter.title,
1319
+ hasDescription: !!twitter.description,
1320
+ hasImage: !!twitter.image,
1321
+ issues: twitterIssues,
1322
+ },
1323
+ };
1324
+ }
1325
+ buildTechnicalAnalysis(meta) {
1326
+ const html = this.root.querySelector('html');
1327
+ const htmlLang = html ? html.getAttribute('lang') : undefined;
1328
+ return {
1329
+ hasCanonical: !!meta.canonical,
1330
+ canonicalUrl: meta.canonical,
1331
+ hasRobotsMeta: !!meta.robots,
1332
+ robotsContent: meta.robots,
1333
+ hasViewport: !!meta.viewport,
1334
+ hasCharset: !!meta.charset,
1335
+ hasLang: !!htmlLang,
1336
+ langValue: htmlLang,
1337
+ };
1338
+ }
1339
+ analyzeResources() {
1340
+ const scripts = this.root.querySelectorAll('script[src]');
1341
+ const styles = this.root.querySelectorAll('link[rel="stylesheet"]');
1342
+ const unminified = [];
1343
+ scripts.forEach((s) => {
1344
+ const src = s.getAttribute('src');
1345
+ if (src && !src.includes('.min.') && !src.includes('cdn'))
1346
+ unminified.push(src);
1347
+ });
1348
+ styles.forEach((s) => {
1349
+ const href = s.getAttribute('href');
1350
+ if (href && !href.includes('.min.') && !href.includes('cdn'))
1351
+ unminified.push(href);
1352
+ });
1353
+ return {
1354
+ jsFilesCount: scripts.length,
1355
+ cssFilesCount: styles.length,
1356
+ unminifiedResources: unminified.length,
1357
+ unminifiedResourceUrls: unminified
1358
+ };
1359
+ }
1360
+ calculateScore(checks) {
1361
+ const weights = {
1362
+ pass: 100,
1363
+ warn: 50,
1364
+ fail: 0,
1365
+ info: 100,
1366
+ };
1367
+ const scoringChecks = checks.filter((c) => c.status !== 'info');
1368
+ if (scoringChecks.length === 0)
1369
+ return { score: 100, grade: 'A' };
1370
+ const totalWeight = scoringChecks.reduce((sum, check) => sum + weights[check.status], 0);
1371
+ const score = Math.round(totalWeight / scoringChecks.length);
1372
+ let grade;
1373
+ if (score >= 90)
1374
+ grade = 'A';
1375
+ else if (score >= 80)
1376
+ grade = 'B';
1377
+ else if (score >= 70)
1378
+ grade = 'C';
1379
+ else if (score >= 60)
1380
+ grade = 'D';
1381
+ else
1382
+ grade = 'F';
1383
+ return { score, grade };
1384
+ }
1385
+ getRules() {
1386
+ return this.rulesEngine.getRules();
1387
+ }
1388
+ getRulesByCategory(category) {
1389
+ return this.rulesEngine.getRulesByCategory(category);
1390
+ }
1391
+ getCategories() {
1392
+ return this.rulesEngine.getCategories();
1393
+ }
1394
+ }
1395
+ export async function analyzeSeo(html, options = {}) {
1396
+ const analyzer = await SeoAnalyzer.fromHtml(html, options);
1397
+ return analyzer.analyze();
1398
+ }
1399
+ export { SEO_THRESHOLDS, createRulesEngine, SeoRulesEngine, } from './rules/index.js';