recker 1.0.43 → 1.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (459) hide show
  1. package/README.md +47 -0
  2. package/dist/bin/recker-linux-x64 +0 -0
  3. package/dist/bin/recker-macos-x64 +0 -0
  4. package/dist/bin/recker-win-x64.exe +0 -0
  5. package/dist/bin/rek.cjs +85152 -100207
  6. package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
  7. package/dist/browser/ai/adaptive-timeout.js +208 -0
  8. package/dist/browser/ai/client.d.ts +22 -0
  9. package/dist/browser/ai/client.js +294 -0
  10. package/dist/browser/ai/index.d.ts +14 -0
  11. package/dist/browser/ai/index.js +11 -0
  12. package/dist/browser/ai/providers/anthropic.d.ts +63 -0
  13. package/dist/browser/ai/providers/anthropic.js +370 -0
  14. package/dist/browser/ai/providers/base.d.ts +48 -0
  15. package/dist/browser/ai/providers/base.js +150 -0
  16. package/dist/browser/ai/providers/google.d.ts +59 -0
  17. package/dist/browser/ai/providers/google.js +305 -0
  18. package/dist/browser/ai/providers/ollama.d.ts +44 -0
  19. package/dist/browser/ai/providers/ollama.js +240 -0
  20. package/dist/browser/ai/providers/openai.d.ts +64 -0
  21. package/dist/browser/ai/providers/openai.js +298 -0
  22. package/dist/browser/ai/rate-limiter.d.ts +43 -0
  23. package/dist/browser/ai/rate-limiter.js +215 -0
  24. package/dist/browser/ai/vector/index.d.ts +2 -0
  25. package/dist/browser/ai/vector/index.js +2 -0
  26. package/dist/browser/ai/vector/similarity.d.ts +2 -0
  27. package/dist/browser/ai/vector/similarity.js +27 -0
  28. package/dist/browser/ai/vector/store.d.ts +27 -0
  29. package/dist/browser/ai/vector/store.js +82 -0
  30. package/dist/browser/browser/cache.d.ts +2 -40
  31. package/dist/browser/browser/cache.js +2 -199
  32. package/dist/browser/browser/index.d.ts +8 -0
  33. package/dist/browser/browser/index.js +8 -0
  34. package/dist/browser/browser/recker.d.ts +8 -1
  35. package/dist/browser/browser/recker.js +8 -2
  36. package/dist/browser/cache/indexed-db.d.ts +10 -0
  37. package/dist/browser/cache/indexed-db.js +88 -0
  38. package/dist/browser/cache/service-worker-cache.d.ts +18 -0
  39. package/dist/browser/cache/service-worker-cache.js +103 -0
  40. package/dist/browser/cache.d.ts +2 -40
  41. package/dist/browser/cache.js +2 -199
  42. package/dist/browser/constants/user-agents.d.ts +7 -0
  43. package/dist/browser/constants/user-agents.js +7 -0
  44. package/dist/browser/core/client.d.ts +2 -0
  45. package/dist/browser/core/client.js +19 -1
  46. package/dist/browser/index.d.ts +8 -0
  47. package/dist/browser/index.js +8 -0
  48. package/dist/browser/plugins/har-recorder.d.ts +40 -0
  49. package/dist/browser/plugins/har-recorder.js +120 -0
  50. package/dist/browser/plugins/network-simulation.d.ts +7 -0
  51. package/dist/browser/plugins/network-simulation.js +13 -0
  52. package/dist/browser/presets/android.d.ts +2 -0
  53. package/dist/browser/presets/android.js +16 -0
  54. package/dist/browser/presets/anthropic.d.ts +8 -0
  55. package/dist/browser/presets/anthropic.js +27 -0
  56. package/dist/browser/presets/aws.d.ts +19 -0
  57. package/dist/browser/presets/aws.js +68 -0
  58. package/dist/browser/presets/azure-openai.d.ts +10 -0
  59. package/dist/browser/presets/azure-openai.js +35 -0
  60. package/dist/browser/presets/azure.d.ts +41 -0
  61. package/dist/browser/presets/azure.js +104 -0
  62. package/dist/browser/presets/chaturbate.d.ts +2 -0
  63. package/dist/browser/presets/chaturbate.js +17 -0
  64. package/dist/browser/presets/cloudflare.d.ts +12 -0
  65. package/dist/browser/presets/cloudflare.js +39 -0
  66. package/dist/browser/presets/cohere.d.ts +7 -0
  67. package/dist/browser/presets/cohere.js +22 -0
  68. package/dist/browser/presets/deepseek.d.ts +7 -0
  69. package/dist/browser/presets/deepseek.js +22 -0
  70. package/dist/browser/presets/digitalocean.d.ts +5 -0
  71. package/dist/browser/presets/digitalocean.js +16 -0
  72. package/dist/browser/presets/discord.d.ts +6 -0
  73. package/dist/browser/presets/discord.js +17 -0
  74. package/dist/browser/presets/elevenlabs.d.ts +6 -0
  75. package/dist/browser/presets/elevenlabs.js +20 -0
  76. package/dist/browser/presets/enhancers.d.ts +20 -0
  77. package/dist/browser/presets/enhancers.js +85 -0
  78. package/dist/browser/presets/fireworks.d.ts +7 -0
  79. package/dist/browser/presets/fireworks.js +22 -0
  80. package/dist/browser/presets/gcp.d.ts +34 -0
  81. package/dist/browser/presets/gcp.js +91 -0
  82. package/dist/browser/presets/gemini.d.ts +7 -0
  83. package/dist/browser/presets/gemini.js +23 -0
  84. package/dist/browser/presets/github.d.ts +6 -0
  85. package/dist/browser/presets/github.js +17 -0
  86. package/dist/browser/presets/gitlab.d.ts +6 -0
  87. package/dist/browser/presets/gitlab.js +16 -0
  88. package/dist/browser/presets/groq.d.ts +7 -0
  89. package/dist/browser/presets/groq.js +22 -0
  90. package/dist/browser/presets/hubspot.d.ts +9 -0
  91. package/dist/browser/presets/hubspot.js +28 -0
  92. package/dist/browser/presets/huggingface.d.ts +7 -0
  93. package/dist/browser/presets/huggingface.js +23 -0
  94. package/dist/browser/presets/index.d.ts +47 -0
  95. package/dist/browser/presets/index.js +47 -0
  96. package/dist/browser/presets/ios.d.ts +2 -0
  97. package/dist/browser/presets/ios.js +13 -0
  98. package/dist/browser/presets/linear.d.ts +5 -0
  99. package/dist/browser/presets/linear.js +16 -0
  100. package/dist/browser/presets/mailgun.d.ts +7 -0
  101. package/dist/browser/presets/mailgun.js +20 -0
  102. package/dist/browser/presets/meta.d.ts +10 -0
  103. package/dist/browser/presets/meta.js +33 -0
  104. package/dist/browser/presets/mistral.d.ts +7 -0
  105. package/dist/browser/presets/mistral.js +22 -0
  106. package/dist/browser/presets/notion.d.ts +6 -0
  107. package/dist/browser/presets/notion.js +17 -0
  108. package/dist/browser/presets/openai.d.ts +9 -0
  109. package/dist/browser/presets/openai.js +30 -0
  110. package/dist/browser/presets/oracle.d.ts +19 -0
  111. package/dist/browser/presets/oracle.js +117 -0
  112. package/dist/browser/presets/perplexity.d.ts +7 -0
  113. package/dist/browser/presets/perplexity.js +22 -0
  114. package/dist/browser/presets/pinecone.d.ts +8 -0
  115. package/dist/browser/presets/pinecone.js +42 -0
  116. package/dist/browser/presets/registry.d.ts +23 -0
  117. package/dist/browser/presets/registry.js +519 -0
  118. package/dist/browser/presets/replicate.d.ts +7 -0
  119. package/dist/browser/presets/replicate.js +23 -0
  120. package/dist/browser/presets/sendgrid.d.ts +6 -0
  121. package/dist/browser/presets/sendgrid.js +20 -0
  122. package/dist/browser/presets/sentry.d.ts +11 -0
  123. package/dist/browser/presets/sentry.js +48 -0
  124. package/dist/browser/presets/sinch.d.ts +9 -0
  125. package/dist/browser/presets/sinch.js +39 -0
  126. package/dist/browser/presets/slack.d.ts +5 -0
  127. package/dist/browser/presets/slack.js +16 -0
  128. package/dist/browser/presets/square.d.ts +10 -0
  129. package/dist/browser/presets/square.js +33 -0
  130. package/dist/browser/presets/stripe.d.ts +7 -0
  131. package/dist/browser/presets/stripe.js +23 -0
  132. package/dist/browser/presets/supabase.d.ts +6 -0
  133. package/dist/browser/presets/supabase.js +18 -0
  134. package/dist/browser/presets/tiktok.d.ts +10 -0
  135. package/dist/browser/presets/tiktok.js +38 -0
  136. package/dist/browser/presets/together.d.ts +7 -0
  137. package/dist/browser/presets/together.js +22 -0
  138. package/dist/browser/presets/twilio.d.ts +6 -0
  139. package/dist/browser/presets/twilio.js +17 -0
  140. package/dist/browser/presets/vercel.d.ts +6 -0
  141. package/dist/browser/presets/vercel.js +23 -0
  142. package/dist/browser/presets/vultr.d.ts +5 -0
  143. package/dist/browser/presets/vultr.js +16 -0
  144. package/dist/browser/presets/xai.d.ts +8 -0
  145. package/dist/browser/presets/xai.js +23 -0
  146. package/dist/browser/presets/youtube.d.ts +5 -0
  147. package/dist/browser/presets/youtube.js +20 -0
  148. package/dist/browser/recker.d.ts +8 -1
  149. package/dist/browser/recker.js +8 -2
  150. package/dist/browser/scrape/document.d.ts +5 -4
  151. package/dist/browser/scrape/document.js +89 -76
  152. package/dist/browser/scrape/element.d.ts +10 -8
  153. package/dist/browser/scrape/element.js +295 -81
  154. package/dist/browser/scrape/extractors.d.ts +11 -11
  155. package/dist/browser/scrape/extractors.js +145 -113
  156. package/dist/browser/scrape/parser/back.d.ts +1 -0
  157. package/dist/browser/scrape/parser/back.js +3 -0
  158. package/dist/browser/scrape/parser/index.d.ts +20 -0
  159. package/dist/browser/scrape/parser/index.js +19 -0
  160. package/dist/browser/scrape/parser/matcher.d.ts +30 -0
  161. package/dist/browser/scrape/parser/matcher.js +99 -0
  162. package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
  163. package/dist/browser/scrape/parser/nodes/comment.js +21 -0
  164. package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
  165. package/dist/browser/scrape/parser/nodes/html.js +978 -0
  166. package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
  167. package/dist/browser/scrape/parser/nodes/node.js +31 -0
  168. package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
  169. package/dist/browser/scrape/parser/nodes/text.js +30 -0
  170. package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
  171. package/dist/browser/scrape/parser/nodes/type.js +7 -0
  172. package/dist/browser/scrape/parser/parse.d.ts +1 -0
  173. package/dist/browser/scrape/parser/parse.js +1 -0
  174. package/dist/browser/scrape/parser/valid.d.ts +2 -0
  175. package/dist/browser/scrape/parser/valid.js +5 -0
  176. package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
  177. package/dist/browser/scrape/parser/void-tag.js +43 -0
  178. package/dist/browser/scrape/types.d.ts +7 -0
  179. package/dist/browser/seo/analyzer.d.ts +59 -0
  180. package/dist/browser/seo/analyzer.js +1399 -0
  181. package/dist/browser/seo/keywords.d.ts +16 -0
  182. package/dist/browser/seo/keywords.js +55 -0
  183. package/dist/browser/seo/rules/accessibility.d.ts +2 -0
  184. package/dist/browser/seo/rules/accessibility.js +733 -0
  185. package/dist/browser/seo/rules/ai-search.d.ts +2 -0
  186. package/dist/browser/seo/rules/ai-search.js +436 -0
  187. package/dist/browser/seo/rules/analytics.d.ts +2 -0
  188. package/dist/browser/seo/rules/analytics.js +306 -0
  189. package/dist/browser/seo/rules/best-practices.d.ts +2 -0
  190. package/dist/browser/seo/rules/best-practices.js +195 -0
  191. package/dist/browser/seo/rules/canonical.d.ts +12 -0
  192. package/dist/browser/seo/rules/canonical.js +270 -0
  193. package/dist/browser/seo/rules/content.d.ts +2 -0
  194. package/dist/browser/seo/rules/content.js +522 -0
  195. package/dist/browser/seo/rules/crawl.d.ts +2 -0
  196. package/dist/browser/seo/rules/crawl.js +435 -0
  197. package/dist/browser/seo/rules/cwv.d.ts +2 -0
  198. package/dist/browser/seo/rules/cwv.js +248 -0
  199. package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
  200. package/dist/browser/seo/rules/ecommerce.js +312 -0
  201. package/dist/browser/seo/rules/i18n.d.ts +2 -0
  202. package/dist/browser/seo/rules/i18n.js +288 -0
  203. package/dist/browser/seo/rules/images.d.ts +2 -0
  204. package/dist/browser/seo/rules/images.js +255 -0
  205. package/dist/browser/seo/rules/index.d.ts +52 -0
  206. package/dist/browser/seo/rules/index.js +159 -0
  207. package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
  208. package/dist/browser/seo/rules/internal-linking.js +394 -0
  209. package/dist/browser/seo/rules/links.d.ts +2 -0
  210. package/dist/browser/seo/rules/links.js +498 -0
  211. package/dist/browser/seo/rules/local.d.ts +2 -0
  212. package/dist/browser/seo/rules/local.js +289 -0
  213. package/dist/browser/seo/rules/meta.d.ts +2 -0
  214. package/dist/browser/seo/rules/meta.js +805 -0
  215. package/dist/browser/seo/rules/mobile.d.ts +2 -0
  216. package/dist/browser/seo/rules/mobile.js +161 -0
  217. package/dist/browser/seo/rules/performance.d.ts +2 -0
  218. package/dist/browser/seo/rules/performance.js +738 -0
  219. package/dist/browser/seo/rules/pwa.d.ts +2 -0
  220. package/dist/browser/seo/rules/pwa.js +299 -0
  221. package/dist/browser/seo/rules/readability.d.ts +2 -0
  222. package/dist/browser/seo/rules/readability.js +264 -0
  223. package/dist/browser/seo/rules/redirects.d.ts +16 -0
  224. package/dist/browser/seo/rules/redirects.js +199 -0
  225. package/dist/browser/seo/rules/resources.d.ts +2 -0
  226. package/dist/browser/seo/rules/resources.js +390 -0
  227. package/dist/browser/seo/rules/schema.d.ts +2 -0
  228. package/dist/browser/seo/rules/schema.js +379 -0
  229. package/dist/browser/seo/rules/security.d.ts +2 -0
  230. package/dist/browser/seo/rules/security.js +877 -0
  231. package/dist/browser/seo/rules/social.d.ts +2 -0
  232. package/dist/browser/seo/rules/social.js +603 -0
  233. package/dist/browser/seo/rules/structural.d.ts +2 -0
  234. package/dist/browser/seo/rules/structural.js +223 -0
  235. package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
  236. package/dist/browser/seo/rules/technical-advanced.js +289 -0
  237. package/dist/browser/seo/rules/technical.d.ts +2 -0
  238. package/dist/browser/seo/rules/technical.js +480 -0
  239. package/dist/browser/seo/rules/thresholds.d.ts +196 -0
  240. package/dist/browser/seo/rules/thresholds.js +118 -0
  241. package/dist/browser/seo/rules/types.d.ts +498 -0
  242. package/dist/browser/seo/rules/types.js +11 -0
  243. package/dist/browser/seo/types.d.ts +211 -0
  244. package/dist/browser/seo/types.js +1 -0
  245. package/dist/browser/transport/curl.d.ts +4 -0
  246. package/dist/browser/transport/curl.js +101 -0
  247. package/dist/browser/transport/undici.js +1 -2
  248. package/dist/browser/transport/worker.d.ts +18 -0
  249. package/dist/browser/transport/worker.js +278 -0
  250. package/dist/browser/types/index.d.ts +4 -1
  251. package/dist/browser/utils/binary-manager.d.ts +4 -0
  252. package/dist/browser/utils/binary-manager.js +72 -0
  253. package/dist/browser/utils/user-agent.js +2 -13
  254. package/dist/cache/indexed-db.d.ts +10 -0
  255. package/dist/cache/indexed-db.js +88 -0
  256. package/dist/cache/service-worker-cache.d.ts +18 -0
  257. package/dist/cache/service-worker-cache.js +103 -0
  258. package/dist/cli/commands/ai.d.ts +2 -0
  259. package/dist/cli/commands/ai.js +162 -0
  260. package/dist/cli/commands/bench.d.ts +2 -0
  261. package/dist/cli/commands/bench.js +51 -0
  262. package/dist/cli/commands/dns.d.ts +2 -0
  263. package/dist/cli/commands/dns.js +295 -0
  264. package/dist/cli/commands/har.d.ts +2 -0
  265. package/dist/cli/commands/har.js +171 -0
  266. package/dist/cli/commands/hls.d.ts +2 -0
  267. package/dist/cli/commands/hls.js +192 -0
  268. package/dist/cli/commands/network.d.ts +2 -0
  269. package/dist/cli/commands/network.js +288 -0
  270. package/dist/cli/commands/protocols.d.ts +2 -0
  271. package/dist/cli/commands/protocols.js +344 -0
  272. package/dist/cli/commands/scrape.d.ts +2 -0
  273. package/dist/cli/commands/scrape.js +176 -0
  274. package/dist/cli/commands/security.d.ts +2 -0
  275. package/dist/cli/commands/security.js +57 -0
  276. package/dist/cli/commands/seo.d.ts +2 -0
  277. package/dist/cli/commands/seo.js +125 -0
  278. package/dist/cli/commands/serve.d.ts +2 -0
  279. package/dist/cli/commands/serve.js +531 -0
  280. package/dist/cli/commands/spider.d.ts +3 -0
  281. package/dist/cli/commands/spider.js +456 -0
  282. package/dist/cli/commands/utils.d.ts +2 -0
  283. package/dist/cli/commands/utils.js +176 -0
  284. package/dist/cli/commands/vector.d.ts +2 -0
  285. package/dist/cli/commands/vector.js +158 -0
  286. package/dist/cli/handler.d.ts +2 -2
  287. package/dist/cli/handler.js +6 -6
  288. package/dist/cli/helpers.d.ts +7 -0
  289. package/dist/cli/helpers.js +128 -0
  290. package/dist/cli/index.js +96 -5228
  291. package/dist/cli/parser/help.d.ts +2 -0
  292. package/dist/cli/parser/help.js +52 -0
  293. package/dist/cli/parser/index.d.ts +3 -0
  294. package/dist/cli/parser/index.js +3 -0
  295. package/dist/cli/parser/parser.d.ts +4 -0
  296. package/dist/cli/parser/parser.js +146 -0
  297. package/dist/cli/parser/types.d.ts +41 -0
  298. package/dist/cli/parser/types.js +1 -0
  299. package/dist/cli/presets.d.ts +1 -1
  300. package/dist/cli/presets.js +1 -1
  301. package/dist/cli/router.d.ts +36 -0
  302. package/dist/cli/router.js +195 -0
  303. package/dist/cli/tui/ai-chat.js +1 -1
  304. package/dist/cli/tui/commands/context.d.ts +9 -0
  305. package/dist/cli/tui/commands/context.js +1 -0
  306. package/dist/cli/tui/commands/dns.d.ts +10 -0
  307. package/dist/cli/tui/commands/dns.js +461 -0
  308. package/dist/cli/tui/commands/hls.d.ts +2 -0
  309. package/dist/cli/tui/commands/hls.js +162 -0
  310. package/dist/cli/tui/commands/ip.d.ts +2 -0
  311. package/dist/cli/tui/commands/ip.js +45 -0
  312. package/dist/cli/tui/commands/network.d.ts +3 -0
  313. package/dist/cli/tui/commands/network.js +81 -0
  314. package/dist/cli/tui/commands/protocols.d.ts +6 -0
  315. package/dist/cli/tui/commands/protocols.js +531 -0
  316. package/dist/cli/tui/commands/security.d.ts +2 -0
  317. package/dist/cli/tui/commands/security.js +48 -0
  318. package/dist/cli/tui/commands/seo.d.ts +2 -0
  319. package/dist/cli/tui/commands/seo.js +74 -0
  320. package/dist/cli/tui/context.d.ts +12 -0
  321. package/dist/cli/tui/context.js +1 -0
  322. package/dist/cli/tui/shell.d.ts +11 -20
  323. package/dist/cli/tui/shell.js +216 -1873
  324. package/dist/constants/user-agents.d.ts +7 -0
  325. package/dist/constants/user-agents.js +7 -0
  326. package/dist/core/client.d.ts +2 -0
  327. package/dist/core/client.js +19 -1
  328. package/dist/index.d.ts +1 -0
  329. package/dist/index.js +1 -0
  330. package/dist/mcp/cli.js +2 -3
  331. package/dist/mcp/data/embeddings.json +1 -1
  332. package/dist/mcp/tools/network.js +298 -158
  333. package/dist/plugins/har-player.d.ts +23 -0
  334. package/dist/plugins/har-player.js +49 -0
  335. package/dist/plugins/har-recorder.d.ts +37 -3
  336. package/dist/plugins/har-recorder.js +116 -63
  337. package/dist/plugins/network-simulation.d.ts +7 -0
  338. package/dist/plugins/network-simulation.js +13 -0
  339. package/dist/presets/android.d.ts +2 -0
  340. package/dist/presets/android.js +16 -0
  341. package/dist/presets/chaturbate.d.ts +2 -0
  342. package/dist/presets/chaturbate.js +17 -0
  343. package/dist/presets/elevenlabs.d.ts +6 -0
  344. package/dist/presets/elevenlabs.js +20 -0
  345. package/dist/presets/enhancers.d.ts +20 -0
  346. package/dist/presets/enhancers.js +85 -0
  347. package/dist/presets/hubspot.d.ts +9 -0
  348. package/dist/presets/hubspot.js +28 -0
  349. package/dist/presets/index.d.ts +10 -0
  350. package/dist/presets/index.js +10 -0
  351. package/dist/presets/ios.d.ts +2 -0
  352. package/dist/presets/ios.js +13 -0
  353. package/dist/presets/pinecone.d.ts +8 -0
  354. package/dist/presets/pinecone.js +42 -0
  355. package/dist/presets/registry.js +60 -0
  356. package/dist/presets/sendgrid.d.ts +6 -0
  357. package/dist/presets/sendgrid.js +20 -0
  358. package/dist/presets/sentry.d.ts +11 -0
  359. package/dist/presets/sentry.js +48 -0
  360. package/dist/presets/square.d.ts +10 -0
  361. package/dist/presets/square.js +33 -0
  362. package/dist/recker.d.ts +3 -0
  363. package/dist/recker.js +4 -0
  364. package/dist/scrape/document.d.ts +5 -4
  365. package/dist/scrape/document.js +89 -76
  366. package/dist/scrape/element.d.ts +10 -8
  367. package/dist/scrape/element.js +295 -81
  368. package/dist/scrape/extractors.d.ts +11 -11
  369. package/dist/scrape/extractors.js +145 -113
  370. package/dist/scrape/index.d.ts +2 -0
  371. package/dist/scrape/index.js +1 -0
  372. package/dist/scrape/parser/back.d.ts +1 -0
  373. package/dist/scrape/parser/back.js +3 -0
  374. package/dist/scrape/parser/index.d.ts +20 -0
  375. package/dist/scrape/parser/index.js +19 -0
  376. package/dist/scrape/parser/matcher.d.ts +30 -0
  377. package/dist/scrape/parser/matcher.js +99 -0
  378. package/dist/scrape/parser/nodes/comment.d.ts +12 -0
  379. package/dist/scrape/parser/nodes/comment.js +21 -0
  380. package/dist/scrape/parser/nodes/html.d.ts +110 -0
  381. package/dist/scrape/parser/nodes/html.js +978 -0
  382. package/dist/scrape/parser/nodes/node.d.ts +18 -0
  383. package/dist/scrape/parser/nodes/node.js +31 -0
  384. package/dist/scrape/parser/nodes/text.d.ts +14 -0
  385. package/dist/scrape/parser/nodes/text.js +30 -0
  386. package/dist/scrape/parser/nodes/type.d.ts +6 -0
  387. package/dist/scrape/parser/nodes/type.js +7 -0
  388. package/dist/scrape/parser/parse.d.ts +1 -0
  389. package/dist/scrape/parser/parse.js +1 -0
  390. package/dist/scrape/parser/valid.d.ts +2 -0
  391. package/dist/scrape/parser/valid.js +5 -0
  392. package/dist/scrape/parser/void-tag.d.ts +7 -0
  393. package/dist/scrape/parser/void-tag.js +43 -0
  394. package/dist/scrape/spider.d.ts +19 -0
  395. package/dist/scrape/spider.js +28 -3
  396. package/dist/scrape/types.d.ts +7 -0
  397. package/dist/seo/analyzer.d.ts +15 -5
  398. package/dist/seo/analyzer.js +636 -175
  399. package/dist/seo/formatter.d.ts +16 -0
  400. package/dist/seo/formatter.js +228 -0
  401. package/dist/seo/index.d.ts +2 -0
  402. package/dist/seo/index.js +1 -0
  403. package/dist/seo/keywords.d.ts +16 -0
  404. package/dist/seo/keywords.js +55 -0
  405. package/dist/seo/rules/accessibility.js +96 -57
  406. package/dist/seo/rules/ai-search.js +44 -31
  407. package/dist/seo/rules/analytics.d.ts +2 -0
  408. package/dist/seo/rules/analytics.js +306 -0
  409. package/dist/seo/rules/best-practices.js +21 -14
  410. package/dist/seo/rules/canonical.js +53 -32
  411. package/dist/seo/rules/content.js +317 -31
  412. package/dist/seo/rules/crawl.js +55 -40
  413. package/dist/seo/rules/cwv.js +21 -15
  414. package/dist/seo/rules/ecommerce.js +82 -22
  415. package/dist/seo/rules/i18n.js +75 -36
  416. package/dist/seo/rules/images.js +109 -30
  417. package/dist/seo/rules/index.js +2 -0
  418. package/dist/seo/rules/internal-linking.js +58 -39
  419. package/dist/seo/rules/links.js +79 -52
  420. package/dist/seo/rules/local.js +49 -25
  421. package/dist/seo/rules/meta.js +339 -81
  422. package/dist/seo/rules/mobile.js +112 -2
  423. package/dist/seo/rules/performance.js +434 -66
  424. package/dist/seo/rules/pwa.js +36 -39
  425. package/dist/seo/rules/readability.js +31 -22
  426. package/dist/seo/rules/redirects.js +21 -15
  427. package/dist/seo/rules/resources.js +59 -42
  428. package/dist/seo/rules/schema.js +333 -8
  429. package/dist/seo/rules/security.js +142 -80
  430. package/dist/seo/rules/social.js +277 -47
  431. package/dist/seo/rules/structural.js +87 -19
  432. package/dist/seo/rules/technical-advanced.js +30 -24
  433. package/dist/seo/rules/technical.js +243 -42
  434. package/dist/seo/rules/types.d.ts +53 -1
  435. package/dist/seo/seo-spider.d.ts +22 -0
  436. package/dist/seo/seo-spider.js +77 -13
  437. package/dist/seo/types.d.ts +8 -1
  438. package/dist/seo/validators/llms-txt.js +19 -0
  439. package/dist/seo/validators/rss.d.ts +11 -0
  440. package/dist/seo/validators/rss.js +93 -0
  441. package/dist/seo/validators/sitemap.js +36 -26
  442. package/dist/transport/curl.d.ts +4 -0
  443. package/dist/transport/curl.js +101 -0
  444. package/dist/transport/udp.js +0 -1
  445. package/dist/transport/undici.js +1 -2
  446. package/dist/transport/worker.d.ts +18 -0
  447. package/dist/transport/worker.js +278 -0
  448. package/dist/types/index.d.ts +4 -1
  449. package/dist/utils/binary-manager.d.ts +4 -0
  450. package/dist/utils/binary-manager.js +72 -0
  451. package/dist/utils/optional-require.d.ts +7 -8
  452. package/dist/utils/optional-require.js +2 -21
  453. package/dist/utils/upload.d.ts +6 -0
  454. package/dist/utils/upload.js +11 -0
  455. package/dist/utils/user-agent.js +2 -13
  456. package/dist/version.js +1 -1
  457. package/package.json +12 -6
  458. package/dist/browser/utils/optional-require.d.ts +0 -19
  459. package/dist/browser/utils/optional-require.js +0 -105
