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
@@ -0,0 +1,805 @@
1
+ import { createResult } from './types.js';
2
+ import { SEO_THRESHOLDS } from './thresholds.js';
3
+ export const metaRules = [
4
+ {
5
+ id: 'title-exists',
6
+ name: 'Title Tag Exists',
7
+ category: 'title',
8
+ severity: 'error',
9
+ description: 'Page must have a title tag',
10
+ check: (ctx) => {
11
+ if (!ctx.title) {
12
+ return createResult({ id: 'title-exists', name: 'Title Tag', category: 'title', severity: 'error' }, 'fail', 'Missing title tag', {
13
+ recommendation: 'Add a unique, descriptive title tag between 50-60 characters',
14
+ evidence: {
15
+ expected: '<title>Your Page Title - Brand Name</title>',
16
+ found: 'No <title> tag found in <head>',
17
+ impact: 'Search engines cannot display your page title in results, reducing click-through rate',
18
+ example: '<head>\n <title>Product Name - Buy Online | YourStore</title>\n</head>',
19
+ },
20
+ });
21
+ }
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
+ },
24
+ },
25
+ {
26
+ id: 'title-length',
27
+ name: 'Title Length',
28
+ category: 'title',
29
+ severity: 'warning',
30
+ description: 'Title should be between 50-60 characters',
31
+ check: (ctx) => {
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
+ }
35
+ const len = ctx.titleLength ?? ctx.title.length;
36
+ const { min, ideal, max } = SEO_THRESHOLDS.title;
37
+ if (len < min) {
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
+ });
47
+ }
48
+ if (len > max) {
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
+ });
58
+ }
59
+ if (len >= ideal.min && len <= ideal.max) {
60
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'pass', `Title length ideal (${len} chars)`, { value: len });
61
+ }
62
+ return createResult({ id: 'title-length', name: 'Title Length', category: 'title', severity: 'warning' }, 'pass', `Title length OK (${len} chars)`, { value: len });
63
+ },
64
+ },
65
+ {
66
+ id: 'title-no-caps',
67
+ name: 'Title Case',
68
+ category: 'title',
69
+ severity: 'warning',
70
+ description: 'Title should not be ALL CAPS',
71
+ check: (ctx) => {
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
+ }
75
+ const words = ctx.title.split(/\s+/).filter((w) => w.length > 3);
76
+ const allCapsWords = words.filter((w) => w === w.toUpperCase() && /[A-Z]/.test(w));
77
+ if (allCapsWords.length > words.length / 2) {
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
+ });
87
+ }
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' });
89
+ },
90
+ },
91
+ {
92
+ id: 'title-h1-different',
93
+ name: 'Title vs H1',
94
+ category: 'title',
95
+ severity: 'warning',
96
+ description: 'Title and H1 should be similar but not identical',
97
+ check: (ctx) => {
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
+ }
101
+ const titleNorm = ctx.title.toLowerCase().trim();
102
+ const h1Norm = ctx.h1Text.toLowerCase().trim();
103
+ if (titleNorm === h1Norm) {
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
+ });
113
+ }
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' });
115
+ },
116
+ },
117
+ {
118
+ id: 'meta-description-exists',
119
+ name: 'Meta Description Exists',
120
+ category: 'meta',
121
+ severity: 'error',
122
+ description: 'Page must have a meta description',
123
+ check: (ctx) => {
124
+ if (!ctx.metaDescription) {
125
+ return createResult({ id: 'meta-description-exists', name: 'Meta Description', category: 'meta', severity: 'error' }, 'fail', 'Missing meta description', {
126
+ recommendation: 'Add a compelling meta description (120-155 characters) that summarizes the page content',
127
+ evidence: {
128
+ expected: '<meta name="description" content="Your page description here...">',
129
+ found: 'No meta description tag found',
130
+ impact: 'Search engines may generate their own snippet, which may not be optimal for click-through rate',
131
+ example: '<meta name="description" content="Shop the best deals on electronics. Free shipping on orders over $50. 30-day returns.">',
132
+ },
133
+ });
134
+ }
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' });
136
+ },
137
+ },
138
+ {
139
+ id: 'meta-description-length',
140
+ name: 'Meta Description Length',
141
+ category: 'meta',
142
+ severity: 'warning',
143
+ description: 'Meta description should be 120-155 characters',
144
+ check: (ctx) => {
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
+ }
148
+ const len = ctx.metaDescriptionLength ?? ctx.metaDescription.length;
149
+ const { min, ideal, max } = SEO_THRESHOLDS.metaDescription;
150
+ if (len < min) {
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
+ });
160
+ }
161
+ if (len > max) {
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
+ });
171
+ }
172
+ if (len >= ideal.min && len <= ideal.max) {
173
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'pass', `Description length ideal (${len} chars)`, { value: len });
174
+ }
175
+ return createResult({ id: 'meta-description-length', name: 'Meta Description Length', category: 'meta', severity: 'warning' }, 'pass', `Description length OK (${len} chars)`, { value: len });
176
+ },
177
+ },
178
+ {
179
+ id: 'meta-description-unique',
180
+ name: 'Description Quality',
181
+ category: 'meta',
182
+ severity: 'info',
183
+ description: 'Meta description should be unique and compelling',
184
+ check: (ctx) => {
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
+ }
188
+ const desc = ctx.metaDescription.toLowerCase();
189
+ const placeholders = ['lorem ipsum', 'description here', 'todo', 'placeholder', 'change this'];
190
+ for (const placeholder of placeholders) {
191
+ if (desc.includes(placeholder)) {
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
+ });
201
+ }
202
+ }
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' });
204
+ },
205
+ },
206
+ {
207
+ id: 'og-title-exists',
208
+ name: 'OG Title Exists',
209
+ category: 'og',
210
+ severity: 'error',
211
+ description: 'og:title must be defined (do not rely on <title>)',
212
+ check: (ctx) => {
213
+ if (!ctx.ogTitle) {
214
+ return createResult({ id: 'og-title-exists', name: 'OG Title', category: 'og', severity: 'error' }, 'fail', 'Missing og:title', {
215
+ recommendation: 'Add og:title meta tag for better social sharing on Facebook, LinkedIn, etc.',
216
+ evidence: {
217
+ expected: '<meta property="og:title" content="Your Page Title">',
218
+ found: 'No og:title meta tag found',
219
+ impact: 'Social platforms may use <title> or auto-generate a title, which may not be optimal',
220
+ example: '<meta property="og:title" content="Amazing Product - 50% Off Today Only!">',
221
+ },
222
+ });
223
+ }
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' });
225
+ },
226
+ },
227
+ {
228
+ id: 'og-title-length',
229
+ name: 'OG Title Length',
230
+ category: 'og',
231
+ severity: 'warning',
232
+ description: 'og:title should be 60-70 characters (max 90)',
233
+ check: (ctx) => {
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
+ }
237
+ const len = ctx.ogTitle.length;
238
+ const { ideal, max } = SEO_THRESHOLDS.og.title;
239
+ if (len > max) {
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
+ });
250
+ }
251
+ if (len >= ideal.min && len <= ideal.max) {
252
+ return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'pass', `og:title length ideal (${len} chars)`, { value: len });
253
+ }
254
+ return createResult({ id: 'og-title-length', name: 'OG Title Length', category: 'og', severity: 'warning' }, 'pass', `og:title length OK (${len} chars)`, { value: len });
255
+ },
256
+ },
257
+ {
258
+ id: 'og-title-no-emoji',
259
+ name: 'OG Title No Emoji',
260
+ category: 'og',
261
+ severity: 'warning',
262
+ description: 'og:title should not contain emojis (some networks remove them)',
263
+ check: (ctx) => {
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
+ }
267
+ const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u;
268
+ if (emojiRegex.test(ctx.ogTitle)) {
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
+ });
278
+ }
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' });
280
+ },
281
+ },
282
+ {
283
+ id: 'og-description-exists',
284
+ name: 'OG Description Exists',
285
+ category: 'og',
286
+ severity: 'error',
287
+ description: 'og:description must be defined',
288
+ check: (ctx) => {
289
+ if (!ctx.ogDescription) {
290
+ return createResult({ id: 'og-description-exists', name: 'OG Description', category: 'og', severity: 'error' }, 'fail', 'Missing og:description', {
291
+ recommendation: 'Add og:description for compelling social media previews',
292
+ evidence: {
293
+ expected: '<meta property="og:description" content="Your description here...">',
294
+ found: 'No og:description meta tag found',
295
+ impact: 'Social shares may have no description or use auto-generated text',
296
+ example: '<meta property="og:description" content="Discover our latest collection. Shop now and get free shipping on orders over $50.">',
297
+ },
298
+ });
299
+ }
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' });
301
+ },
302
+ },
303
+ {
304
+ id: 'og-description-length',
305
+ name: 'OG Description Length',
306
+ category: 'og',
307
+ severity: 'warning',
308
+ description: 'og:description should be 110-155 characters (max 200)',
309
+ check: (ctx) => {
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
+ }
313
+ const len = ctx.ogDescription.length;
314
+ const { ideal, max } = SEO_THRESHOLDS.og.description;
315
+ if (len > max) {
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
+ });
326
+ }
327
+ if (len >= ideal.min && len <= ideal.max) {
328
+ return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'pass', `og:description length ideal (${len} chars)`, { value: len });
329
+ }
330
+ return createResult({ id: 'og-description-length', name: 'OG Description Length', category: 'og', severity: 'warning' }, 'pass', `og:description length OK (${len} chars)`, { value: len });
331
+ },
332
+ },
333
+ {
334
+ id: 'og-image-exists',
335
+ name: 'OG Image Exists',
336
+ category: 'og',
337
+ severity: 'error',
338
+ description: 'og:image must be defined and publicly accessible',
339
+ check: (ctx) => {
340
+ if (!ctx.ogImage) {
341
+ return createResult({ id: 'og-image-exists', name: 'OG Image', category: 'og', severity: 'error' }, 'fail', 'Missing og:image', {
342
+ recommendation: 'Add og:image with a publicly accessible image (1200×630px recommended)',
343
+ evidence: {
344
+ expected: '<meta property="og:image" content="https://yoursite.com/image.jpg">',
345
+ found: 'No og:image meta tag found',
346
+ impact: 'Social shares will have no image preview, significantly reducing engagement',
347
+ example: '<meta property="og:image" content="https://yoursite.com/og-image.jpg">\n<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="630">',
348
+ learnMore: 'https://developers.facebook.com/docs/sharing/webmasters/images/',
349
+ },
350
+ });
351
+ }
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' });
353
+ },
354
+ },
355
+ {
356
+ id: 'og-image-https',
357
+ name: 'OG Image HTTPS',
358
+ category: 'og',
359
+ severity: 'error',
360
+ description: 'og:image URL must use HTTPS',
361
+ check: (ctx) => {
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
+ }
365
+ if (ctx.ogImage.startsWith('http://')) {
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
+ });
376
+ }
377
+ return createResult({ id: 'og-image-https', name: 'OG Image HTTPS', category: 'og', severity: 'error' }, 'pass', 'og:image uses HTTPS');
378
+ },
379
+ },
380
+ {
381
+ id: 'og-url-exists',
382
+ name: 'OG URL Exists',
383
+ category: 'og',
384
+ severity: 'warning',
385
+ description: 'og:url should be defined (canonical URL for sharing)',
386
+ check: (ctx) => {
387
+ if (!ctx.ogUrl) {
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
+ });
397
+ }
398
+ return createResult({ id: 'og-url-exists', name: 'OG URL', category: 'og', severity: 'warning' }, 'pass', 'og:url is defined', { value: ctx.ogUrl });
399
+ },
400
+ },
401
+ {
402
+ id: 'og-type-exists',
403
+ name: 'OG Type Exists',
404
+ category: 'og',
405
+ severity: 'warning',
406
+ description: 'og:type should be defined (website, article, etc.)',
407
+ check: (ctx) => {
408
+ if (!ctx.ogType) {
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
+ });
418
+ }
419
+ return createResult({ id: 'og-type-exists', name: 'OG Type', category: 'og', severity: 'warning' }, 'pass', `og:type is defined (${ctx.ogType})`);
420
+ },
421
+ },
422
+ {
423
+ id: 'og-image-url-length',
424
+ name: 'OG Image URL Length',
425
+ category: 'og',
426
+ severity: 'warning',
427
+ description: 'og:image URL should be under 2000 characters',
428
+ check: (ctx) => {
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
+ }
432
+ const maxLen = SEO_THRESHOLDS.og.meta.maxUrlLength;
433
+ if (ctx.ogImage.length > maxLen) {
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
+ });
444
+ }
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' });
446
+ },
447
+ },
448
+ {
449
+ id: 'og-image-url-quality',
450
+ name: 'OG Image URL Quality',
451
+ category: 'og',
452
+ severity: 'warning',
453
+ description: 'og:image URL should not have expiring tokens or excessive query params',
454
+ check: (ctx) => {
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
+ }
458
+ try {
459
+ const url = new URL(ctx.ogImage);
460
+ const params = url.searchParams;
461
+ const expiringParams = ['expires', 'exp', 'token', 'sig', 'signature', 'auth'];
462
+ const hasExpiring = expiringParams.some((p) => params.has(p));
463
+ if (hasExpiring) {
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
+ });
473
+ }
474
+ if (Array.from(params.keys()).length > 5) {
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' });
476
+ }
477
+ }
478
+ catch {
479
+ }
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' });
481
+ },
482
+ },
483
+ {
484
+ id: 'og-description-emojis',
485
+ name: 'OG Description Emojis',
486
+ category: 'og',
487
+ severity: 'info',
488
+ description: 'og:description should not have excessive emojis',
489
+ check: (ctx) => {
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
+ }
493
+ const emojiRegex = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]/gu;
494
+ const emojis = ctx.ogDescription.match(emojiRegex) || [];
495
+ const maxEmojis = SEO_THRESHOLDS.og.meta.maxDescriptionEmojis;
496
+ if (emojis.length > maxEmojis) {
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' });
498
+ }
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' });
500
+ },
501
+ },
502
+ {
503
+ id: 'og-title-caps',
504
+ name: 'OG Title Caps',
505
+ category: 'og',
506
+ severity: 'warning',
507
+ description: 'og:title should not be mostly uppercase (Meta may flag as low quality)',
508
+ check: (ctx) => {
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
+ }
512
+ const letters = ctx.ogTitle.replace(/[^a-zA-Z]/g, '');
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
+ }
516
+ const uppercase = letters.replace(/[^A-Z]/g, '').length;
517
+ const percentage = Math.round((uppercase / letters.length) * 100);
518
+ const maxCaps = SEO_THRESHOLDS.og.meta.maxCapsPercentage;
519
+ if (percentage > maxCaps) {
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
+ });
530
+ }
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' });
532
+ },
533
+ },
534
+ {
535
+ id: 'og-meta-complete',
536
+ name: 'Meta Complete',
537
+ category: 'og',
538
+ severity: 'warning',
539
+ description: 'All required OG tags for Meta/Facebook/Instagram must be present',
540
+ check: (ctx) => {
541
+ const required = {
542
+ 'og:title': ctx.ogTitle,
543
+ 'og:description': ctx.ogDescription,
544
+ 'og:image': ctx.ogImage,
545
+ 'og:url': ctx.ogUrl,
546
+ 'og:type': ctx.ogType,
547
+ };
548
+ const missing = Object.entries(required)
549
+ .filter(([, value]) => !value)
550
+ .map(([key]) => key);
551
+ if (missing.length > 0) {
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
+ });
561
+ }
562
+ return createResult({ id: 'og-meta-complete', name: 'Meta Complete', category: 'og', severity: 'warning' }, 'pass', 'All required Meta OG tags present');
563
+ },
564
+ },
565
+ {
566
+ id: 'og-fallback-meta-title',
567
+ name: 'Fallback Meta Title',
568
+ category: 'og',
569
+ severity: 'info',
570
+ description: 'Having <meta name="title"> helps fallback on Reddit, Teams, Telegram',
571
+ check: (ctx) => {
572
+ if (ctx.ogTitle && !ctx.title) {
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' });
574
+ }
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' });
576
+ },
577
+ },
578
+ {
579
+ id: 'og-image-redirects',
580
+ name: 'OG Image Redirects',
581
+ category: 'og',
582
+ severity: 'warning',
583
+ description: 'og:image should not have redirect chains (Meta blocks >2 redirects)',
584
+ check: (ctx) => {
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
+ }
588
+ try {
589
+ const url = new URL(ctx.ogImage);
590
+ const redirectPatterns = ['redirect', 'proxy', 'forward', 'goto', 'redir', 'bounce'];
591
+ const hasRedirectPattern = redirectPatterns.some((p) => url.pathname.toLowerCase().includes(p) || url.hostname.toLowerCase().includes(p));
592
+ if (hasRedirectPattern) {
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
+ });
602
+ }
603
+ }
604
+ catch {
605
+ }
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' });
607
+ },
608
+ },
609
+ {
610
+ id: 'og-image-public',
611
+ name: 'OG Image Public',
612
+ category: 'og',
613
+ severity: 'warning',
614
+ description: 'og:image must be publicly accessible (no auth, no private URLs)',
615
+ check: (ctx) => {
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
+ }
619
+ try {
620
+ const url = new URL(ctx.ogImage);
621
+ if (url.username || url.password) {
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
+ });
631
+ }
632
+ const hostname = url.hostname.toLowerCase();
633
+ const privatePatterns = ['localhost', '127.0.0.1', '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.'];
634
+ if (privatePatterns.some((p) => hostname.startsWith(p) || hostname === p.slice(0, -1))) {
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
+ });
644
+ }
645
+ }
646
+ catch {
647
+ }
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' });
649
+ },
650
+ },
651
+ {
652
+ id: 'twitter-card-exists',
653
+ name: 'Twitter Card Exists',
654
+ category: 'twitter',
655
+ severity: 'warning',
656
+ description: 'twitter:card should be defined (summary or summary_large_image)',
657
+ check: (ctx) => {
658
+ if (!ctx.twitterCard) {
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
+ });
668
+ }
669
+ const validCards = ['summary', 'summary_large_image', 'player', 'app'];
670
+ if (!validCards.includes(ctx.twitterCard)) {
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
+ });
680
+ }
681
+ return createResult({ id: 'twitter-card-exists', name: 'Twitter Card', category: 'twitter', severity: 'warning' }, 'pass', `twitter:card is defined (${ctx.twitterCard})`);
682
+ },
683
+ },
684
+ {
685
+ id: 'twitter-title-length',
686
+ name: 'Twitter Title Length',
687
+ category: 'twitter',
688
+ severity: 'warning',
689
+ description: 'twitter:title should be 55-70 characters',
690
+ check: (ctx) => {
691
+ const title = ctx.twitterTitle || ctx.ogTitle;
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
+ }
695
+ const len = title.length;
696
+ const { ideal, max } = SEO_THRESHOLDS.twitter.title;
697
+ if (len > max) {
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
+ });
708
+ }
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' });
710
+ },
711
+ },
712
+ {
713
+ id: 'twitter-description-length',
714
+ name: 'Twitter Description Length',
715
+ category: 'twitter',
716
+ severity: 'warning',
717
+ description: 'twitter:description should be 125-200 characters',
718
+ check: (ctx) => {
719
+ const description = ctx.twitterDescription || ctx.ogDescription;
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
+ }
723
+ const len = description.length;
724
+ const { max } = SEO_THRESHOLDS.twitter.description;
725
+ if (len > max) {
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
+ });
736
+ }
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' });
738
+ },
739
+ },
740
+ {
741
+ id: 'title-too-short',
742
+ name: 'Title Too Short',
743
+ category: 'title',
744
+ severity: 'warning',
745
+ description: 'Title should have at least 10 characters for SEO value',
746
+ check: (ctx) => {
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
+ }
750
+ const len = ctx.titleLength ?? ctx.title.length;
751
+ if (len <= 10) {
752
+ return createResult({ id: 'title-too-short', name: 'Title Too Short', category: 'title', severity: 'warning' }, 'warn', `Title is very short (${len} chars)`, {
753
+ value: len,
754
+ recommendation: 'Add more descriptive text to your title (50-60 chars ideal)',
755
+ evidence: {
756
+ found: `${len} characters`,
757
+ expected: 'At least 10 characters, ideally 50-60',
758
+ impact: 'Short titles do not provide enough information about the page and limit keyword potential'
759
+ }
760
+ });
761
+ }
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' });
803
+ },
804
+ },
805
+ ];