recker 1.0.42 → 1.0.43-next.7a08602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. package/dist/bin/recker-linux-x64 +0 -0
  2. package/dist/bin/recker-macos-x64 +0 -0
  3. package/dist/bin/recker-win-x64.exe +0 -0
  4. package/dist/bin/rek.cjs +93958 -0
  5. package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
  6. package/dist/browser/ai/adaptive-timeout.js +208 -0
  7. package/dist/browser/ai/client.d.ts +22 -0
  8. package/dist/browser/ai/client.js +294 -0
  9. package/dist/browser/ai/index.d.ts +14 -0
  10. package/dist/browser/ai/index.js +11 -0
  11. package/dist/browser/ai/providers/anthropic.d.ts +63 -0
  12. package/dist/browser/ai/providers/anthropic.js +370 -0
  13. package/dist/browser/ai/providers/base.d.ts +48 -0
  14. package/dist/browser/ai/providers/base.js +150 -0
  15. package/dist/browser/ai/providers/google.d.ts +59 -0
  16. package/dist/browser/ai/providers/google.js +305 -0
  17. package/dist/browser/ai/providers/ollama.d.ts +44 -0
  18. package/dist/browser/ai/providers/ollama.js +240 -0
  19. package/dist/browser/ai/providers/openai.d.ts +64 -0
  20. package/dist/browser/ai/providers/openai.js +298 -0
  21. package/dist/browser/ai/rate-limiter.d.ts +43 -0
  22. package/dist/browser/ai/rate-limiter.js +215 -0
  23. package/dist/browser/ai/vector/index.d.ts +2 -0
  24. package/dist/browser/ai/vector/index.js +2 -0
  25. package/dist/browser/ai/vector/similarity.d.ts +2 -0
  26. package/dist/browser/ai/vector/similarity.js +27 -0
  27. package/dist/browser/ai/vector/store.d.ts +27 -0
  28. package/dist/browser/ai/vector/store.js +82 -0
  29. package/dist/browser/browser/cache.d.ts +2 -40
  30. package/dist/browser/browser/cache.js +2 -199
  31. package/dist/browser/browser/index.d.ts +8 -0
  32. package/dist/browser/browser/index.js +8 -0
  33. package/dist/browser/browser/recker.d.ts +8 -1
  34. package/dist/browser/browser/recker.js +8 -2
  35. package/dist/browser/cache/indexed-db.d.ts +10 -0
  36. package/dist/browser/cache/indexed-db.js +88 -0
  37. package/dist/browser/cache/service-worker-cache.d.ts +18 -0
  38. package/dist/browser/cache/service-worker-cache.js +103 -0
  39. package/dist/browser/cache.d.ts +2 -40
  40. package/dist/browser/cache.js +2 -199
  41. package/dist/browser/constants/user-agents.d.ts +7 -0
  42. package/dist/browser/constants/user-agents.js +7 -0
  43. package/dist/browser/core/client.d.ts +2 -0
  44. package/dist/browser/core/client.js +19 -1
  45. package/dist/browser/index.d.ts +8 -0
  46. package/dist/browser/index.js +8 -0
  47. package/dist/browser/plugins/har-recorder.d.ts +40 -0
  48. package/dist/browser/plugins/har-recorder.js +120 -0
  49. package/dist/browser/plugins/network-simulation.d.ts +7 -0
  50. package/dist/browser/plugins/network-simulation.js +13 -0
  51. package/dist/browser/presets/android.d.ts +2 -0
  52. package/dist/browser/presets/android.js +16 -0
  53. package/dist/browser/presets/anthropic.d.ts +8 -0
  54. package/dist/browser/presets/anthropic.js +27 -0
  55. package/dist/browser/presets/aws.d.ts +19 -0
  56. package/dist/browser/presets/aws.js +68 -0
  57. package/dist/browser/presets/azure-openai.d.ts +10 -0
  58. package/dist/browser/presets/azure-openai.js +35 -0
  59. package/dist/browser/presets/azure.d.ts +41 -0
  60. package/dist/browser/presets/azure.js +104 -0
  61. package/dist/browser/presets/chaturbate.d.ts +2 -0
  62. package/dist/browser/presets/chaturbate.js +17 -0
  63. package/dist/browser/presets/cloudflare.d.ts +12 -0
  64. package/dist/browser/presets/cloudflare.js +39 -0
  65. package/dist/browser/presets/cohere.d.ts +7 -0
  66. package/dist/browser/presets/cohere.js +22 -0
  67. package/dist/browser/presets/deepseek.d.ts +7 -0
  68. package/dist/browser/presets/deepseek.js +22 -0
  69. package/dist/browser/presets/digitalocean.d.ts +5 -0
  70. package/dist/browser/presets/digitalocean.js +16 -0
  71. package/dist/browser/presets/discord.d.ts +6 -0
  72. package/dist/browser/presets/discord.js +17 -0
  73. package/dist/browser/presets/elevenlabs.d.ts +6 -0
  74. package/dist/browser/presets/elevenlabs.js +20 -0
  75. package/dist/browser/presets/enhancers.d.ts +20 -0
  76. package/dist/browser/presets/enhancers.js +85 -0
  77. package/dist/browser/presets/fireworks.d.ts +7 -0
  78. package/dist/browser/presets/fireworks.js +22 -0
  79. package/dist/browser/presets/gcp.d.ts +34 -0
  80. package/dist/browser/presets/gcp.js +91 -0
  81. package/dist/browser/presets/gemini.d.ts +7 -0
  82. package/dist/browser/presets/gemini.js +23 -0
  83. package/dist/browser/presets/github.d.ts +6 -0
  84. package/dist/browser/presets/github.js +17 -0
  85. package/dist/browser/presets/gitlab.d.ts +6 -0
  86. package/dist/browser/presets/gitlab.js +16 -0
  87. package/dist/browser/presets/groq.d.ts +7 -0
  88. package/dist/browser/presets/groq.js +22 -0
  89. package/dist/browser/presets/hubspot.d.ts +9 -0
  90. package/dist/browser/presets/hubspot.js +28 -0
  91. package/dist/browser/presets/huggingface.d.ts +7 -0
  92. package/dist/browser/presets/huggingface.js +23 -0
  93. package/dist/browser/presets/index.d.ts +47 -0
  94. package/dist/browser/presets/index.js +47 -0
  95. package/dist/browser/presets/ios.d.ts +2 -0
  96. package/dist/browser/presets/ios.js +13 -0
  97. package/dist/browser/presets/linear.d.ts +5 -0
  98. package/dist/browser/presets/linear.js +16 -0
  99. package/dist/browser/presets/mailgun.d.ts +7 -0
  100. package/dist/browser/presets/mailgun.js +20 -0
  101. package/dist/browser/presets/meta.d.ts +10 -0
  102. package/dist/browser/presets/meta.js +33 -0
  103. package/dist/browser/presets/mistral.d.ts +7 -0
  104. package/dist/browser/presets/mistral.js +22 -0
  105. package/dist/browser/presets/notion.d.ts +6 -0
  106. package/dist/browser/presets/notion.js +17 -0
  107. package/dist/browser/presets/openai.d.ts +9 -0
  108. package/dist/browser/presets/openai.js +30 -0
  109. package/dist/browser/presets/oracle.d.ts +19 -0
  110. package/dist/browser/presets/oracle.js +117 -0
  111. package/dist/browser/presets/perplexity.d.ts +7 -0
  112. package/dist/browser/presets/perplexity.js +22 -0
  113. package/dist/browser/presets/pinecone.d.ts +8 -0
  114. package/dist/browser/presets/pinecone.js +42 -0
  115. package/dist/browser/presets/registry.d.ts +23 -0
  116. package/dist/browser/presets/registry.js +519 -0
  117. package/dist/browser/presets/replicate.d.ts +7 -0
  118. package/dist/browser/presets/replicate.js +23 -0
  119. package/dist/browser/presets/sendgrid.d.ts +6 -0
  120. package/dist/browser/presets/sendgrid.js +20 -0
  121. package/dist/browser/presets/sentry.d.ts +11 -0
  122. package/dist/browser/presets/sentry.js +48 -0
  123. package/dist/browser/presets/sinch.d.ts +9 -0
  124. package/dist/browser/presets/sinch.js +39 -0
  125. package/dist/browser/presets/slack.d.ts +5 -0
  126. package/dist/browser/presets/slack.js +16 -0
  127. package/dist/browser/presets/square.d.ts +10 -0
  128. package/dist/browser/presets/square.js +33 -0
  129. package/dist/browser/presets/stripe.d.ts +7 -0
  130. package/dist/browser/presets/stripe.js +23 -0
  131. package/dist/browser/presets/supabase.d.ts +6 -0
  132. package/dist/browser/presets/supabase.js +18 -0
  133. package/dist/browser/presets/tiktok.d.ts +10 -0
  134. package/dist/browser/presets/tiktok.js +38 -0
  135. package/dist/browser/presets/together.d.ts +7 -0
  136. package/dist/browser/presets/together.js +22 -0
  137. package/dist/browser/presets/twilio.d.ts +6 -0
  138. package/dist/browser/presets/twilio.js +17 -0
  139. package/dist/browser/presets/vercel.d.ts +6 -0
  140. package/dist/browser/presets/vercel.js +23 -0
  141. package/dist/browser/presets/vultr.d.ts +5 -0
  142. package/dist/browser/presets/vultr.js +16 -0
  143. package/dist/browser/presets/xai.d.ts +8 -0
  144. package/dist/browser/presets/xai.js +23 -0
  145. package/dist/browser/presets/youtube.d.ts +5 -0
  146. package/dist/browser/presets/youtube.js +20 -0
  147. package/dist/browser/recker.d.ts +8 -1
  148. package/dist/browser/recker.js +8 -2
  149. package/dist/browser/scrape/document.d.ts +5 -4
  150. package/dist/browser/scrape/document.js +89 -76
  151. package/dist/browser/scrape/element.d.ts +10 -8
  152. package/dist/browser/scrape/element.js +295 -81
  153. package/dist/browser/scrape/extractors.d.ts +11 -11
  154. package/dist/browser/scrape/extractors.js +145 -113
  155. package/dist/browser/scrape/parser/back.d.ts +1 -0
  156. package/dist/browser/scrape/parser/back.js +3 -0
  157. package/dist/browser/scrape/parser/index.d.ts +20 -0
  158. package/dist/browser/scrape/parser/index.js +19 -0
  159. package/dist/browser/scrape/parser/matcher.d.ts +30 -0
  160. package/dist/browser/scrape/parser/matcher.js +99 -0
  161. package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
  162. package/dist/browser/scrape/parser/nodes/comment.js +21 -0
  163. package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
  164. package/dist/browser/scrape/parser/nodes/html.js +978 -0
  165. package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
  166. package/dist/browser/scrape/parser/nodes/node.js +31 -0
  167. package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
  168. package/dist/browser/scrape/parser/nodes/text.js +30 -0
  169. package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
  170. package/dist/browser/scrape/parser/nodes/type.js +7 -0
  171. package/dist/browser/scrape/parser/parse.d.ts +1 -0
  172. package/dist/browser/scrape/parser/parse.js +1 -0
  173. package/dist/browser/scrape/parser/valid.d.ts +2 -0
  174. package/dist/browser/scrape/parser/valid.js +5 -0
  175. package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
  176. package/dist/browser/scrape/parser/void-tag.js +43 -0
  177. package/dist/browser/scrape/types.d.ts +7 -0
  178. package/dist/browser/seo/analyzer.d.ts +59 -0
  179. package/dist/browser/seo/analyzer.js +1399 -0
  180. package/dist/browser/seo/keywords.d.ts +16 -0
  181. package/dist/browser/seo/keywords.js +55 -0
  182. package/dist/browser/seo/rules/accessibility.d.ts +2 -0
  183. package/dist/browser/seo/rules/accessibility.js +722 -0
  184. package/dist/browser/seo/rules/ai-search.d.ts +2 -0
  185. package/dist/browser/seo/rules/ai-search.js +436 -0
  186. package/dist/browser/seo/rules/analytics.d.ts +2 -0
  187. package/dist/browser/seo/rules/analytics.js +306 -0
  188. package/dist/browser/seo/rules/best-practices.d.ts +2 -0
  189. package/dist/browser/seo/rules/best-practices.js +195 -0
  190. package/dist/browser/seo/rules/canonical.d.ts +12 -0
  191. package/dist/browser/seo/rules/canonical.js +258 -0
  192. package/dist/browser/seo/rules/content.d.ts +2 -0
  193. package/dist/browser/seo/rules/content.js +424 -0
  194. package/dist/browser/seo/rules/crawl.d.ts +2 -0
  195. package/dist/browser/seo/rules/crawl.js +435 -0
  196. package/dist/browser/seo/rules/cwv.d.ts +2 -0
  197. package/dist/browser/seo/rules/cwv.js +248 -0
  198. package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
  199. package/dist/browser/seo/rules/ecommerce.js +283 -0
  200. package/dist/browser/seo/rules/i18n.d.ts +2 -0
  201. package/dist/browser/seo/rules/i18n.js +277 -0
  202. package/dist/browser/seo/rules/images.d.ts +2 -0
  203. package/dist/browser/seo/rules/images.js +235 -0
  204. package/dist/browser/seo/rules/index.d.ts +52 -0
  205. package/dist/browser/seo/rules/index.js +159 -0
  206. package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
  207. package/dist/browser/seo/rules/internal-linking.js +394 -0
  208. package/dist/browser/seo/rules/links.d.ts +2 -0
  209. package/dist/browser/seo/rules/links.js +490 -0
  210. package/dist/browser/seo/rules/local.d.ts +2 -0
  211. package/dist/browser/seo/rules/local.js +289 -0
  212. package/dist/browser/seo/rules/meta.d.ts +2 -0
  213. package/dist/browser/seo/rules/meta.js +646 -0
  214. package/dist/browser/seo/rules/mobile.d.ts +2 -0
  215. package/dist/browser/seo/rules/mobile.js +161 -0
  216. package/dist/browser/seo/rules/performance.d.ts +2 -0
  217. package/dist/browser/seo/rules/performance.js +625 -0
  218. package/dist/browser/seo/rules/pwa.d.ts +2 -0
  219. package/dist/browser/seo/rules/pwa.js +299 -0
  220. package/dist/browser/seo/rules/readability.d.ts +2 -0
  221. package/dist/browser/seo/rules/readability.js +264 -0
  222. package/dist/browser/seo/rules/redirects.d.ts +16 -0
  223. package/dist/browser/seo/rules/redirects.js +199 -0
  224. package/dist/browser/seo/rules/resources.d.ts +2 -0
  225. package/dist/browser/seo/rules/resources.js +390 -0
  226. package/dist/browser/seo/rules/schema.d.ts +2 -0
  227. package/dist/browser/seo/rules/schema.js +379 -0
  228. package/dist/browser/seo/rules/security.d.ts +2 -0
  229. package/dist/browser/seo/rules/security.js +877 -0
  230. package/dist/browser/seo/rules/social.d.ts +2 -0
  231. package/dist/browser/seo/rules/social.js +603 -0
  232. package/dist/browser/seo/rules/structural.d.ts +2 -0
  233. package/dist/browser/seo/rules/structural.js +179 -0
  234. package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
  235. package/dist/browser/seo/rules/technical-advanced.js +288 -0
  236. package/dist/browser/seo/rules/technical.d.ts +2 -0
  237. package/dist/browser/seo/rules/technical.js +480 -0
  238. package/dist/browser/seo/rules/thresholds.d.ts +196 -0
  239. package/dist/browser/seo/rules/thresholds.js +118 -0
  240. package/dist/browser/seo/rules/types.d.ts +498 -0
  241. package/dist/browser/seo/rules/types.js +11 -0
  242. package/dist/browser/seo/types.d.ts +211 -0
  243. package/dist/browser/seo/types.js +1 -0
  244. package/dist/browser/transport/curl.d.ts +4 -0
  245. package/dist/browser/transport/curl.js +101 -0
  246. package/dist/browser/transport/undici.js +1 -2
  247. package/dist/browser/transport/worker.d.ts +18 -0
  248. package/dist/browser/transport/worker.js +278 -0
  249. package/dist/browser/types/index.d.ts +4 -1
  250. package/dist/browser/utils/binary-manager.d.ts +4 -0
  251. package/dist/browser/utils/binary-manager.js +72 -0
  252. package/dist/browser/utils/user-agent.js +2 -13
  253. package/dist/cache/indexed-db.d.ts +10 -0
  254. package/dist/cache/indexed-db.js +88 -0
  255. package/dist/cache/service-worker-cache.d.ts +18 -0
  256. package/dist/cache/service-worker-cache.js +103 -0
  257. package/dist/cli/commands/ai.d.ts +2 -0
  258. package/dist/cli/commands/ai.js +162 -0
  259. package/dist/cli/commands/bench.d.ts +2 -0
  260. package/dist/cli/commands/bench.js +51 -0
  261. package/dist/cli/commands/dns.d.ts +2 -0
  262. package/dist/cli/commands/dns.js +295 -0
  263. package/dist/cli/commands/har.d.ts +2 -0
  264. package/dist/cli/commands/har.js +171 -0
  265. package/dist/cli/commands/hls.d.ts +2 -0
  266. package/dist/cli/commands/hls.js +192 -0
  267. package/dist/cli/commands/network.d.ts +2 -0
  268. package/dist/cli/commands/network.js +288 -0
  269. package/dist/cli/commands/protocols.d.ts +2 -0
  270. package/dist/cli/commands/protocols.js +344 -0
  271. package/dist/cli/commands/scrape.d.ts +2 -0
  272. package/dist/cli/commands/scrape.js +176 -0
  273. package/dist/cli/commands/security.d.ts +2 -0
  274. package/dist/cli/commands/security.js +57 -0
  275. package/dist/cli/commands/seo.d.ts +2 -0
  276. package/dist/cli/commands/seo.js +125 -0
  277. package/dist/cli/commands/serve.d.ts +2 -0
  278. package/dist/cli/commands/serve.js +531 -0
  279. package/dist/cli/commands/spider.d.ts +3 -0
  280. package/dist/cli/commands/spider.js +456 -0
  281. package/dist/cli/commands/utils.d.ts +2 -0
  282. package/dist/cli/commands/utils.js +176 -0
  283. package/dist/cli/commands/vector.d.ts +2 -0
  284. package/dist/cli/commands/vector.js +158 -0
  285. package/dist/cli/handler.d.ts +2 -2
  286. package/dist/cli/handler.js +6 -6
  287. package/dist/cli/helpers.d.ts +7 -0
  288. package/dist/cli/helpers.js +128 -0
  289. package/dist/cli/index.js +96 -5228
  290. package/dist/cli/parser/help.d.ts +2 -0
  291. package/dist/cli/parser/help.js +52 -0
  292. package/dist/cli/parser/index.d.ts +3 -0
  293. package/dist/cli/parser/index.js +3 -0
  294. package/dist/cli/parser/parser.d.ts +4 -0
  295. package/dist/cli/parser/parser.js +146 -0
  296. package/dist/cli/parser/types.d.ts +41 -0
  297. package/dist/cli/parser/types.js +1 -0
  298. package/dist/cli/presets.d.ts +1 -1
  299. package/dist/cli/presets.js +1 -1
  300. package/dist/cli/router.d.ts +36 -0
  301. package/dist/cli/router.js +195 -0
  302. package/dist/cli/tui/ai-chat.js +1 -1
  303. package/dist/cli/tui/commands/context.d.ts +9 -0
  304. package/dist/cli/tui/commands/context.js +1 -0
  305. package/dist/cli/tui/commands/dns.d.ts +10 -0
  306. package/dist/cli/tui/commands/dns.js +461 -0
  307. package/dist/cli/tui/commands/hls.d.ts +2 -0
  308. package/dist/cli/tui/commands/hls.js +162 -0
  309. package/dist/cli/tui/commands/ip.d.ts +2 -0
  310. package/dist/cli/tui/commands/ip.js +45 -0
  311. package/dist/cli/tui/commands/network.d.ts +3 -0
  312. package/dist/cli/tui/commands/network.js +81 -0
  313. package/dist/cli/tui/commands/protocols.d.ts +6 -0
  314. package/dist/cli/tui/commands/protocols.js +531 -0
  315. package/dist/cli/tui/commands/security.d.ts +2 -0
  316. package/dist/cli/tui/commands/security.js +48 -0
  317. package/dist/cli/tui/commands/seo.d.ts +2 -0
  318. package/dist/cli/tui/commands/seo.js +74 -0
  319. package/dist/cli/tui/context.d.ts +12 -0
  320. package/dist/cli/tui/context.js +1 -0
  321. package/dist/cli/tui/shell.d.ts +11 -20
  322. package/dist/cli/tui/shell.js +216 -1873
  323. package/dist/constants/user-agents.d.ts +7 -0
  324. package/dist/constants/user-agents.js +7 -0
  325. package/dist/core/client.d.ts +2 -0
  326. package/dist/core/client.js +19 -1
  327. package/dist/index.d.ts +1 -0
  328. package/dist/index.js +1 -0
  329. package/dist/mcp/cli.js +2 -3
  330. package/dist/mcp/data/embeddings.json +1 -0
  331. package/dist/mcp/tools/network.js +298 -158
  332. package/dist/plugins/har-player.d.ts +23 -0
  333. package/dist/plugins/har-player.js +49 -0
  334. package/dist/plugins/har-recorder.d.ts +37 -3
  335. package/dist/plugins/har-recorder.js +116 -63
  336. package/dist/plugins/network-simulation.d.ts +7 -0
  337. package/dist/plugins/network-simulation.js +13 -0
  338. package/dist/presets/android.d.ts +2 -0
  339. package/dist/presets/android.js +16 -0
  340. package/dist/presets/chaturbate.d.ts +2 -0
  341. package/dist/presets/chaturbate.js +17 -0
  342. package/dist/presets/elevenlabs.d.ts +6 -0
  343. package/dist/presets/elevenlabs.js +20 -0
  344. package/dist/presets/enhancers.d.ts +20 -0
  345. package/dist/presets/enhancers.js +85 -0
  346. package/dist/presets/hubspot.d.ts +9 -0
  347. package/dist/presets/hubspot.js +28 -0
  348. package/dist/presets/index.d.ts +10 -0
  349. package/dist/presets/index.js +10 -0
  350. package/dist/presets/ios.d.ts +2 -0
  351. package/dist/presets/ios.js +13 -0
  352. package/dist/presets/pinecone.d.ts +8 -0
  353. package/dist/presets/pinecone.js +42 -0
  354. package/dist/presets/registry.js +60 -0
  355. package/dist/presets/sendgrid.d.ts +6 -0
  356. package/dist/presets/sendgrid.js +20 -0
  357. package/dist/presets/sentry.d.ts +11 -0
  358. package/dist/presets/sentry.js +48 -0
  359. package/dist/presets/square.d.ts +10 -0
  360. package/dist/presets/square.js +33 -0
  361. package/dist/recker.d.ts +3 -0
  362. package/dist/recker.js +4 -0
  363. package/dist/scrape/document.d.ts +5 -4
  364. package/dist/scrape/document.js +89 -76
  365. package/dist/scrape/element.d.ts +10 -8
  366. package/dist/scrape/element.js +295 -81
  367. package/dist/scrape/extractors.d.ts +11 -11
  368. package/dist/scrape/extractors.js +145 -113
  369. package/dist/scrape/index.d.ts +2 -0
  370. package/dist/scrape/index.js +1 -0
  371. package/dist/scrape/parser/back.d.ts +1 -0
  372. package/dist/scrape/parser/back.js +3 -0
  373. package/dist/scrape/parser/index.d.ts +20 -0
  374. package/dist/scrape/parser/index.js +19 -0
  375. package/dist/scrape/parser/matcher.d.ts +30 -0
  376. package/dist/scrape/parser/matcher.js +99 -0
  377. package/dist/scrape/parser/nodes/comment.d.ts +12 -0
  378. package/dist/scrape/parser/nodes/comment.js +21 -0
  379. package/dist/scrape/parser/nodes/html.d.ts +110 -0
  380. package/dist/scrape/parser/nodes/html.js +978 -0
  381. package/dist/scrape/parser/nodes/node.d.ts +18 -0
  382. package/dist/scrape/parser/nodes/node.js +31 -0
  383. package/dist/scrape/parser/nodes/text.d.ts +14 -0
  384. package/dist/scrape/parser/nodes/text.js +30 -0
  385. package/dist/scrape/parser/nodes/type.d.ts +6 -0
  386. package/dist/scrape/parser/nodes/type.js +7 -0
  387. package/dist/scrape/parser/parse.d.ts +1 -0
  388. package/dist/scrape/parser/parse.js +1 -0
  389. package/dist/scrape/parser/valid.d.ts +2 -0
  390. package/dist/scrape/parser/valid.js +5 -0
  391. package/dist/scrape/parser/void-tag.d.ts +7 -0
  392. package/dist/scrape/parser/void-tag.js +43 -0
  393. package/dist/scrape/spider.d.ts +19 -0
  394. package/dist/scrape/spider.js +28 -3
  395. package/dist/scrape/types.d.ts +7 -0
  396. package/dist/seo/analyzer.d.ts +15 -5
  397. package/dist/seo/analyzer.js +636 -175
  398. package/dist/seo/formatter.d.ts +16 -0
  399. package/dist/seo/formatter.js +228 -0
  400. package/dist/seo/index.d.ts +2 -0
  401. package/dist/seo/index.js +1 -0
  402. package/dist/seo/keywords.d.ts +16 -0
  403. package/dist/seo/keywords.js +55 -0
  404. package/dist/seo/rules/accessibility.js +85 -57
  405. package/dist/seo/rules/ai-search.js +44 -31
  406. package/dist/seo/rules/analytics.d.ts +2 -0
  407. package/dist/seo/rules/analytics.js +306 -0
  408. package/dist/seo/rules/best-practices.js +21 -14
  409. package/dist/seo/rules/canonical.js +31 -22
  410. package/dist/seo/rules/content.js +207 -19
  411. package/dist/seo/rules/crawl.js +55 -40
  412. package/dist/seo/rules/cwv.js +21 -15
  413. package/dist/seo/rules/ecommerce.js +51 -20
  414. package/dist/seo/rules/i18n.js +61 -33
  415. package/dist/seo/rules/images.js +87 -28
  416. package/dist/seo/rules/index.js +2 -0
  417. package/dist/seo/rules/internal-linking.js +58 -39
  418. package/dist/seo/rules/links.js +70 -51
  419. package/dist/seo/rules/local.js +49 -25
  420. package/dist/seo/rules/meta.js +161 -62
  421. package/dist/seo/rules/mobile.js +112 -2
  422. package/dist/seo/rules/performance.js +309 -54
  423. package/dist/seo/rules/pwa.js +36 -39
  424. package/dist/seo/rules/readability.js +31 -22
  425. package/dist/seo/rules/redirects.js +21 -15
  426. package/dist/seo/rules/resources.js +59 -42
  427. package/dist/seo/rules/schema.js +333 -8
  428. package/dist/seo/rules/security.js +142 -80
  429. package/dist/seo/rules/social.js +277 -47
  430. package/dist/seo/rules/structural.js +40 -16
  431. package/dist/seo/rules/technical-advanced.js +27 -22
  432. package/dist/seo/rules/technical.js +243 -42
  433. package/dist/seo/rules/types.d.ts +53 -1
  434. package/dist/seo/seo-spider.d.ts +22 -0
  435. package/dist/seo/seo-spider.js +77 -13
  436. package/dist/seo/types.d.ts +8 -1
  437. package/dist/seo/validators/llms-txt.js +19 -0
  438. package/dist/seo/validators/rss.d.ts +11 -0
  439. package/dist/seo/validators/rss.js +93 -0
  440. package/dist/seo/validators/sitemap.js +36 -26
  441. package/dist/transport/curl.d.ts +4 -0
  442. package/dist/transport/curl.js +101 -0
  443. package/dist/transport/udp.js +0 -1
  444. package/dist/transport/undici.js +1 -2
  445. package/dist/transport/worker.d.ts +18 -0
  446. package/dist/transport/worker.js +278 -0
  447. package/dist/types/index.d.ts +4 -1
  448. package/dist/utils/binary-manager.d.ts +4 -0
  449. package/dist/utils/binary-manager.js +72 -0
  450. package/dist/utils/optional-require.d.ts +7 -8
  451. package/dist/utils/optional-require.js +2 -21
  452. package/dist/utils/upload.d.ts +6 -0
  453. package/dist/utils/upload.js +11 -0
  454. package/dist/utils/user-agent.js +2 -13
  455. package/dist/version.js +1 -1
  456. package/package.json +14 -5
  457. package/dist/browser/utils/optional-require.d.ts +0 -19
  458. package/dist/browser/utils/optional-require.js +0 -105