@@ -19,7 +19,7 @@ export const metaRules = [
19
19
  },
20
20
  });
21
21
  }
22
- return null;
22
+ return createResult({ id: 'title-exists', name: 'Title Tag Exists', category: 'title', severity: 'error' }, 'info', 'Not applicable (page has title tag)', { recommendation: 'This rule checks for the presence of a title tag' });
23
23
  },
24
24
  },
25
25
  {
@@ -29,15 +29,32 @@ export const metaRules = [
29
29
  severity: 'warning',
30
30
  description: 'Title should be between 50-60 characters',
31
31
  check: (ctx) => {
32
- if (!ctx.title)
33
- return null;
32
+ if (!ctx.title) {
33
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks title length when a title tag is present' });
34
+ }
34
35
  const len = ctx.titleLength ?? ctx.title.length;
35
36
  const { min, ideal, max } = SEO_THRESHOLDS.title;
36
37
  if (len < min) {
37
- return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too short (${len} chars, min: ${min})`, { value: len, recommendation: `Expand title to ${ideal.min}-${ideal.max} characters` });
38
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too short (${len} chars, min: ${min})`, {
39
+ value: len,
40
+ recommendation: `Expand title to ${ideal.min}-${ideal.max} characters. Ensure it includes target keywords and encourages clicks.`,
41
+ evidence: {
42
+ found: `${len} characters`,
43
+ expected: `${ideal.min}-${ideal.max} characters`,
44
+ impact: 'Short titles limit keyword potential and may be replaced by Google.'
45
+ }
46
+ });
38
47
  }
39
48
  if (len > max) {
40
- return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too long (${len} chars, will be truncated after ~60)`, { value: len, recommendation: `Shorten title to under ${ideal.max} characters` });
49
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'warn', `Title too long (${len} chars, will be truncated after ~60)`, {
50
+ value: len,
51
+ recommendation: `Shorten title to under ${ideal.max} characters to ensure visibility in SERPs.`,
52
+ evidence: {
53
+ found: `${len} characters`,
54
+ expected: `< ${max} characters`,
55
+ impact: 'Truncated titles may lose click-through rate if key information is hidden.'
56
+ }
57
+ });
41
58
  }
42
59
  if (len >= ideal.min && len <= ideal.max) {
43
60
  return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'pass', `Title length ideal (${len} chars)`, { value: len });
@@ -52,14 +69,23 @@ export const metaRules = [
52
69
  severity: 'warning',
53
70
  description: 'Title should not be ALL CAPS',
54
71
  check: (ctx) => {
55
- if (!ctx.title)
56
- return null;
72
+ if (!ctx.title) {
73
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks title capitalization when a title tag is present' });
74
+ }
57
75
  const words = ctx.title.split(/\s+/).filter((w) => w.length > 3);
58
76
  const allCapsWords = words.filter((w) => w === w.toUpperCase() && /[A-Z]/.test(w));
59
77
  if (allCapsWords.length > words.length / 2) {
60
- return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'warn', 'Title appears to be ALL CAPS', { recommendation: 'Use title case or sentence case for better readability' });
78
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'warn', 'Title appears to be ALL CAPS', {
79
+ recommendation: 'Use title case or sentence case for better readability and click-through rate.',
80
+ evidence: {
81
+ found: ctx.title,
82
+ expected: 'Normal capitalization (Title Case or Sentence case)',
83
+ impact: 'ALL CAPS titles look spammy and may be ignored by users. Google may also rewrite them.',
84
+ example: 'Instead of "BUY SHOES ONLINE NOW", use "Buy Shoes Online - Free Shipping"'
85
+ }
86
+ });
61
87
  }
62
- return null;
88
+ return createResult({ id: 'title-no-caps', name: 'Title Case', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title uses proper capitalization)', { recommendation: 'This rule checks for excessive ALL CAPS usage in title' });
63
89
  },
64
90
  },
65
91
  {
@@ -69,14 +95,23 @@ export const metaRules = [
69
95
  severity: 'warning',
70
96
  description: 'Title and H1 should be similar but not identical',
71
97
  check: (ctx) => {
72
- if (!ctx.title || !ctx.h1Text)
73
- return null;
98
+ if (!ctx.title || !ctx.h1Text) {
99
+ return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'info', 'Not applicable (missing title or H1)', { recommendation: 'This rule compares title and H1 when both are present' });
100
+ }
74
101
  const titleNorm = ctx.title.toLowerCase().trim();
75
102
  const h1Norm = ctx.h1Text.toLowerCase().trim();
76
103
  if (titleNorm === h1Norm) {
77
- return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'warn', 'Title and H1 are identical', { recommendation: 'Consider making H1 slightly different from title for variety' });
104
+ return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'warn', 'Title and H1 are identical', {
105
+ recommendation: 'Consider making H1 slightly different from title for variety',
106
+ evidence: {
107
+ found: `Both are: "${ctx.title}"`,
108
+ expected: 'Similar but not identical text (variation helps SEO)',
109
+ impact: 'Identical title and H1 waste an opportunity to include related keywords and may appear redundant',
110
+ example: 'Title: "Buy Running Shoes Online - Free Shipping"\nH1: "Best Running Shoes - Shop Now"'
111
+ }
112
+ });
78
113
  }
79
- return null;
114
+ return createResult({ id: 'title-h1-different', name: 'Title vs H1', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title and H1 are different)', { recommendation: 'This rule checks if title and H1 are identical' });
80
115
  },
81
116
  },
82
117
  {
@@ -97,7 +132,7 @@ export const metaRules = [
97
132
  },
98
133
  });
99
134
  }
100
- return null;
135
+ return createResult({ id: 'meta-description-exists', name: 'Meta Description Exists', category: 'meta', severity: 'error' }, 'info', 'Not applicable (page has meta description)', { recommendation: 'This rule checks for the presence of a meta description' });
101
136
  },
102
137
  },
103
138
  {
@@ -107,15 +142,32 @@ export const metaRules = [
107
142
  severity: 'warning',
108
143
  description: 'Meta description should be 120-155 characters',
109
144
  check: (ctx) => {
110
- if (!ctx.metaDescription)
111
- return null;
145
+ if (!ctx.metaDescription) {
146
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'info', 'Not applicable (no meta description)', { recommendation: 'This rule checks meta description length when present' });
147
+ }
112
148
  const len = ctx.metaDescriptionLength ?? ctx.metaDescription.length;
113
149
  const { min, ideal, max } = SEO_THRESHOLDS.metaDescription;
114
150
  if (len < min) {
115
- return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description too short (${len} chars, min: ${min})`, { value: len, recommendation: `Expand to ${ideal.min}-${ideal.max} characters` });
151
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description too short (${len} chars, min: ${min})`, {
152
+ value: len,
153
+ recommendation: `Expand to ${ideal.min}-${ideal.max} characters. Summarize content and include keywords naturally.`,
154
+ evidence: {
155
+ found: `${len} characters`,
156
+ expected: `${ideal.min}-${ideal.max} characters`,
157
+ impact: 'Short descriptions may be ignored by search engines in favor of auto-generated snippets.'
158
+ }
159
+ });
116
160
  }
117
161
  if (len > max) {
118
- return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description may be truncated (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to under ${max} characters` });
162
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'warn', `Description may be truncated (${len} chars, max: ${max})`, {
163
+ value: len,
164
+ recommendation: `Shorten to under ${max} characters. Ensure the most important info is at the start.`,
165
+ evidence: {
166
+ found: `${len} characters`,
167
+ expected: `< ${max} characters`,
168
+ impact: 'Truncated descriptions look unprofessional and may lower CTR.'
169
+ }
170
+ });
119
171
  }
