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.
- package/README.md +47 -0
- package/dist/bin/recker-linux-x64 +0 -0
- package/dist/bin/recker-macos-x64 +0 -0
- package/dist/bin/recker-win-x64.exe +0 -0
- package/dist/bin/rek.cjs +85152 -100207
- package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
- package/dist/browser/ai/adaptive-timeout.js +208 -0
- package/dist/browser/ai/client.d.ts +22 -0
- package/dist/browser/ai/client.js +294 -0
- package/dist/browser/ai/index.d.ts +14 -0
- package/dist/browser/ai/index.js +11 -0
- package/dist/browser/ai/providers/anthropic.d.ts +63 -0
- package/dist/browser/ai/providers/anthropic.js +370 -0
- package/dist/browser/ai/providers/base.d.ts +48 -0
- package/dist/browser/ai/providers/base.js +150 -0
- package/dist/browser/ai/providers/google.d.ts +59 -0
- package/dist/browser/ai/providers/google.js +305 -0
- package/dist/browser/ai/providers/ollama.d.ts +44 -0
- package/dist/browser/ai/providers/ollama.js +240 -0
- package/dist/browser/ai/providers/openai.d.ts +64 -0
- package/dist/browser/ai/providers/openai.js +298 -0
- package/dist/browser/ai/rate-limiter.d.ts +43 -0
- package/dist/browser/ai/rate-limiter.js +215 -0
- package/dist/browser/ai/vector/index.d.ts +2 -0
- package/dist/browser/ai/vector/index.js +2 -0
- package/dist/browser/ai/vector/similarity.d.ts +2 -0
- package/dist/browser/ai/vector/similarity.js +27 -0
- package/dist/browser/ai/vector/store.d.ts +27 -0
- package/dist/browser/ai/vector/store.js +82 -0
- package/dist/browser/browser/cache.d.ts +2 -40
- package/dist/browser/browser/cache.js +2 -199
- package/dist/browser/browser/index.d.ts +8 -0
- package/dist/browser/browser/index.js +8 -0
- package/dist/browser/browser/recker.d.ts +8 -1
- package/dist/browser/browser/recker.js +8 -2
- package/dist/browser/cache/indexed-db.d.ts +10 -0
- package/dist/browser/cache/indexed-db.js +88 -0
- package/dist/browser/cache/service-worker-cache.d.ts +18 -0
- package/dist/browser/cache/service-worker-cache.js +103 -0
- package/dist/browser/cache.d.ts +2 -40
- package/dist/browser/cache.js +2 -199
- package/dist/browser/constants/user-agents.d.ts +7 -0
- package/dist/browser/constants/user-agents.js +7 -0
- package/dist/browser/core/client.d.ts +2 -0
- package/dist/browser/core/client.js +19 -1
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.js +8 -0
- package/dist/browser/plugins/har-recorder.d.ts +40 -0
- package/dist/browser/plugins/har-recorder.js +120 -0
- package/dist/browser/plugins/network-simulation.d.ts +7 -0
- package/dist/browser/plugins/network-simulation.js +13 -0
- package/dist/browser/presets/android.d.ts +2 -0
- package/dist/browser/presets/android.js +16 -0
- package/dist/browser/presets/anthropic.d.ts +8 -0
- package/dist/browser/presets/anthropic.js +27 -0
- package/dist/browser/presets/aws.d.ts +19 -0
- package/dist/browser/presets/aws.js +68 -0
- package/dist/browser/presets/azure-openai.d.ts +10 -0
- package/dist/browser/presets/azure-openai.js +35 -0
- package/dist/browser/presets/azure.d.ts +41 -0
- package/dist/browser/presets/azure.js +104 -0
- package/dist/browser/presets/chaturbate.d.ts +2 -0
- package/dist/browser/presets/chaturbate.js +17 -0
- package/dist/browser/presets/cloudflare.d.ts +12 -0
- package/dist/browser/presets/cloudflare.js +39 -0
- package/dist/browser/presets/cohere.d.ts +7 -0
- package/dist/browser/presets/cohere.js +22 -0
- package/dist/browser/presets/deepseek.d.ts +7 -0
- package/dist/browser/presets/deepseek.js +22 -0
- package/dist/browser/presets/digitalocean.d.ts +5 -0
- package/dist/browser/presets/digitalocean.js +16 -0
- package/dist/browser/presets/discord.d.ts +6 -0
- package/dist/browser/presets/discord.js +17 -0
- package/dist/browser/presets/elevenlabs.d.ts +6 -0
- package/dist/browser/presets/elevenlabs.js +20 -0
- package/dist/browser/presets/enhancers.d.ts +20 -0
- package/dist/browser/presets/enhancers.js +85 -0
- package/dist/browser/presets/fireworks.d.ts +7 -0
- package/dist/browser/presets/fireworks.js +22 -0
- package/dist/browser/presets/gcp.d.ts +34 -0
- package/dist/browser/presets/gcp.js +91 -0
- package/dist/browser/presets/gemini.d.ts +7 -0
- package/dist/browser/presets/gemini.js +23 -0
- package/dist/browser/presets/github.d.ts +6 -0
- package/dist/browser/presets/github.js +17 -0
- package/dist/browser/presets/gitlab.d.ts +6 -0
- package/dist/browser/presets/gitlab.js +16 -0
- package/dist/browser/presets/groq.d.ts +7 -0
- package/dist/browser/presets/groq.js +22 -0
- package/dist/browser/presets/hubspot.d.ts +9 -0
- package/dist/browser/presets/hubspot.js +28 -0
- package/dist/browser/presets/huggingface.d.ts +7 -0
- package/dist/browser/presets/huggingface.js +23 -0
- package/dist/browser/presets/index.d.ts +47 -0
- package/dist/browser/presets/index.js +47 -0
- package/dist/browser/presets/ios.d.ts +2 -0
- package/dist/browser/presets/ios.js +13 -0
- package/dist/browser/presets/linear.d.ts +5 -0
- package/dist/browser/presets/linear.js +16 -0
- package/dist/browser/presets/mailgun.d.ts +7 -0
- package/dist/browser/presets/mailgun.js +20 -0
- package/dist/browser/presets/meta.d.ts +10 -0
- package/dist/browser/presets/meta.js +33 -0
- package/dist/browser/presets/mistral.d.ts +7 -0
- package/dist/browser/presets/mistral.js +22 -0
- package/dist/browser/presets/notion.d.ts +6 -0
- package/dist/browser/presets/notion.js +17 -0
- package/dist/browser/presets/openai.d.ts +9 -0
- package/dist/browser/presets/openai.js +30 -0
- package/dist/browser/presets/oracle.d.ts +19 -0
- package/dist/browser/presets/oracle.js +117 -0
- package/dist/browser/presets/perplexity.d.ts +7 -0
- package/dist/browser/presets/perplexity.js +22 -0
- package/dist/browser/presets/pinecone.d.ts +8 -0
- package/dist/browser/presets/pinecone.js +42 -0
- package/dist/browser/presets/registry.d.ts +23 -0
- package/dist/browser/presets/registry.js +519 -0
- package/dist/browser/presets/replicate.d.ts +7 -0
- package/dist/browser/presets/replicate.js +23 -0
- package/dist/browser/presets/sendgrid.d.ts +6 -0
- package/dist/browser/presets/sendgrid.js +20 -0
- package/dist/browser/presets/sentry.d.ts +11 -0
- package/dist/browser/presets/sentry.js +48 -0
- package/dist/browser/presets/sinch.d.ts +9 -0
- package/dist/browser/presets/sinch.js +39 -0
- package/dist/browser/presets/slack.d.ts +5 -0
- package/dist/browser/presets/slack.js +16 -0
- package/dist/browser/presets/square.d.ts +10 -0
- package/dist/browser/presets/square.js +33 -0
- package/dist/browser/presets/stripe.d.ts +7 -0
- package/dist/browser/presets/stripe.js +23 -0
- package/dist/browser/presets/supabase.d.ts +6 -0
- package/dist/browser/presets/supabase.js +18 -0
- package/dist/browser/presets/tiktok.d.ts +10 -0
- package/dist/browser/presets/tiktok.js +38 -0
- package/dist/browser/presets/together.d.ts +7 -0
- package/dist/browser/presets/together.js +22 -0
- package/dist/browser/presets/twilio.d.ts +6 -0
- package/dist/browser/presets/twilio.js +17 -0
- package/dist/browser/presets/vercel.d.ts +6 -0
- package/dist/browser/presets/vercel.js +23 -0
- package/dist/browser/presets/vultr.d.ts +5 -0
- package/dist/browser/presets/vultr.js +16 -0
- package/dist/browser/presets/xai.d.ts +8 -0
- package/dist/browser/presets/xai.js +23 -0
- package/dist/browser/presets/youtube.d.ts +5 -0
- package/dist/browser/presets/youtube.js +20 -0
- package/dist/browser/recker.d.ts +8 -1
- package/dist/browser/recker.js +8 -2
- package/dist/browser/scrape/document.d.ts +5 -4
- package/dist/browser/scrape/document.js +89 -76
- package/dist/browser/scrape/element.d.ts +10 -8
- package/dist/browser/scrape/element.js +295 -81
- package/dist/browser/scrape/extractors.d.ts +11 -11
- package/dist/browser/scrape/extractors.js +145 -113
- package/dist/browser/scrape/parser/back.d.ts +1 -0
- package/dist/browser/scrape/parser/back.js +3 -0
- package/dist/browser/scrape/parser/index.d.ts +20 -0
- package/dist/browser/scrape/parser/index.js +19 -0
- package/dist/browser/scrape/parser/matcher.d.ts +30 -0
- package/dist/browser/scrape/parser/matcher.js +99 -0
- package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
- package/dist/browser/scrape/parser/nodes/comment.js +21 -0
- package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
- package/dist/browser/scrape/parser/nodes/html.js +978 -0
- package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
- package/dist/browser/scrape/parser/nodes/node.js +31 -0
- package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
- package/dist/browser/scrape/parser/nodes/text.js +30 -0
- package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
- package/dist/browser/scrape/parser/nodes/type.js +7 -0
- package/dist/browser/scrape/parser/parse.d.ts +1 -0
- package/dist/browser/scrape/parser/parse.js +1 -0
- package/dist/browser/scrape/parser/valid.d.ts +2 -0
- package/dist/browser/scrape/parser/valid.js +5 -0
- package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
- package/dist/browser/scrape/parser/void-tag.js +43 -0
- package/dist/browser/scrape/types.d.ts +7 -0
- package/dist/browser/seo/analyzer.d.ts +59 -0
- package/dist/browser/seo/analyzer.js +1399 -0
- package/dist/browser/seo/keywords.d.ts +16 -0
- package/dist/browser/seo/keywords.js +55 -0
- package/dist/browser/seo/rules/accessibility.d.ts +2 -0
- package/dist/browser/seo/rules/accessibility.js +733 -0
- package/dist/browser/seo/rules/ai-search.d.ts +2 -0
- package/dist/browser/seo/rules/ai-search.js +436 -0
- package/dist/browser/seo/rules/analytics.d.ts +2 -0
- package/dist/browser/seo/rules/analytics.js +306 -0
- package/dist/browser/seo/rules/best-practices.d.ts +2 -0
- package/dist/browser/seo/rules/best-practices.js +195 -0
- package/dist/browser/seo/rules/canonical.d.ts +12 -0
- package/dist/browser/seo/rules/canonical.js +270 -0
- package/dist/browser/seo/rules/content.d.ts +2 -0
- package/dist/browser/seo/rules/content.js +522 -0
- package/dist/browser/seo/rules/crawl.d.ts +2 -0
- package/dist/browser/seo/rules/crawl.js +435 -0
- package/dist/browser/seo/rules/cwv.d.ts +2 -0
- package/dist/browser/seo/rules/cwv.js +248 -0
- package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
- package/dist/browser/seo/rules/ecommerce.js +312 -0
- package/dist/browser/seo/rules/i18n.d.ts +2 -0
- package/dist/browser/seo/rules/i18n.js +288 -0
- package/dist/browser/seo/rules/images.d.ts +2 -0
- package/dist/browser/seo/rules/images.js +255 -0
- package/dist/browser/seo/rules/index.d.ts +52 -0
- package/dist/browser/seo/rules/index.js +159 -0
- package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
- package/dist/browser/seo/rules/internal-linking.js +394 -0
- package/dist/browser/seo/rules/links.d.ts +2 -0
- package/dist/browser/seo/rules/links.js +498 -0
- package/dist/browser/seo/rules/local.d.ts +2 -0
- package/dist/browser/seo/rules/local.js +289 -0
- package/dist/browser/seo/rules/meta.d.ts +2 -0
- package/dist/browser/seo/rules/meta.js +805 -0
- package/dist/browser/seo/rules/mobile.d.ts +2 -0
- package/dist/browser/seo/rules/mobile.js +161 -0
- package/dist/browser/seo/rules/performance.d.ts +2 -0
- package/dist/browser/seo/rules/performance.js +738 -0
- package/dist/browser/seo/rules/pwa.d.ts +2 -0
- package/dist/browser/seo/rules/pwa.js +299 -0
- package/dist/browser/seo/rules/readability.d.ts +2 -0
- package/dist/browser/seo/rules/readability.js +264 -0
- package/dist/browser/seo/rules/redirects.d.ts +16 -0
- package/dist/browser/seo/rules/redirects.js +199 -0
- package/dist/browser/seo/rules/resources.d.ts +2 -0
- package/dist/browser/seo/rules/resources.js +390 -0
- package/dist/browser/seo/rules/schema.d.ts +2 -0
- package/dist/browser/seo/rules/schema.js +379 -0
- package/dist/browser/seo/rules/security.d.ts +2 -0
- package/dist/browser/seo/rules/security.js +877 -0
- package/dist/browser/seo/rules/social.d.ts +2 -0
- package/dist/browser/seo/rules/social.js +603 -0
- package/dist/browser/seo/rules/structural.d.ts +2 -0
- package/dist/browser/seo/rules/structural.js +223 -0
- package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
- package/dist/browser/seo/rules/technical-advanced.js +289 -0
- package/dist/browser/seo/rules/technical.d.ts +2 -0
- package/dist/browser/seo/rules/technical.js +480 -0
- package/dist/browser/seo/rules/thresholds.d.ts +196 -0
- package/dist/browser/seo/rules/thresholds.js +118 -0
- package/dist/browser/seo/rules/types.d.ts +498 -0
- package/dist/browser/seo/rules/types.js +11 -0
- package/dist/browser/seo/types.d.ts +211 -0
- package/dist/browser/seo/types.js +1 -0
- package/dist/browser/transport/curl.d.ts +4 -0
- package/dist/browser/transport/curl.js +101 -0
- package/dist/browser/transport/undici.js +1 -2
- package/dist/browser/transport/worker.d.ts +18 -0
- package/dist/browser/transport/worker.js +278 -0
- package/dist/browser/types/index.d.ts +4 -1
- package/dist/browser/utils/binary-manager.d.ts +4 -0
- package/dist/browser/utils/binary-manager.js +72 -0
- package/dist/browser/utils/user-agent.js +2 -13
- package/dist/cache/indexed-db.d.ts +10 -0
- package/dist/cache/indexed-db.js +88 -0
- package/dist/cache/service-worker-cache.d.ts +18 -0
- package/dist/cache/service-worker-cache.js +103 -0
- package/dist/cli/commands/ai.d.ts +2 -0
- package/dist/cli/commands/ai.js +162 -0
- package/dist/cli/commands/bench.d.ts +2 -0
- package/dist/cli/commands/bench.js +51 -0
- package/dist/cli/commands/dns.d.ts +2 -0
- package/dist/cli/commands/dns.js +295 -0
- package/dist/cli/commands/har.d.ts +2 -0
- package/dist/cli/commands/har.js +171 -0
- package/dist/cli/commands/hls.d.ts +2 -0
- package/dist/cli/commands/hls.js +192 -0
- package/dist/cli/commands/network.d.ts +2 -0
- package/dist/cli/commands/network.js +288 -0
- package/dist/cli/commands/protocols.d.ts +2 -0
- package/dist/cli/commands/protocols.js +344 -0
- package/dist/cli/commands/scrape.d.ts +2 -0
- package/dist/cli/commands/scrape.js +176 -0
- package/dist/cli/commands/security.d.ts +2 -0
- package/dist/cli/commands/security.js +57 -0
- package/dist/cli/commands/seo.d.ts +2 -0
- package/dist/cli/commands/seo.js +125 -0
- package/dist/cli/commands/serve.d.ts +2 -0
- package/dist/cli/commands/serve.js +531 -0
- package/dist/cli/commands/spider.d.ts +3 -0
- package/dist/cli/commands/spider.js +456 -0
- package/dist/cli/commands/utils.d.ts +2 -0
- package/dist/cli/commands/utils.js +176 -0
- package/dist/cli/commands/vector.d.ts +2 -0
- package/dist/cli/commands/vector.js +158 -0
- package/dist/cli/handler.d.ts +2 -2
- package/dist/cli/handler.js +6 -6
- package/dist/cli/helpers.d.ts +7 -0
- package/dist/cli/helpers.js +128 -0
- package/dist/cli/index.js +96 -5228
- package/dist/cli/parser/help.d.ts +2 -0
- package/dist/cli/parser/help.js +52 -0
- package/dist/cli/parser/index.d.ts +3 -0
- package/dist/cli/parser/index.js +3 -0
- package/dist/cli/parser/parser.d.ts +4 -0
- package/dist/cli/parser/parser.js +146 -0
- package/dist/cli/parser/types.d.ts +41 -0
- package/dist/cli/parser/types.js +1 -0
- package/dist/cli/presets.d.ts +1 -1
- package/dist/cli/presets.js +1 -1
- package/dist/cli/router.d.ts +36 -0
- package/dist/cli/router.js +195 -0
- package/dist/cli/tui/ai-chat.js +1 -1
- package/dist/cli/tui/commands/context.d.ts +9 -0
- package/dist/cli/tui/commands/context.js +1 -0
- package/dist/cli/tui/commands/dns.d.ts +10 -0
- package/dist/cli/tui/commands/dns.js +461 -0
- package/dist/cli/tui/commands/hls.d.ts +2 -0
- package/dist/cli/tui/commands/hls.js +162 -0
- package/dist/cli/tui/commands/ip.d.ts +2 -0
- package/dist/cli/tui/commands/ip.js +45 -0
- package/dist/cli/tui/commands/network.d.ts +3 -0
- package/dist/cli/tui/commands/network.js +81 -0
- package/dist/cli/tui/commands/protocols.d.ts +6 -0
- package/dist/cli/tui/commands/protocols.js +531 -0
- package/dist/cli/tui/commands/security.d.ts +2 -0
- package/dist/cli/tui/commands/security.js +48 -0
- package/dist/cli/tui/commands/seo.d.ts +2 -0
- package/dist/cli/tui/commands/seo.js +74 -0
- package/dist/cli/tui/context.d.ts +12 -0
- package/dist/cli/tui/context.js +1 -0
- package/dist/cli/tui/shell.d.ts +11 -20
- package/dist/cli/tui/shell.js +216 -1873
- package/dist/constants/user-agents.d.ts +7 -0
- package/dist/constants/user-agents.js +7 -0
- package/dist/core/client.d.ts +2 -0
- package/dist/core/client.js +19 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli.js +2 -3
- package/dist/mcp/data/embeddings.json +1 -1
- package/dist/mcp/tools/network.js +298 -158
- package/dist/plugins/har-player.d.ts +23 -0
- package/dist/plugins/har-player.js +49 -0
- package/dist/plugins/har-recorder.d.ts +37 -3
- package/dist/plugins/har-recorder.js +116 -63
- package/dist/plugins/network-simulation.d.ts +7 -0
- package/dist/plugins/network-simulation.js +13 -0
- package/dist/presets/android.d.ts +2 -0
- package/dist/presets/android.js +16 -0
- package/dist/presets/chaturbate.d.ts +2 -0
- package/dist/presets/chaturbate.js +17 -0
- package/dist/presets/elevenlabs.d.ts +6 -0
- package/dist/presets/elevenlabs.js +20 -0
- package/dist/presets/enhancers.d.ts +20 -0
- package/dist/presets/enhancers.js +85 -0
- package/dist/presets/hubspot.d.ts +9 -0
- package/dist/presets/hubspot.js +28 -0
- package/dist/presets/index.d.ts +10 -0
- package/dist/presets/index.js +10 -0
- package/dist/presets/ios.d.ts +2 -0
- package/dist/presets/ios.js +13 -0
- package/dist/presets/pinecone.d.ts +8 -0
- package/dist/presets/pinecone.js +42 -0
- package/dist/presets/registry.js +60 -0
- package/dist/presets/sendgrid.d.ts +6 -0
- package/dist/presets/sendgrid.js +20 -0
- package/dist/presets/sentry.d.ts +11 -0
- package/dist/presets/sentry.js +48 -0
- package/dist/presets/square.d.ts +10 -0
- package/dist/presets/square.js +33 -0
- package/dist/recker.d.ts +3 -0
- package/dist/recker.js +4 -0
- package/dist/scrape/document.d.ts +5 -4
- package/dist/scrape/document.js +89 -76
- package/dist/scrape/element.d.ts +10 -8
- package/dist/scrape/element.js +295 -81
- package/dist/scrape/extractors.d.ts +11 -11
- package/dist/scrape/extractors.js +145 -113
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/parser/back.d.ts +1 -0
- package/dist/scrape/parser/back.js +3 -0
- package/dist/scrape/parser/index.d.ts +20 -0
- package/dist/scrape/parser/index.js +19 -0
- package/dist/scrape/parser/matcher.d.ts +30 -0
- package/dist/scrape/parser/matcher.js +99 -0
- package/dist/scrape/parser/nodes/comment.d.ts +12 -0
- package/dist/scrape/parser/nodes/comment.js +21 -0
- package/dist/scrape/parser/nodes/html.d.ts +110 -0
- package/dist/scrape/parser/nodes/html.js +978 -0
- package/dist/scrape/parser/nodes/node.d.ts +18 -0
- package/dist/scrape/parser/nodes/node.js +31 -0
- package/dist/scrape/parser/nodes/text.d.ts +14 -0
- package/dist/scrape/parser/nodes/text.js +30 -0
- package/dist/scrape/parser/nodes/type.d.ts +6 -0
- package/dist/scrape/parser/nodes/type.js +7 -0
- package/dist/scrape/parser/parse.d.ts +1 -0
- package/dist/scrape/parser/parse.js +1 -0
- package/dist/scrape/parser/valid.d.ts +2 -0
- package/dist/scrape/parser/valid.js +5 -0
- package/dist/scrape/parser/void-tag.d.ts +7 -0
- package/dist/scrape/parser/void-tag.js +43 -0
- package/dist/scrape/spider.d.ts +19 -0
- package/dist/scrape/spider.js +28 -3
- package/dist/scrape/types.d.ts +7 -0
- package/dist/seo/analyzer.d.ts +15 -5
- package/dist/seo/analyzer.js +636 -175
- package/dist/seo/formatter.d.ts +16 -0
- package/dist/seo/formatter.js +228 -0
- package/dist/seo/index.d.ts +2 -0
- package/dist/seo/index.js +1 -0
- package/dist/seo/keywords.d.ts +16 -0
- package/dist/seo/keywords.js +55 -0
- package/dist/seo/rules/accessibility.js +96 -57
- package/dist/seo/rules/ai-search.js +44 -31
- package/dist/seo/rules/analytics.d.ts +2 -0
- package/dist/seo/rules/analytics.js +306 -0
- package/dist/seo/rules/best-practices.js +21 -14
- package/dist/seo/rules/canonical.js +53 -32
- package/dist/seo/rules/content.js +317 -31
- package/dist/seo/rules/crawl.js +55 -40
- package/dist/seo/rules/cwv.js +21 -15
- package/dist/seo/rules/ecommerce.js +82 -22
- package/dist/seo/rules/i18n.js +75 -36
- package/dist/seo/rules/images.js +109 -30
- package/dist/seo/rules/index.js +2 -0
- package/dist/seo/rules/internal-linking.js +58 -39
- package/dist/seo/rules/links.js +79 -52
- package/dist/seo/rules/local.js +49 -25
- package/dist/seo/rules/meta.js +339 -81
- package/dist/seo/rules/mobile.js +112 -2
- package/dist/seo/rules/performance.js +434 -66
- package/dist/seo/rules/pwa.js +36 -39
- package/dist/seo/rules/readability.js +31 -22
- package/dist/seo/rules/redirects.js +21 -15
- package/dist/seo/rules/resources.js +59 -42
- package/dist/seo/rules/schema.js +333 -8
- package/dist/seo/rules/security.js +142 -80
- package/dist/seo/rules/social.js +277 -47
- package/dist/seo/rules/structural.js +87 -19
- package/dist/seo/rules/technical-advanced.js +30 -24
- package/dist/seo/rules/technical.js +243 -42
- package/dist/seo/rules/types.d.ts +53 -1
- package/dist/seo/seo-spider.d.ts +22 -0
- package/dist/seo/seo-spider.js +77 -13
- package/dist/seo/types.d.ts +8 -1
- package/dist/seo/validators/llms-txt.js +19 -0
- package/dist/seo/validators/rss.d.ts +11 -0
- package/dist/seo/validators/rss.js +93 -0
- package/dist/seo/validators/sitemap.js +36 -26
- package/dist/transport/curl.d.ts +4 -0
- package/dist/transport/curl.js +101 -0
- package/dist/transport/udp.js +0 -1
- package/dist/transport/undici.js +1 -2
- package/dist/transport/worker.d.ts +18 -0
- package/dist/transport/worker.js +278 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/utils/binary-manager.d.ts +4 -0
- package/dist/utils/binary-manager.js +72 -0
- package/dist/utils/optional-require.d.ts +7 -8
- package/dist/utils/optional-require.js +2 -21
- package/dist/utils/upload.d.ts +6 -0
- package/dist/utils/upload.js +11 -0
- package/dist/utils/user-agent.js +2 -13
- package/dist/version.js +1 -1
- package/package.json +12 -6
- package/dist/browser/utils/optional-require.d.ts +0 -19
- package/dist/browser/utils/optional-require.js +0 -105
package/dist/seo/rules/meta.js
CHANGED
|
@@ -19,7 +19,7 @@ export const metaRules = [
|
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
return
|
|
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
|
|
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})`, {
|
|
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)`, {
|
|
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
|
|
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', {
|
|
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
|
|
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
|
|
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', {
|
|
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
|
|
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
|
|
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
|
|
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})`, {
|
|
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})`, {
|
|
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
|
|
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', {
|
|
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
|
|
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
|
|
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
|
|
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})`, {
|
|
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
|
|
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)', {
|
|
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
|
|
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
|
|
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
|
|
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})`, {
|
|
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
|
|
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
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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
|
|
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})`, {
|
|
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
|
|
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
|
|
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)', {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)`, {
|
|
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
|
|
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(', ')}`, {
|
|
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
|
|
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
|
|
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)', {
|
|
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
|
|
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
|
|
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)', {
|
|
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)', {
|
|
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
|
|
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', {
|
|
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}`, {
|
|
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
|
|
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})`, {
|
|
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
|
|
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
|
|
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})`, {
|
|
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
|
|
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
|
|
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
|
|
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
|
];
|