@@ -1,40 +1,52 @@
1
+ import { parse } from '../scrape/parser/index.js';
1
2
  import { extractMeta, extractOpenGraph, extractTwitterCard, extractJsonLd, extractLinks, extractImages, } from '../scrape/extractors.js';
2
- import { requireOptional } from '../utils/optional-require.js';
3
+ import { generateKeywordCloud } from './keywords.js';
3
4
  import { createRulesEngine, SEO_THRESHOLDS, } from './rules/index.js';
4
- let cheerioModule = null;
5
- async function loadCheerio() {
6
- if (cheerioModule)
7
- return cheerioModule;
8
- cheerioModule = await requireOptional('cheerio', 'recker/seo');
9
- return cheerioModule;
10
- }
11
5
  export class SeoAnalyzer {
12
- $;
6
+ root;
13
7
  options;
14
8
  rulesEngine;
15
- constructor($, options = {}) {
16
- this.$ = $;
9
+ constructor(root, options = {}) {
10
+ this.root = root;
17
11
  this.options = options;
18
12
  this.rulesEngine = createRulesEngine(options.rules);
19
13
  }
20
14
  static async fromHtml(html, options = {}) {
21
- const { load } = await loadCheerio();
22
- return new SeoAnalyzer(load(html), options);
15
+ const root = parse(html);
16
+ return new SeoAnalyzer(root, options);
23
17
  }
24
18
  analyze() {
25
19
  const url = this.options.baseUrl || '';
26
- const meta = extractMeta(this.$);
27
- const og = extractOpenGraph(this.$);
28
- const twitter = extractTwitterCard(this.$);
29
- const jsonLd = extractJsonLd(this.$);
30
- const links = extractLinks(this.$, { baseUrl: this.options.baseUrl });
31
- const images = extractImages(this.$, { baseUrl: 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);
32
40
  const headings = this.analyzeHeadings();
33
41
  const content = this.analyzeContent(headings);
34
42
  const linkAnalysis = this.buildLinkAnalysis(links);
35
43
  const imageAnalysis = this.buildImageAnalysis(images);
36
44
  const social = this.buildSocialAnalysis(og, twitter);
37
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);
38
50
  const context = this.buildRuleContext({
39
51
  meta,
40
52
  og,
@@ -45,6 +57,14 @@ export class SeoAnalyzer {
45
57
  linkAnalysis,
46
58
  imageAnalysis,
47
59
  links,
60
+ keywords,
61
+ resources,
62
+ emailsFound,
63
+ socialLinksFound,
64
+ socialLinkDetails,
65
+ analytics,
66
+ feeds,
67
+ conversion,
48
68
  });
49
69
  const ruleResults = this.rulesEngine.evaluate(context);
50
70
  const checks = this.convertToCheckResults(ruleResults);
@@ -65,23 +85,33 @@ export class SeoAnalyzer {
65
85
  score,
66
86
  summary,
67
87
  checks,
68
- title: meta.title ? { text: meta.title, length: meta.title.length } : undefined,
69
- metaDescription: meta.description ? { text: meta.description, length: meta.description.length } : undefined,
70
- openGraph: Object.keys(og).length > 0 ? {
71
- title: og.title,
72
- description: og.description,
73
- image: Array.isArray(og.image) ? og.image[0] : og.image,
74
- url: og.url,
75
- type: og.type,
76
- siteName: og.siteName,
77
- } : undefined,
78
- twitterCard: Object.keys(twitter).length > 0 ? {
79
- card: twitter.card,
80
- title: twitter.title,
81
- description: twitter.description,
82
- image: Array.isArray(twitter.image) ? twitter.image[0] : twitter.image,
83
- site: twitter.site,
84
- } : undefined,
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,
85
115
  structuredData: {
86
116
  count: jsonLd.length,
87
117
  types: jsonLd.map((j) => j['@type']).filter(Boolean),
@@ -89,32 +119,66 @@ export class SeoAnalyzer {
89
119
  },
90
120
  headings: headings,
91
121
  content,
122
+ keywords,
92
123
  links: linkAnalysis,
93
124
  images: imageAnalysis,
94
125
  social,
95
126
  technical,
96
127
  };
97
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
+ }
98
154
  buildRuleContext(data) {
99
- const { meta, og, twitter, jsonLd, headings, content, linkAnalysis, imageAnalysis, links } = data;
100
- const htmlLang = this.$('html').attr('lang');
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;
101
158
  const hreflangTags = [];
102
- this.$('link[rel="alternate"][hreflang]').each((_, el) => {
103
- const $el = this.$(el);
104
- const lang = $el.attr('hreflang');
105
- const href = $el.attr('href');
159
+ this.root
160
+ .querySelectorAll('link[rel="alternate"][hreflang]')
161
+ .forEach((el) => {
162
+ const lang = el.getAttribute('hreflang');
163
+ const href = el.getAttribute('href');
106
164
  if (lang && href) {
107
165
  hreflangTags.push({ lang, href });
108
166
  }
109
167
  });
110
- const ogLocale = this.$('meta[property="og:locale"]').attr('content');
168
+ const ogLocaleEl = this.root.querySelector('meta[property="og:locale"]');
169
+ const ogLocale = ogLocaleEl ? ogLocaleEl.getAttribute('content') : undefined;
111
170
  const genericTexts = SEO_THRESHOLDS.links.genericTexts;
112
171
  const genericTextLinks = links.filter((l) => {
113
172
  const text = l.text?.toLowerCase().trim();
114
173
  return text && genericTexts.some((g) => text === g || text.includes(g));
115
174
  });
116
175
  const linksWithGenericText = genericTextLinks.length;
117
- const linksWithoutTextArray = links.filter((l) => !l.text || l.text.trim() === '');
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
+ });
118
182
  const externalBlankLinks = links.filter((l) => l.type === 'external' && l.target === '_blank');
119
183
  const missingNoopenerLinks = externalBlankLinks.filter((l) => !l.rel?.includes('noopener'));
120
184
  const missingNoreferrerLinks = externalBlankLinks.filter((l) => !l.rel?.includes('noreferrer'));
@@ -125,11 +189,37 @@ export class SeoAnalyzer {
125
189
  missingNoreferrer: missingNoreferrerLinks,
126
190
  };
127
191
  const hasMixedContent = this.checkMixedContent();
128
- const h1Elements = this.$('h1');
129
- const h1Text = h1Elements.first().text().trim();
130
- const viewportContent = this.$('meta[name="viewport"]').attr('content');
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;
131
221
  const a11yMetrics = this.analyzeAccessibility();
132
- const imagesWithEmptyAlt = this.$('img[alt=""]').length;
222
+ const imagesWithEmptyAlt = this.root.querySelectorAll('img[alt=""]').length;
133
223
  const linkSecurityMetrics = this.analyzeLinkSecurity();
134
224
  const faviconInfo = this.detectFavicon();
135
225
  const perfHints = this.analyzePerformanceHints();
@@ -137,11 +227,34 @@ export class SeoAnalyzer {
137
227
  const structuralHtml = this.analyzeStructuralHtml();
138
228
  const breadcrumbs = this.analyzeBreadcrumbs(jsonLd.map((j) => j['@type']).filter(Boolean));
139
229
  const multimedia = this.analyzeMultimedia();
230
+ const advancedImages = this.analyzeAdvancedImages();
231
+ const responsiveImages = this.analyzeResponsiveImages();
232
+ const inlineImages = this.analyzeInlineImages();
140
233
  const trustSignals = this.analyzeTrustSignals(links);
141
- const totalSubheadings = (headings.structure.filter((h) => h.level === 2).length || 0) + (headings.structure.filter((h) => h.level === 3).length || 0);
142
- const subheadingFrequency = content.wordCount > 0 ? (totalSubheadings / content.wordCount) * 100 : 0;
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;
143
239
  const textHtmlRatio = this.calculateTextHtmlRatio(content.characterCount);
144
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,
145
258
  title: meta.title,
146
259
  titleLength: meta.title?.length,
147
260
  metaDescription: meta.description,
@@ -157,7 +270,9 @@ export class SeoAnalyzer {
157
270
  twitterCard: twitter.card,
158
271
  twitterTitle: twitter.title,
159
272
  twitterDescription: twitter.description,
160
- twitterImage: Array.isArray(twitter.image) ? twitter.image[0] : twitter.image,
273
+ twitterImage: Array.isArray(twitter.image)
274
+ ? twitter.image[0]
275
+ : twitter.image,
161
276
  twitterSite: twitter.site,
162
277
  h1Count: headings.h1Count,
163
278
  h1Text: h1Text || undefined,
@@ -174,13 +289,18 @@ export class SeoAnalyzer {
174
289
  imagesWithEmptyAlt,
175
290
  imagesUsingModernFormats: imageAnalysis.modernFormats,
176
291
  altTextLengths: imageAnalysis.altTextLengths,
292
+ imageAltTexts: imageAnalysis.imageAltTexts,
177
293
  imageFilenames: imageAnalysis.imageFilenames,
178
294
  imagesWithAsyncDecoding: imageAnalysis.imagesWithAsyncDecoding,
295
+ imagesWithSrcset: responsiveImages.imagesWithSrcset,
296
+ largeBase64ImagesCount: inlineImages.largeBase64ImagesCount,
179
297
  ...a11yMetrics,
180
298
  allLinks: links,
181
299
  totalLinks: linkAnalysis.total,
182
300
  internalLinks: linkAnalysis.internal,
183
301
  externalLinks: linkAnalysis.external,
302
+ internalHttpLinks: linkAnalysis.internalHttpLinks,
303
+ internalHttpLinkUrls: linkAnalysis.internalHttpLinkUrls,
184
304
  linksWithoutText: linkAnalysis.withoutText,
185
305
  nofollowLinks: linkAnalysis.nofollow,
186
306
  sponsoredLinks: linkAnalysis.sponsoredLinks,
@@ -221,6 +341,12 @@ export class SeoAnalyzer {
221
341
  hasMixedContent,
222
342
  responseHeaders: this.options.responseHeaders,
223
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,
224
350
  ...faviconInfo,
225
351
  ...perfHints,
226
352
  lcpHints: cwvHints.lcpHints,
@@ -229,11 +355,20 @@ export class SeoAnalyzer {
229
355
  jsonLdTypes: jsonLd.map((j) => j['@type']).filter(Boolean),
230
356
  url: this.options.baseUrl,
231
357
  urlLength: this.options.baseUrl?.length,
232
- titleMatchesH1: meta.title && h1Text ? meta.title.toLowerCase().trim() === h1Text.toLowerCase().trim() : undefined,
358
+ titleMatchesH1: meta.title && h1Text
359
+ ? meta.title.toLowerCase().trim() === h1Text.toLowerCase().trim()
360
+ : undefined,
233
361
  ...this.analyzeUrlQuality(),
234
362
  ...this.analyzeJsRendering(content),
235
363
  hreflangTags: hreflangTags.length > 0 ? hreflangTags : undefined,
236
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,
237
372
  };
238
373
  }
239
374
  analyzeUrlQuality() {
@@ -249,7 +384,7 @@ export class SeoAnalyzer {
249
384
  const path = url.pathname + url.search;
250
385
  const urlHasUppercase = /[A-Z]/.test(path);
251
386
  const urlHasAccents = /[àáâãäåæçèéêëìíîïñòóôõöùúûüýÿ]/i.test(path);
252
- const urlHasSpecialChars = /[<>{}|\\^`\[\]]/.test(path) || /%[0-9A-F]{2}/i.test(path);
387
+ const urlHasSpecialChars = /[<>{}|\^`\[\]]/.test(path) || /%[0-9A-F]{2}/i.test(path);
253
388
  return { urlHasUppercase, urlHasSpecialChars, urlHasAccents };
254
389
  }
255
390
  catch {
@@ -262,20 +397,40 @@ export class SeoAnalyzer {
262
397
  }
263
398
  analyzeJsRendering(content) {
264
399
  const bodyTextLength = content.characterCount;
265
- const scriptCount = this.$('script').length;
266
- const noscriptContent = this.$('noscript').text().trim();
267
- const hasNoscriptContent = noscriptContent.length > 50;
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;
268
404
  return { bodyTextLength, scriptCount, hasNoscriptContent };
269
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
+ }
270
425
  checkMixedContent() {
271
426
  let hasMixed = false;
272
- this.$('img[src^="http://"]').each(() => {
427
+ this.root.querySelectorAll('img[src^="http://"]').forEach(() => {
273
428
  hasMixed = true;
274
429
  });
275
- this.$('script[src^="http://"]').each(() => {
430
+ this.root.querySelectorAll('script[src^="http://"]').forEach(() => {
276
431
  hasMixed = true;
277
432
  });
278
- this.$('link[href^="http://"]').each(() => {
433
+ this.root.querySelectorAll('link[href^="http://"]').forEach(() => {
279
434
  hasMixed = true;
280
435
  });
281
436
  return hasMixed;
@@ -283,10 +438,11 @@ export class SeoAnalyzer {
283
438
  analyzeLinkSecurity() {
284
439
  let withoutNoopener = 0;
285
440
  let withoutNoreferrer = 0;
286
- this.$('a[href^="http"][target="_blank"]').each((_, el) => {
287
- const $el = this.$(el);
288
- const href = $el.attr('href') || '';
289
- const rel = ($el.attr('rel') || '').toLowerCase();
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();
290
446
  if (this.options.baseUrl && href.startsWith(this.options.baseUrl)) {
291
447
  return;
292
448
  }
@@ -310,39 +466,42 @@ export class SeoAnalyzer {
310
466
  'link[rel="apple-touch-icon-precomposed"]',
311
467
  ];
312
468
  for (const selector of faviconSelectors) {
313
- const favicon = this.$(selector).first();
314
- if (favicon.length > 0) {
469
+ const favicon = this.root.querySelector(selector);
470
+ if (favicon) {
315
471
  return {
316
472
  hasFavicon: true,
317
- faviconUrl: favicon.attr('href'),
473
+ faviconUrl: favicon.getAttribute('href'),
318
474
  };
319
475
  }
320
476
  }
321
477
  return { hasFavicon: false };
322
478
  }
323
479
  analyzePerformanceHints() {
324
- const preconnectLinks = this.$('link[rel="preconnect"]');
480
+ const preconnectLinks = this.root.querySelectorAll('link[rel="preconnect"]');
325
481
  const preconnectCount = preconnectLinks.length;
326
- const dnsPrefetchLinks = this.$('link[rel="dns-prefetch"]');
482
+ const dnsPrefetchLinks = this.root.querySelectorAll('link[rel="dns-prefetch"]');
327
483
  const dnsPrefetchCount = dnsPrefetchLinks.length;
328
- const preloadLinks = this.$('link[rel="preload"]');
484
+ const preloadLinks = this.root.querySelectorAll('link[rel="preload"]');
329
485
  const preloadCount = preloadLinks.length;
330
486
  let renderBlockingResources = 0;
331
- this.$('head script[src]:not([async]):not([defer])').each(() => {
487
+ this.root
488
+ .querySelectorAll('head script[src]:not([async]):not([defer])')
489
+ .forEach(() => {
332
490
  renderBlockingResources++;
333
491
  });
334
- this.$('head link[rel="stylesheet"]').each((_, el) => {
335
- const $el = this.$(el);
336
- const media = $el.attr('media');
492
+ this.root.querySelectorAll('head link[rel="stylesheet"]').forEach((el) => {
493
+ const media = el.getAttribute('media');
337
494
  if (!media || media === 'all' || media === 'screen') {
338
495
  renderBlockingResources++;
339
496
  }
340
497
  });
341
- const inlineScriptsCount = this.$('script:not([src])').filter((_, el) => {
342
- const content = this.$(el).html() || '';
498
+ const inlineScriptsCount = this.root
499
+ .querySelectorAll('script:not([src])')
500
+ .filter((el) => {
501
+ const content = el.innerHTML || '';
343
502
  return content.trim().length > 0;
344
503
  }).length;
345
- const inlineStylesCount = this.$('style').length;
504
+ const inlineStylesCount = this.root.querySelectorAll('style').length;
346
505
  return {
347
506
  hasPreconnect: preconnectCount > 0,
348
507
  preconnectCount,
@@ -356,16 +515,15 @@ export class SeoAnalyzer {
356
515
  };
357
516
  }
358
517
  analyzeCWVHints() {
359
- const images = this.$('img');
518
+ const images = this.root.querySelectorAll('img');
360
519
  let hasLargeImages = false;
361
520
  let hasLazyLcp = false;
362
521
  let hasPriorityHints = false;
363
- images.slice(0, 3).each((index, el) => {
364
- const $el = this.$(el);
365
- const width = parseInt($el.attr('width') || '0', 10);
366
- const height = parseInt($el.attr('height') || '0', 10);
367
- const loading = $el.attr('loading');
368
- const fetchPriority = $el.attr('fetchpriority');
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');
369
527
  if (width >= 400 || height >= 300) {
370
528
  hasLargeImages = true;
371
529
  }
@@ -376,10 +534,15 @@ export class SeoAnalyzer {
376
534
  hasPriorityHints = true;
377
535
  }
378
536
  });
379
- this.$('[style*="background-image"]').slice(0, 3).each(() => {
537
+ this.root
538
+ .querySelectorAll('[style*="background-image"]')
539
+ .slice(0, 3)
540
+ .forEach(() => {
380
541
  hasLargeImages = true;
381
542
  });
382
- const imagesWithoutDimensions = this.$('img:not([width]):not([height])').length + this.$('img[width="auto"], img[height="auto"]').length;
543
+ const imagesWithoutDimensions = this.root.querySelectorAll('img:not([width]):not([height])').length +
544
+ this.root.querySelectorAll('img[width="auto"], img[height="auto"]')
545
+ .length;
383
546
  return {
384
547
  lcpHints: {
385
548
  hasLargeImages,
@@ -393,65 +556,75 @@ export class SeoAnalyzer {
393
556
  }
394
557
  analyzeAccessibility() {
395
558
  let buttonsWithoutAriaLabel = 0;
396
- this.$('button').each((_, el) => {
397
- const $el = this.$(el);
398
- const text = $el.text().trim();
399
- const ariaLabel = $el.attr('aria-label');
400
- const ariaLabelledBy = $el.attr('aria-labelledby');
401
- const title = $el.attr('title');
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');
402
564
  if (!text && !ariaLabel && !ariaLabelledBy && !title) {
403
565
  buttonsWithoutAriaLabel++;
404
566
  }
405
567
  });
406
568
  let linksWithoutAriaLabel = 0;
407
- this.$('a[href]').each((_, el) => {
408
- const $el = this.$(el);
409
- const text = $el.text().trim();
410
- const ariaLabel = $el.attr('aria-label');
411
- const ariaLabelledBy = $el.attr('aria-labelledby');
412
- const title = $el.attr('title');
413
- const hasOnlyImage = $el.find('img, svg').length > 0 && !text;
414
- if (hasOnlyImage && !ariaLabel && !ariaLabelledBy && !title) {
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) {
415
582
  linksWithoutAriaLabel++;
416
583
  }
417
584
  });
418
585
  let inputsWithoutLabel = 0;
419
- this.$('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea').each((_, el) => {
420
- const $el = this.$(el);
421
- const id = $el.attr('id');
422
- const ariaLabel = $el.attr('aria-label');
423
- const ariaLabelledBy = $el.attr('aria-labelledby');
424
- const placeholder = $el.attr('placeholder');
425
- const title = $el.attr('title');
426
- const hasLabel = id ? this.$(`label[for="${id}"]`).length > 0 : false;
427
- const wrappedInLabel = $el.closest('label').length > 0;
428
- if (!hasLabel && !wrappedInLabel && !ariaLabel && !ariaLabelledBy && !title && !placeholder) {
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) {
429
604
  inputsWithoutLabel++;
430
605
  }
431
606
  });
432
- const iframesWithoutTitle = this.$('iframe:not([title])').length;
607
+ const iframesWithoutTitle = this.root.querySelectorAll('iframe:not([title])').length;
433
608
  let tablesWithoutCaption = 0;
434
- this.$('table').each((_, el) => {
435
- const $el = this.$(el);
436
- const hasCaption = $el.find('caption').length > 0;
437
- const ariaLabel = $el.attr('aria-label');
438
- const ariaLabelledBy = $el.attr('aria-labelledby');
439
- const role = $el.attr('role');
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');
440
614
  if (role === 'presentation' || role === 'none')
441
615
  return;
442
- const hasHeaders = $el.find('th').length > 0;
616
+ const hasHeaders = el.querySelectorAll('th').length > 0;
443
617
  if (hasHeaders && !hasCaption && !ariaLabel && !ariaLabelledBy) {
444
618
  tablesWithoutCaption++;
445
619
  }
446
620
  });
447
621
  let svgsWithoutTitle = 0;
448
- this.$('svg').each((_, el) => {
449
- const $el = this.$(el);
450
- const hasTitle = $el.find('title').length > 0;
451
- const ariaLabel = $el.attr('aria-label');
452
- const ariaLabelledBy = $el.attr('aria-labelledby');
453
- const ariaHidden = $el.attr('aria-hidden');
454
- const role = $el.attr('role');
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');
455
628
  if (ariaHidden === 'true' || role === 'presentation' || role === 'none')
456
629
  return;
457
630
  if (!hasTitle && !ariaLabel && !ariaLabelledBy) {
@@ -469,36 +642,271 @@ export class SeoAnalyzer {
469
642
  }
470
643
  analyzeStructuralHtml() {
471
644
  return {
472
- hasHeader: this.$('header').length > 0,
473
- hasNav: this.$('nav').length > 0,
474
- hasMain: this.$('main').length > 0,
475
- hasArticle: this.$('article').length > 0,
476
- hasSection: this.$('section').length > 0,
477
- hasFooter: this.$('footer').length > 0,
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,
478
651
  };
479
652
  }
480
653
  analyzeBreadcrumbs(jsonLdTypes) {
481
- const hasBreadcrumbsHtml = this.$('nav[aria-label="breadcrumb"], .breadcrumb, .breadcrumbs').length > 0;
654
+ const hasBreadcrumbsHtml = this.root.querySelectorAll('nav[aria-label="breadcrumb"], .breadcrumb, .breadcrumbs').length > 0;
482
655
  const hasBreadcrumbsSchema = jsonLdTypes.includes('BreadcrumbList');
483
656
  return { hasBreadcrumbsHtml, hasBreadcrumbsSchema };
484
657
  }
485
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);
486
671
  return {
487
- videoCount: this.$('video').length,
488
- audioCount: this.$('audio').length,
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,
489
755
  };
490
756
  }
491
757
  analyzeTrustSignals(links) {
492
- const linkHrefs = links.map(l => l.href.toLowerCase());
758
+ const linkHrefs = links.map((l) => l.href.toLowerCase());
493
759
  return {
494
- hasAboutPageLink: linkHrefs.some(href => href.includes('about') || href.includes('quem-somos')),
495
- hasContactPageLink: linkHrefs.some(href => href.includes('contact') || href.includes('contato')),
496
- hasPrivacyPolicyLink: linkHrefs.some(href => href.includes('privacy') || href.includes('privacidade')),
497
- hasTermsOfServiceLink: linkHrefs.some(href => href.includes('terms') || href.includes('termos-de-uso')),
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')),
498
764
  };
499
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
+ }
500
908
  calculateTextHtmlRatio(bodyTextLength) {
501
- const htmlSize = this.$('html').html()?.length;
909
+ const htmlSize = this.root.innerHTML?.length;
502
910
  if (htmlSize && htmlSize > 0) {
503
911
  return (bodyTextLength / htmlSize) * 100;
504
912
  }
@@ -507,6 +915,7 @@ export class SeoAnalyzer {
507
915
  convertToCheckResults(results) {
508
916
  return results.map((r) => ({
509
917
  name: r.name,
918
+ category: r.category,
510
919
  status: r.status,
511
920
  message: r.message,
512
921
  value: r.value,
@@ -515,10 +924,10 @@ export class SeoAnalyzer {
515
924
  }));
516
925
  }
517
926
  buildSummary(ruleResults, checks, data) {
518
- const passed = checks.filter(c => c.status === 'pass').length;
519
- const warnings = checks.filter(c => c.status === 'warn').length;
520
- const errors = checks.filter(c => c.status === 'fail').length;
521
- const infos = checks.filter(c => c.status === 'info').length;
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;
522
931
  const totalChecks = checks.length;
523
932
  const scoringChecks = totalChecks - infos;
524
933
  const passRate = scoringChecks > 0 ? Math.round((passed / scoringChecks) * 100) : 100;
@@ -536,7 +945,7 @@ export class SeoAnalyzer {
536
945
  issuesByCategory[cat].errors++;
537
946
  }
538
947
  const topIssues = ruleResults
539
- .filter(r => r.status === 'fail' || r.status === 'warn')
948
+ .filter((r) => r.status === 'fail' || r.status === 'warn')
540
949
  .sort((a, b) => {
541
950
  if (a.status === 'fail' && b.status !== 'fail')
542
951
  return -1;
@@ -545,7 +954,7 @@ export class SeoAnalyzer {
545
954
  return 0;
546
955
  })
547
956
  .slice(0, 5)
548
- .map(r => ({
957
+ .map((r) => ({
549
958
  name: r.name,
550
959
  message: r.message,
551
960
  category: r.category,
@@ -569,8 +978,8 @@ export class SeoAnalyzer {
569
978
  if (data.linkAnalysis.withoutText > 0)
570
979
  quickWins.push(`Add text to ${data.linkAnalysis.withoutText} empty link(s)`);
571
980
  const limitedQuickWins = quickWins.slice(0, 5);
572
- const htmlSize = this.$('html').html()?.length;
573
- const domElements = this.$('*').length;
981
+ const htmlSize = this.root.innerHTML?.length;
982
+ const domElements = this.root.querySelectorAll('*').length;
574
983
  const vitals = {
575
984
  htmlSize,
576
985
  domElements,
@@ -703,14 +1112,23 @@ export class SeoAnalyzer {
703
1112
  const issues = [];
704
1113
  const structure = [];
705
1114
  const sectionWordCounts = [];
706
- const counts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
1115
+ const counts = {
1116
+ 1: 0,
1117
+ 2: 0,
1118
+ 3: 0,
1119
+ 4: 0,
1120
+ 5: 0,
1121
+ 6: 0,
1122
+ };
707
1123
  let currentSectionWordCount = 0;
708
1124
  let inSection = false;
709
- this.$('body').find('*').each((_, el) => {
1125
+ const body = this.getMainBody();
1126
+ const allElements = body ? body.querySelectorAll('*') : [];
1127
+ allElements.forEach((el) => {
710
1128
  const tagName = el.tagName.toLowerCase();
711
1129
  if (tagName.match(/^h[1-6]$/)) {
712
1130
  const level = parseInt(tagName.substring(1), 10);
713
- const text = this.$(el).text().trim();
1131
+ const text = el.text.trim();
714
1132
  counts[level] = (counts[level] || 0) + 1;
715
1133
  structure.push({ level, text: text.slice(0, 80), count: 1 });
716
1134
  if (level === 2) {
@@ -721,8 +1139,14 @@ export class SeoAnalyzer {
721
1139
  inSection = true;
722
1140
  }
723
1141
  }
724
- else if (inSection && ['p', 'ul', 'ol', 'div', 'article', 'section'].includes(tagName)) {
725
- const text = this.$(el).clone().children().remove().end().text().trim();
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();
726
1150
  if (text.length > 0) {
727
1151
  currentSectionWordCount += text.split(/\s+/).length;
728
1152
  }
@@ -755,18 +1179,19 @@ export class SeoAnalyzer {
755
1179
  };
756
1180
  }
757
1181
  analyzeContent(headings) {
758
- const $body = this.$('body').clone();
759
- $body.find('script, style, noscript, svg, header, footer, nav').remove();
760
- const bodyText = $body.text().replace(/\s+/g, ' ').trim();
1182
+ const body = this.getMainBody();
1183
+ const bodyText = body ? body.text.replace(/\s+/g, ' ').trim() : '';
761
1184
  const words = bodyText.split(/\s+/).filter((w) => w.length > 0);
762
- const sentences = bodyText.split(/[.!?]+/).filter((s) => s.trim().length > 0);
763
- const paragraphs = this.$('p');
1185
+ const sentences = bodyText
1186
+ .split(/[.!?]+/)
1187
+ .filter((s) => s.trim().length > 0);
1188
+ const paragraphs = this.root.querySelectorAll('p');
764
1189
  let totalParagraphLength = 0;
765
1190
  const paragraphWordCounts = [];
766
- paragraphs.each((_, el) => {
767
- const text = this.$(el).text().trim();
1191
+ paragraphs.forEach((el) => {
1192
+ const text = el.text.trim();
768
1193
  totalParagraphLength += text.length;
769
- const pWords = text.split(/\s+/).filter(w => w.length > 0).length;
1194
+ const pWords = text.split(/\s+/).filter((w) => w.length > 0).length;
770
1195
  if (pWords > 0)
771
1196
  paragraphWordCounts.push(pWords);
772
1197
  });
@@ -774,15 +1199,17 @@ export class SeoAnalyzer {
774
1199
  const readingTimeMinutes = Math.ceil(wordCount / 200);
775
1200
  const avgWordsPerSentence = sentences.length > 0 ? Math.round(wordCount / sentences.length) : 0;
776
1201
  let faqCount = 0;
777
- headings.structure.forEach(h => {
778
- if (h.level === 3 && /^(what|how|why|when|where|who|can|do|is|are)\b/i.test(h.text)) {
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)) {
779
1205
  faqCount++;
780
1206
  }
781
1207
  });
782
- const imageCount = this.$('img').length;
1208
+ const imageCount = this.root.querySelectorAll('img').length;
783
1209
  const imagePerWordRatio = wordCount > 0 ? imageCount / wordCount : 0;
784
1210
  const fleschReadingEase = undefined;
785
- const hasQuestionHeadings = headings.structure.some((h) => (h.level === 2 || h.level === 3) && /^(what|how|why|when|where|who|can|do|is|are)\b/i.test(h.text));
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));
786
1213
  return {
787
1214
  wordCount,
788
1215
  characterCount: bodyText.length,
@@ -790,10 +1217,12 @@ export class SeoAnalyzer {
790
1217
  paragraphCount: paragraphs.length,
791
1218
  readingTimeMinutes,
792
1219
  avgWordsPerSentence,
793
- avgParagraphLength: paragraphs.length > 0 ? Math.round(totalParagraphLength / paragraphs.length) : 0,
794
- listCount: this.$('ul, ol').length,
795
- strongTagCount: this.$('strong').length,
796
- emTagCount: this.$('em').length,
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,
797
1226
  paragraphWordCounts,
798
1227
  avgSentenceLength: avgWordsPerSentence,
799
1228
  faqCount,
@@ -803,6 +1232,9 @@ export class SeoAnalyzer {
803
1232
  };
804
1233
  }
805
1234
  buildLinkAnalysis(links) {
1235
+ const internalHttpLinkUrls = links
1236
+ .filter((l) => l.type === 'internal' && l.href.startsWith('http://'))
1237
+ .map((l) => l.href);
806
1238
  return {
807
1239
  total: links.length,
808
1240
  internal: links.filter((l) => l.type === 'internal').length,
@@ -812,6 +1244,8 @@ export class SeoAnalyzer {
812
1244
  ugcLinks: links.filter((l) => l.rel?.includes('ugc')).length,
813
1245
  broken: 0,
814
1246
  withoutText: links.filter((l) => !l.text?.trim()).length,
1247
+ internalHttpLinks: internalHttpLinkUrls.length,
1248
+ internalHttpLinkUrls,
815
1249
  };
816
1250
  }
817
1251
  buildImageAnalysis(images) {
@@ -821,9 +1255,12 @@ export class SeoAnalyzer {
821
1255
  withoutAlt: images.filter((i) => !i.alt || i.alt.trim().length === 0).length,
822
1256
  lazy: images.filter((i) => i.loading === 'lazy').length,
823
1257
  missingDimensions: images.filter((i) => !i.width || !i.height).length,
824
- modernFormats: images.filter((i) => /\.(webp|avif)$/i.test(i.src)).length,
825
- altTextLengths: images.filter(i => i.alt).map(i => i.alt.length),
826
- imageFilenames: images.map(i => {
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) => {
827
1264
  try {
828
1265
  const url = new URL(i.src);
829
1266
  return url.pathname.split('/').pop() || '';
@@ -831,8 +1268,10 @@ export class SeoAnalyzer {
831
1268
  catch {
832
1269
  return '';
833
1270
  }
834
- }).filter(Boolean),
835
- imagesWithAsyncDecoding: images.filter(i => i.decoding === 'async').length,
1271
+ })
1272
+ .filter(Boolean),
1273
+ imagesWithAsyncDecoding: images.filter((i) => i.decoding === 'async')
1274
+ .length,
836
1275
  };
837
1276
  }
838
1277
  buildSocialAnalysis(og, twitter) {
@@ -884,7 +1323,8 @@ export class SeoAnalyzer {
884
1323
  };
885
1324
  }
886
1325
  buildTechnicalAnalysis(meta) {
887
- const htmlLang = this.$('html').attr('lang');
1326
+ const html = this.root.querySelector('html');
1327
+ const htmlLang = html ? html.getAttribute('lang') : undefined;
888
1328
  return {
889
1329
  hasCanonical: !!meta.canonical,
890
1330
  canonicalUrl: meta.canonical,
@@ -896,6 +1336,27 @@ export class SeoAnalyzer {
896
1336
  langValue: htmlLang,
897
1337
  };
898
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
+ }
899
1360
  calculateScore(checks) {
900
1361
  const weights = {
901
1362
  pass: 100,
@@ -935,4 +1396,4 @@ export async function analyzeSeo(html, options = {}) {
935
1396
  const analyzer = await SeoAnalyzer.fromHtml(html, options);
936
1397
  return analyzer.analyze();
937
1398
  }
938
- export { SEO_THRESHOLDS, createRulesEngine, SeoRulesEngine } from './rules/index.js';
1399
+ export { SEO_THRESHOLDS, createRulesEngine, SeoRulesEngine, } from './rules/index.js';