120
172
  if (len >= ideal.min && len <= ideal.max) {
121
173
  return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'pass', `Description length ideal (${len} chars)`, { value: len });
@@ -130,16 +182,25 @@ export const metaRules = [
130
182
  severity: 'info',
131
183
  description: 'Meta description should be unique and compelling',
132
184
  check: (ctx) => {
133
- if (!ctx.metaDescription)
134
- return null;
185
+ if (!ctx.metaDescription) {
186
+ return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'info', 'Not applicable (no meta description)', { recommendation: 'This rule checks meta description quality when present' });
187
+ }
135
188
  const desc = ctx.metaDescription.toLowerCase();
136
189
  const placeholders = ['lorem ipsum', 'description here', 'todo', 'placeholder', 'change this'];
137
190
  for (const placeholder of placeholders) {
138
191
  if (desc.includes(placeholder)) {
139
- return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'warn', 'Meta description appears to be a placeholder', { recommendation: 'Replace with a unique, compelling description for better CTR' });
192
+ return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'warn', 'Meta description appears to be a placeholder', {
193
+ recommendation: 'Replace with a unique, compelling description for better CTR',
194
+ evidence: {
195
+ found: `Contains placeholder text: "${placeholder}"`,
196
+ expected: 'Unique, compelling description that summarizes page content',
197
+ impact: 'Placeholder text looks unprofessional and will hurt click-through rates in search results',
198
+ example: 'Instead of "Description here", use "Shop premium running shoes with free shipping on orders over $50. 30-day returns."'
199
+ }
200
+ });
140
201
  }
141
202
  }
142
- return null;
203
+ return createResult({ id: 'meta-description-unique', name: 'Description Quality', category: 'meta', severity: 'info' }, 'info', 'Not applicable (description has good quality)', { recommendation: 'This rule checks for placeholder patterns in meta description' });
143
204
  },
144
205
  },
145
206
  {
@@ -160,7 +221,7 @@ export const metaRules = [
160
221
  },
161
222
  });
162
223
  }
163
- return null;
224
+ return createResult({ id: 'og-title-exists', name: 'OG Title Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:title)', { recommendation: 'This rule checks for the presence of og:title meta tag' });
164
225
  },
165
226
  },
166
227
  {
@@ -170,12 +231,22 @@ export const metaRules = [
170
231
  severity: 'warning',
171
232
  description: 'og:title should be 60-70 characters (max 90)',
172
233
  check: (ctx) => {
173
- if (!ctx.ogTitle)
174
- return null;
234
+ if (!ctx.ogTitle) {
235
+ return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks og:title length when present' });
236
+ }
175
237
  const len = ctx.ogTitle.length;
176
238
  const { ideal, max } = SEO_THRESHOLDS.og.title;
177
239
  if (len > max) {
178
- return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'warn', `og:title too long (${len} chars, truncates at ~${max})`, { value: len, recommendation: `Shorten to ${ideal.max} characters` });
240
+ return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'warn', `og:title too long (${len} chars, truncates at ~${max})`, {
241
+ value: len,
242
+ recommendation: `Shorten to ${ideal.max} characters`,
243
+ evidence: {
244
+ found: `${len} characters`,
245
+ expected: `${ideal.min}-${ideal.max} characters (max ${max})`,
246
+ impact: 'Long titles get truncated on Facebook, LinkedIn, and other platforms, potentially hiding key information',
247
+ example: 'Instead of a 100-char title, use "Amazing Product - 50% Off Today | YourBrand" (48 chars)'
248
+ }
249
+ });
179
250
  }
180
251
  if (len >= ideal.min && len <= ideal.max) {
181
252
  return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'pass', `og:title length ideal (${len} chars)`, { value: len });
@@ -190,13 +261,22 @@ export const metaRules = [
190
261
  severity: 'warning',
191
262
  description: 'og:title should not contain emojis (some networks remove them)',
192
263
  check: (ctx) => {
193
- if (!ctx.ogTitle)
194
- return null;
264
+ if (!ctx.ogTitle) {
265
+ return createResult({ id: 'og-title-no-emoji', name: 'OG Title No Emoji', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks for emojis in og:title when present' });
266
+ }
195
267
  const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u;
196
268
  if (emojiRegex.test(ctx.ogTitle)) {
197
- return createResult({ id: 'og-title-no-emoji', name: 'OG Title Emoji', category: 'og', severity: 'warning' }, 'warn', 'og:title contains emojis (some networks remove them)', { recommendation: 'Remove emojis from og:title for consistent display' });
269
+ return createResult({ id: 'og-title-no-emoji', name: 'OG Title Emoji', category: 'og', severity: 'warning' }, 'warn', 'og:title contains emojis (some networks remove them)', {
270
+ recommendation: 'Remove emojis from og:title for consistent display',
271
+ evidence: {
272
+ found: `Title contains emojis: "${ctx.ogTitle}"`,
273
+ expected: 'Text without emoji characters',
274
+ impact: 'Facebook, LinkedIn, and some other platforms strip emojis from titles, causing inconsistent display',
275
+ example: 'Instead of "🚀 Amazing Product", use "Amazing Product - Fast Delivery"'
276
+ }
277
+ });
198
278
  }
199
- return null;
279
+ return createResult({ id: 'og-title-no-emoji', name: 'OG Title No Emoji', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title has no emojis)', { recommendation: 'This rule checks for emoji characters in og:title' });
200
280
  },
201
281
  },
202
282
  {
@@ -217,7 +297,7 @@ export const metaRules = [
217
297
  },
218
298
  });
219
299
  }
220
- return null;
300
+ return createResult({ id: 'og-description-exists', name: 'OG Description Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:description)', { recommendation: 'This rule checks for the presence of og:description meta tag' });
221
301
  },
222
302
  },
223
303
  {
@@ -227,12 +307,22 @@ export const metaRules = [
227
307
  severity: 'warning',
228
308
  description: 'og:description should be 110-155 characters (max 200)',
229
309
  check: (ctx) => {
230
- if (!ctx.ogDescription)
231
- return null;
310
+ if (!ctx.ogDescription) {
311
+ return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:description)', { recommendation: 'This rule checks og:description length when present' });
312
+ }
232
313
  const len = ctx.ogDescription.length;
233
314
  const { ideal, max } = SEO_THRESHOLDS.og.description;
234
315
  if (len > max) {
235
- return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'warn', `og:description too long (${len} chars, truncates at ~${max})`, { value: len, recommendation: `Shorten to ${ideal.max} characters` });
316
+ return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'warn', `og:description too long (${len} chars, truncates at ~${max})`, {
317
+ value: len,
318
+ recommendation: `Shorten to ${ideal.max} characters`,
319
+ evidence: {
320
+ found: `${len} characters`,
321
+ expected: `${ideal.min}-${ideal.max} characters (max ${max})`,
322
+ impact: 'Descriptions longer than 200 chars get truncated on social platforms, hiding important information',
323
+ example: 'Keep description concise: "Shop premium running shoes. Free shipping over $50. 30-day returns." (85 chars)'
324
+ }
325
+ });
236
326
  }
237
327
  if (len >= ideal.min && len <= ideal.max) {
238
328
  return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'pass', `og:description length ideal (${len} chars)`, { value: len });
@@ -259,7 +349,7 @@ export const metaRules = [
259
349
  },
260
350
  });
261
351
  }
262
- return null;
352
+ return createResult({ id: 'og-image-exists', name: 'OG Image Exists', category: 'og', severity: 'error' }, 'info', 'Not applicable (page has og:image)', { recommendation: 'This rule checks for the presence of og:image meta tag' });
263
353
  },
264
354
  },
265
355
  {
@@ -269,10 +359,20 @@ export const metaRules = [
269
359
  severity: 'error',
270
360
  description: 'og:image URL must use HTTPS',
271
361
  check: (ctx) => {
272
- if (!ctx.ogImage)
273
- return null;
362
+ if (!ctx.ogImage) {
363
+ return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL protocol when present' });
364
+ }
274
365
  if (ctx.ogImage.startsWith('http://')) {
275
- return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'fail', 'og:image uses HTTP instead of HTTPS', { value: ctx.ogImage, recommendation: 'Always use HTTPS for og:image URLs' });
366
+ return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'fail', 'og:image uses HTTP instead of HTTPS', {
367
+ value: ctx.ogImage,
368
+ recommendation: 'Always use HTTPS for og:image URLs',
369
+ evidence: {
370
+ found: `HTTP URL: ${ctx.ogImage}`,
371
+ expected: 'HTTPS URL (https://...)',
372
+ impact: 'Facebook and other platforms may block HTTP images due to mixed content security policies',
373
+ example: `Change http://yoursite.com/image.jpg to https://yoursite.com/image.jpg`
374
+ }
375
+ });
276
376
  }
277
377
  return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'pass', 'og:image uses HTTPS');
278
378
  },
@@ -285,7 +385,15 @@ export const metaRules = [
285
385
  description: 'og:url should be defined (canonical URL for sharing)',
286
386
  check: (ctx) => {
287
387
  if (!ctx.ogUrl) {
288
- return createResult({ id: 'og-url-exists', name: 'OG URL', category: 'og', severity: 'warning' }, 'warn', 'Missing og:url', { recommendation: 'Add og:url with the canonical URL of the page' });
388
+ return createResult({ id: 'og-url-exists', name: 'OG URL', category: 'og', severity: 'warning' }, 'warn', 'Missing og:url', {
389
+ recommendation: 'Add og:url with the canonical URL of the page',
390
+ evidence: {
391
+ found: 'No og:url meta tag',
392
+ expected: '<meta property="og:url" content="https://yoursite.com/page">',
393
+ impact: 'Without og:url, social platforms may use incorrect URLs, causing share count fragmentation',
394
+ example: '<meta property="og:url" content="https://yoursite.com/products/shoes">'
395
+ }
396
+ });
289
397
  }
290
398
  return createResult({ id: 'og-url-exists', name: 'OG URL', category: 'og', severity: 'warning' }, 'pass', 'og:url is defined', { value: ctx.ogUrl });
291
399
  },
@@ -298,7 +406,15 @@ export const metaRules = [
298
406
  description: 'og:type should be defined (website, article, etc.)',
299
407
  check: (ctx) => {
300
408
  if (!ctx.ogType) {
301
- return createResult({ id: 'og-type-exists', name: 'OG Type', category: 'og', severity: 'warning' }, 'warn', 'Missing og:type', { recommendation: 'Add og:type (website, article, product, etc.)' });
409
+ return createResult({ id: 'og-type-exists', name: 'OG Type', category: 'og', severity: 'warning' }, 'warn', 'Missing og:type', {
410
+ recommendation: 'Add og:type (website, article, product, etc.)',
411
+ evidence: {
412
+ found: 'No og:type meta tag',
413
+ expected: '<meta property="og:type" content="website"> or "article"',
414
+ impact: 'Social platforms use og:type to determine how to display the content (website vs article vs product)',
415
+ example: '<meta property="og:type" content="article"> for blog posts\n<meta property="og:type" content="website"> for general pages'
416
+ }
417
+ });
302
418
  }
303
419
  return createResult({ id: 'og-type-exists', name: 'OG Type', category: 'og', severity: 'warning' }, 'pass', `og:type is defined (${ctx.ogType})`);
304
420
  },
@@ -310,13 +426,23 @@ export const metaRules = [
310
426
  severity: 'warning',
311
427
  description: 'og:image URL should be under 2000 characters',
312
428
  check: (ctx) => {
313
- if (!ctx.ogImage)
314
- return null;
429
+ if (!ctx.ogImage) {
430
+ return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL length when present' });
431
+ }
315
432
  const maxLen = SEO_THRESHOLDS.og.meta.maxUrlLength;
316
433
  if (ctx.ogImage.length > maxLen) {
317
- return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'warn', `og:image URL too long (${ctx.ogImage.length} chars, max: ${maxLen})`, { value: ctx.ogImage.length, recommendation: 'Shorten the image URL path' });
434
+ return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'warn', `og:image URL too long (${ctx.ogImage.length} chars, max: ${maxLen})`, {
435
+ value: ctx.ogImage.length,
436
+ recommendation: 'Shorten the image URL path',
437
+ evidence: {
438
+ found: `${ctx.ogImage.length} characters`,
439
+ expected: `Less than ${maxLen} characters`,
440
+ impact: 'Very long URLs may be truncated or rejected by some social platforms',
441
+ example: 'Use shorter paths: "/og-images/product-123.jpg" instead of "/assets/generated/optimized/social-media/open-graph/product-images/..."'
442
+ }
443
+ });
318
444
  }
319
- return null;
445
+ return createResult({ id: 'og-image-url-length', name: 'OG Image URL Length', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL length is acceptable)', { recommendation: 'This rule checks if og:image URL exceeds length limits' });
320
446
  },
321
447
  },
322
448
  {
@@ -326,15 +452,24 @@ export const metaRules = [
326
452
  severity: 'warning',
327
453
  description: 'og:image URL should not have expiring tokens or excessive query params',
328
454
  check: (ctx) => {
329
- if (!ctx.ogImage)
330
- return null;
455
+ if (!ctx.ogImage) {
456
+ return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL for expiring tokens when present' });
457
+ }
331
458
  try {
332
459
  const url = new URL(ctx.ogImage);
333
460
  const params = url.searchParams;
334
461
  const expiringParams = ['expires', 'exp', 'token', 'sig', 'signature', 'auth'];
335
462
  const hasExpiring = expiringParams.some((p) => params.has(p));
336
463
  if (hasExpiring) {
337
- return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'warn', 'og:image URL may have expiring tokens (Meta caches images)', { recommendation: 'Use permanent URLs without expiration tokens for og:image' });
464
+ return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'warn', 'og:image URL may have expiring tokens (Meta caches images)', {
465
+ recommendation: 'Use permanent URLs without expiration tokens for og:image',
466
+ evidence: {
467
+ found: `URL contains expiring parameters: ${ctx.ogImage}`,
468
+ expected: 'Permanent URL without time-based tokens',
469
+ impact: 'Meta caches images for weeks. Expiring URLs will break after cache, showing broken images on shares',
470
+ example: 'Instead of "...?token=abc&expires=123", use a permanent path like "/og-images/product.jpg"'
471
+ }
472
+ });
338
473
  }
339
474
  if (Array.from(params.keys()).length > 5) {
340
475
  return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'info', 'og:image URL has many query parameters', { recommendation: 'Simplify og:image URL for better caching' });
@@ -342,7 +477,7 @@ export const metaRules = [
342
477
  }
343
478
  catch {
344
479
  }
345
- return null;
480
+ return createResult({ id: 'og-image-url-quality', name: 'OG Image URL Quality', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL has good quality)', { recommendation: 'This rule checks for expiring tokens and excessive query parameters' });
346
481
  },
347
482
  },
348
483
  {
@@ -352,15 +487,16 @@ export const metaRules = [
352
487
  severity: 'info',
353
488
  description: 'og:description should not have excessive emojis',
354
489
  check: (ctx) => {
355
- if (!ctx.ogDescription)
356
- return null;
490
+ if (!ctx.ogDescription) {
491
+ return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', 'Not applicable (no og:description)', { recommendation: 'This rule checks emoji usage in og:description when present' });
492
+ }
357
493
  const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]/gu;
358
494
  const emojis = ctx.ogDescription.match(emojiRegex) || [];
359
495
  const maxEmojis = SEO_THRESHOLDS.og.meta.maxDescriptionEmojis;
360
496
  if (emojis.length > maxEmojis) {
361
497
  return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', `og:description has ${emojis.length} emojis (recommended max: ${maxEmojis})`, { value: emojis.length, recommendation: 'Reduce emojis in og:description for better compatibility' });
362
498
  }
363
- return null;
499
+ return createResult({ id: 'og-description-emojis', name: 'OG Description Emojis', category: 'og', severity: 'info' }, 'info', 'Not applicable (og:description has acceptable emoji count)', { recommendation: 'This rule checks for excessive emoji usage in og:description' });
364
500
  },
365
501
  },
366
502
  {
@@ -370,18 +506,29 @@ export const metaRules = [
370
506
  severity: 'warning',
371
507
  description: 'og:title should not be mostly uppercase (Meta may flag as low quality)',
372
508
  check: (ctx) => {
373
- if (!ctx.ogTitle)
374
- return null;
509
+ if (!ctx.ogTitle) {
510
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:title)', { recommendation: 'This rule checks capitalization in og:title when present' });
511
+ }
375
512
  const letters = ctx.ogTitle.replace(/[^a-zA-Z]/g, '');
376
- if (letters.length < 5)
377
- return null;
513
+ if (letters.length < 5) {
514
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title has too few letters to evaluate)', { recommendation: 'This rule checks for excessive uppercase in og:title' });
515
+ }
378
516
  const uppercase = letters.replace(/[^A-Z]/g, '').length;
379
517
  const percentage = Math.round((uppercase / letters.length) * 100);
380
518
  const maxCaps = SEO_THRESHOLDS.og.meta.maxCapsPercentage;
381
519
  if (percentage > maxCaps) {
382
- return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'warn', `og:title has ${percentage}% uppercase (Meta may flag as low quality)`, { value: percentage, recommendation: 'Use normal capitalization in og:title' });
520
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'warn', `og:title has ${percentage}% uppercase (Meta may flag as low quality)`, {
521
+ value: percentage,
522
+ recommendation: 'Use normal capitalization in og:title',
523
+ evidence: {
524
+ found: `${percentage}% uppercase letters in "${ctx.ogTitle}"`,
525
+ expected: `Less than ${maxCaps}% uppercase`,
526
+ impact: 'Meta may flag posts with excessive capitals as spam or low quality, reducing reach',
527
+ example: 'Instead of "BUY NOW - HUGE SALE", use "Buy Now - Huge Sale on All Items"'
528
+ }
529
+ });
383
530
  }
384
- return null;
531
+ return createResult({ id: 'og-title-caps', name: 'OG Title Caps', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:title uses proper capitalization)', { recommendation: 'This rule checks for excessive uppercase in og:title' });
385
532
  },
386
533
  },
387
534
  {
@@ -402,7 +549,15 @@ export const metaRules = [
402
549
  .filter(([, value]) => !value)
403
550
  .map(([key]) => key);
404
551
  if (missing.length > 0) {
405
- return createResult({ id: 'og-meta-complete', name: 'Meta Complete', category: 'og', severity: 'warning' }, 'warn', `Missing required Meta tags: ${missing.join(', ')}`, { recommendation: 'Meta (Facebook/Instagram) requires all 5 OG tags for proper previews' });
552
+ return createResult({ id: 'og-meta-complete', name: 'Meta Complete', category: 'og', severity: 'warning' }, 'warn', `Missing required Meta tags: ${missing.join(', ')}`, {
553
+ recommendation: 'Meta (Facebook/Instagram) requires all 5 OG tags for proper previews',
554
+ evidence: {
555
+ found: `Missing: ${missing.join(', ')}`,
556
+ expected: 'All 5 tags: og:title, og:description, og:image, og:url, og:type',
557
+ impact: 'Incomplete Open Graph data leads to poor or broken previews on Facebook, Instagram, and LinkedIn',
558
+ example: 'Add all required tags:\n<meta property="og:title" content="...">\n<meta property="og:description" content="...">\n<meta property="og:image" content="...">\n<meta property="og:url" content="...">\n<meta property="og:type" content="website">'
559
+ }
560
+ });
406
561
  }
407
562
  return createResult({ id: 'og-meta-complete', name: 'Meta Complete', category: 'og', severity: 'warning' }, 'pass', 'All required Meta OG tags present');
408
563
  },
@@ -417,7 +572,7 @@ export const metaRules = [
417
572
  if (ctx.ogTitle && !ctx.title) {
418
573
  return createResult({ id: 'og-fallback-meta-title', name: 'Fallback Meta Title', category: 'og', severity: 'info' }, 'info', 'No <title> tag found (og:title exists)', { recommendation: 'Add <title> tag as fallback for universal compatibility' });
419
574
  }
420
- return null;
575
+ return createResult({ id: 'og-fallback-meta-title', name: 'Fallback Meta Title', category: 'og', severity: 'info' }, 'info', 'Not applicable (page has title tag or no og:title)', { recommendation: 'This rule checks for title tag when og:title exists' });
421
576
  },
422
577
  },
423
578
  {
@@ -427,19 +582,28 @@ export const metaRules = [
427
582
  severity: 'warning',
428
583
  description: 'og:image should not have redirect chains (Meta blocks >2 redirects)',
429
584
  check: (ctx) => {
430
- if (!ctx.ogImage)
431
- return null;
585
+ if (!ctx.ogImage) {
586
+ return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL for redirect patterns when present' });
587
+ }
432
588
  try {
433
589
  const url = new URL(ctx.ogImage);
434
590
  const redirectPatterns = ['redirect', 'proxy', 'forward', 'goto', 'redir', 'bounce'];
435
591
  const hasRedirectPattern = redirectPatterns.some((p) => url.pathname.toLowerCase().includes(p) || url.hostname.toLowerCase().includes(p));
436
592
  if (hasRedirectPattern) {
437
- return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'warn', 'og:image URL may contain redirects (Meta blocks >2 redirect chains)', { recommendation: 'Use direct, permanent image URLs for og:image' });
593
+ return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'warn', 'og:image URL may contain redirects (Meta blocks >2 redirect chains)', {
594
+ recommendation: 'Use direct, permanent image URLs for og:image',
595
+ evidence: {
596
+ found: `URL contains redirect pattern: ${ctx.ogImage}`,
597
+ expected: 'Direct URL to the image file',
598
+ impact: 'Facebook blocks images with more than 2 redirects, causing broken previews',
599
+ example: 'Instead of "/redirect?url=image.jpg", use direct URL: "/images/og-image.jpg"'
600
+ }
601
+ });
438
602
  }
439
603
  }
440
604
  catch {
441
605
  }
442
- return null;
606
+ return createResult({ id: 'og-image-redirects', name: 'OG Image Redirects', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL has no redirect patterns)', { recommendation: 'This rule checks for redirect patterns in og:image URL' });
443
607
  },
444
608
  },
445
609
  {
@@ -449,22 +613,39 @@ export const metaRules = [
449
613
  severity: 'warning',
450
614
  description: 'og:image must be publicly accessible (no auth, no private URLs)',
451
615
  check: (ctx) => {
452
- if (!ctx.ogImage)
453
- return null;
616
+ if (!ctx.ogImage) {
617
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'info', 'Not applicable (no og:image)', { recommendation: 'This rule checks og:image URL accessibility when present' });
618
+ }
454
619
  try {
455
620
  const url = new URL(ctx.ogImage);
456
621
  if (url.username || url.password) {
457
- return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'fail', 'og:image URL contains credentials (will fail on social platforms)', { recommendation: 'Use publicly accessible URLs without authentication' });
622
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'fail', 'og:image URL contains credentials (will fail on social platforms)', {
623
+ recommendation: 'Use publicly accessible URLs without authentication',
624
+ evidence: {
625
+ found: 'URL contains username/password credentials',
626
+ expected: 'Public URL without authentication',
627
+ impact: 'Social platforms cannot fetch images with HTTP authentication, resulting in broken previews',
628
+ example: 'Instead of "https://user:pass@site.com/image.jpg", host image publicly at "https://cdn.site.com/og-image.jpg"'
629
+ }
630
+ });
458
631
  }
459
632
  const hostname = url.hostname.toLowerCase();
460
633
  const privatePatterns = ['localhost', '127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.'];
461
634
  if (privatePatterns.some((p) => hostname.startsWith(p) || hostname === p.slice(0, -1))) {
462
- return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'fail', 'og:image URL points to localhost/private IP (not accessible)', { recommendation: 'Use publicly accessible URLs for og:image' });
635
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'fail', 'og:image URL points to localhost/private IP (not accessible)', {
636
+ recommendation: 'Use publicly accessible URLs for og:image',
637
+ evidence: {
638
+ found: `Private/local URL: ${ctx.ogImage}`,
639
+ expected: 'Public URL accessible from the internet',
640
+ impact: 'Social platforms cannot access localhost or private IPs, resulting in broken image previews',
641
+ example: 'Instead of "http://localhost:3000/image.jpg", deploy to "https://yoursite.com/og-image.jpg"'
642
+ }
643
+ });
463
644
  }
464
645
  }
465
646
  catch {
466
647
  }
467
- return null;
648
+ return createResult({ id: 'og-image-public', name: 'OG Image Public', category: 'og', severity: 'warning' }, 'info', 'Not applicable (og:image URL is publicly accessible)', { recommendation: 'This rule checks for authentication or private IPs in og:image URL' });
468
649
  },
469
650
  },
470
651
  {
@@ -475,11 +656,27 @@ export const metaRules = [
475
656
  description: 'twitter:card should be defined (summary or summary_large_image)',
476
657
  check: (ctx) => {
477
658
  if (!ctx.twitterCard) {
478
- return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'warn', 'Missing twitter:card', { recommendation: 'Add twitter:card (summary or summary_large_image)' });
659
+ return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'warn', 'Missing twitter:card', {
660
+ recommendation: 'Add twitter:card (summary or summary_large_image)',
661
+ evidence: {
662
+ found: 'No twitter:card meta tag',
663
+ expected: '<meta name="twitter:card" content="summary_large_image">',
664
+ impact: 'Twitter will use a default card type, which may not display optimally',
665
+ example: '<meta name="twitter:card" content="summary_large_image">\n<meta name="twitter:card" content="summary"> for smaller images'
666
+ }
667
+ });
479
668
  }
480
669
  const validCards = ['summary', 'summary_large_image', 'player', 'app'];
481
670
  if (!validCards.includes(ctx.twitterCard)) {
482
- return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'warn', `Invalid twitter:card value: ${ctx.twitterCard}`, { recommendation: 'Use summary or summary_large_image' });
671
+ return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'warn', `Invalid twitter:card value: ${ctx.twitterCard}`, {
672
+ recommendation: 'Use summary or summary_large_image',
673
+ evidence: {
674
+ found: `Invalid value: "${ctx.twitterCard}"`,
675
+ expected: 'One of: summary, summary_large_image, player, app',
676
+ impact: 'Invalid card types are ignored by Twitter, falling back to default display',
677
+ example: '<meta name="twitter:card" content="summary_large_image">'
678
+ }
679
+ });
483
680
  }
484
681
  return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'pass', `twitter:card is defined (${ctx.twitterCard})`);
485
682
  },
@@ -492,14 +689,24 @@ export const metaRules = [
492
689
  description: 'twitter:title should be 55-70 characters',
493
690
  check: (ctx) => {
494
691
  const title = ctx.twitterTitle || ctx.ogTitle;
495
- if (!title)
496
- return null;
692
+ if (!title) {
693
+ return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (no twitter:title or og:title)', { recommendation: 'This rule checks Twitter title length when present' });
694
+ }
497
695
  const len = title.length;
498
696
  const { ideal, max } = SEO_THRESHOLDS.twitter.title;
499
697
  if (len > max) {
500
- return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:title too long (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to ${ideal.max} characters` });
698
+ return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:title too long (${len} chars, max: ${max})`, {
699
+ value: len,
700
+ recommendation: `Shorten to ${ideal.max} characters`,
701
+ evidence: {
702
+ found: `${len} characters`,
703
+ expected: `${ideal.min}-${ideal.max} characters (max ${max})`,
704
+ impact: 'Twitter truncates long titles, potentially hiding key information in card previews',
705
+ example: 'Keep it concise: "New Product Launch - 50% Off" (34 chars)'
706
+ }
707
+ });
501
708
  }
502
- return null;
709
+ return createResult({ id: 'twitter-title-length', name: 'Twitter Title Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (Twitter title length is acceptable)', { recommendation: 'This rule checks if Twitter title exceeds length limits' });
503
710
  },
504
711
  },
505
712
  {
@@ -510,14 +717,24 @@ export const metaRules = [
510
717
  description: 'twitter:description should be 125-200 characters',
511
718
  check: (ctx) => {
512
719
  const description = ctx.twitterDescription || ctx.ogDescription;
513
- if (!description)
514
- return null;
720
+ if (!description) {
721
+ return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (no twitter:description or og:description)', { recommendation: 'This rule checks Twitter description length when present' });
722
+ }
515
723
  const len = description.length;
516
724
  const { max } = SEO_THRESHOLDS.twitter.description;
517
725
  if (len > max) {
518
- return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:description too long (${len} chars, max: ${max})`, { value: len, recommendation: `Shorten to ${max} characters` });
726
+ return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'warn', `twitter:description too long (${len} chars, max: ${max})`, {
727
+ value: len,
728
+ recommendation: `Shorten to ${max} characters`,
729
+ evidence: {
730
+ found: `${len} characters`,
731
+ expected: `125-${max} characters`,
732
+ impact: 'Twitter truncates descriptions over 200 characters, potentially hiding your call-to-action',
733
+ example: 'Concise description: "Discover the best deals on electronics. Free shipping & 30-day returns." (89 chars)'
734
+ }
735
+ });
519
736
  }
520
- return null;
737
+ return createResult({ id: 'twitter-description-length', name: 'Twitter Description Length', category: 'twitter', severity: 'warning' }, 'info', 'Not applicable (Twitter description length is acceptable)', { recommendation: 'This rule checks if Twitter description exceeds length limits' });
521
738
  },
522
739
  },
523
740
  {
@@ -527,8 +744,9 @@ export const metaRules = [
527
744
  severity: 'warning',
528
745
  description: 'Title should have at least 10 characters for SEO value',
529
746
  check: (ctx) => {
530
- if (!ctx.title)
531
- return null;
747
+ if (!ctx.title) {
748
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'info', 'Not applicable (no title tag)', { recommendation: 'This rule checks if title is too short when present' });
749
+ }
532
750
  const len = ctx.titleLength ?? ctx.title.length;
533
751
  if (len <= 10) {
534
752
  return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'warn', `Title is very short (${len} chars)`, {
@@ -541,7 +759,47 @@ export const metaRules = [
541
759
  }
542
760
  });
543
761
  }
544
- return null;
762
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title has sufficient length)', { recommendation: 'This rule checks if title is shorter than 10 characters' });
763
+ },
764
+ },
765
+ {
766
+ id: 'keywords-in-title',
767
+ name: 'Keywords in Title',
768
+ category: 'title',
769
+ severity: 'warning',
770
+ description: 'Title should contain main keywords found in content',
771
+ check: (ctx) => {
772
+ if (ctx.keywordsInTitle === false && ctx.topKeywords && ctx.topKeywords.length > 0) {
773
+ return createResult({ id: 'keywords-in-title', name: 'Keywords in Title', category: 'title', severity: 'warning' }, 'warn', 'Title does not appear to contain top keywords', {
774
+ recommendation: 'Include your main target keywords in the page title.',
775
+ evidence: {
776
+ found: `Title: "${ctx.title}"`,
777
+ expected: `Should contain one of: ${ctx.topKeywords.join(', ')}`,
778
+ impact: 'Keywords in title are a strong ranking signal.'
779
+ }
780
+ });
781
+ }
782
+ return createResult({ id: 'keywords-in-title', name: 'Keywords in Title', category: 'title', severity: 'warning' }, 'info', 'Not applicable (title contains keywords or no keyword data)', { recommendation: 'This rule checks if title contains main keywords from content' });
783
+ },
784
+ },
785
+ {
786
+ id: 'keywords-in-description',
787
+ name: 'Keywords in Description',
788
+ category: 'meta',
789
+ severity: 'info',
790
+ description: 'Meta description should contain main keywords',
791
+ check: (ctx) => {
792
+ if (ctx.keywordsInDescription === false && ctx.topKeywords && ctx.topKeywords.length > 0) {
793
+ return createResult({ id: 'keywords-in-description', name: 'Keywords in Description', category: 'meta', severity: 'info' }, 'info', 'Meta description does not appear to contain top keywords', {
794
+ recommendation: 'Include main keywords in the description to embolden them in search results.',
795
+ evidence: {
796
+ found: 'Description does not match top keywords',
797
+ expected: `Should contain one of: ${ctx.topKeywords.join(', ')}`,
798
+ impact: 'Keywords in description are bolded in SERPs, improving CTR.'
799
+ }
800
+ });
801
+ }
802
+ return createResult({ id: 'keywords-in-description', name: 'Keywords in Description', category: 'meta', severity: 'info' }, 'info', 'Not applicable (description contains keywords or no keyword data)', { recommendation: 'This rule checks if meta description contains main keywords' });
545
803
  },
546
804
  },
547
805
  ];