recker 1.0.79 → 1.0.80-next.70ea84d

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 (196) hide show
  1. package/dist/browser/browser/mini.d.ts +2 -2
  2. package/dist/browser/browser/recker-mini.d.ts +8 -8
  3. package/dist/browser/browser/recker.d.ts +11 -8
  4. package/dist/browser/browser/recker.js +54 -6
  5. package/dist/browser/core/client.d.ts +15 -10
  6. package/dist/browser/core/client.js +54 -38
  7. package/dist/browser/index.iife.min.js +129 -130
  8. package/dist/browser/index.min.js +129 -130
  9. package/dist/browser/index.mini.iife.js +5697 -636
  10. package/dist/browser/index.mini.iife.min.js +47 -48
  11. package/dist/browser/index.mini.min.js +47 -48
  12. package/dist/browser/index.mini.umd.js +5697 -636
  13. package/dist/browser/index.mini.umd.min.js +47 -48
  14. package/dist/browser/index.umd.min.js +129 -130
  15. package/dist/browser/mini.d.ts +2 -2
  16. package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
  17. package/dist/browser/plugins/proxy-rotator.js +6 -28
  18. package/dist/browser/recker-mini.d.ts +8 -8
  19. package/dist/browser/recker.d.ts +11 -8
  20. package/dist/browser/recker.js +54 -6
  21. package/dist/browser/scrape/document.js +2 -2
  22. package/dist/browser/scrape/element.js +7 -1
  23. package/dist/browser/scrape/parser/nodes/html.js +1 -1
  24. package/dist/browser/scrape/spider.d.ts +52 -0
  25. package/dist/browser/scrape/spider.js +620 -38
  26. package/dist/browser/scrape/types.d.ts +2 -0
  27. package/dist/browser/search/google.d.ts +26 -1
  28. package/dist/browser/search/google.js +427 -45
  29. package/dist/browser/seo/analyzer.d.ts +1 -0
  30. package/dist/browser/seo/analyzer.js +144 -1
  31. package/dist/browser/seo/index.d.ts +1 -1
  32. package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
  33. package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
  34. package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  35. package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
  36. package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
  37. package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
  38. package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
  39. package/dist/browser/seo/keyword-campaign-shared.js +59 -0
  40. package/dist/browser/seo/keyword-campaign.d.ts +4 -107
  41. package/dist/browser/seo/keyword-campaign.js +2 -380
  42. package/dist/browser/seo/keywords.js +5 -22
  43. package/dist/browser/seo/types.d.ts +19 -0
  44. package/dist/browser/transport/curl.d.ts +5 -1
  45. package/dist/browser/transport/curl.js +207 -50
  46. package/dist/browser/transport/undici.d.ts +4 -3
  47. package/dist/browser/transport/undici.js +123 -49
  48. package/dist/browser/types/index.d.ts +9 -3
  49. package/dist/browser/utils/binary-manager.js +26 -3
  50. package/dist/browser/utils/block-detector.d.ts +8 -0
  51. package/dist/browser/utils/block-detector.js +542 -7
  52. package/dist/cli/commands/hls-runner.js +5 -4
  53. package/dist/cli/commands/live-runner.js +5 -4
  54. package/dist/cli/commands/loadtest-runner.js +3 -2
  55. package/dist/cli/commands/search.d.ts +2 -0
  56. package/dist/cli/commands/search.js +105 -0
  57. package/dist/cli/commands/seo-runner.js +9 -7
  58. package/dist/cli/commands/seo.js +140 -1
  59. package/dist/cli/commands/serve.js +75 -131
  60. package/dist/cli/commands/server-runner.js +59 -82
  61. package/dist/cli/commands/spider-runner.d.ts +37 -1
  62. package/dist/cli/commands/spider-runner.js +134 -10
  63. package/dist/cli/commands/spider.d.ts +18 -1
  64. package/dist/cli/commands/spider.js +457 -27
  65. package/dist/cli/events/handlers/cli.js +30 -1
  66. package/dist/cli/events/handlers/tui.js +26 -0
  67. package/dist/cli/events/types.d.ts +27 -0
  68. package/dist/cli/handler.d.ts +2 -12
  69. package/dist/cli/handler.js +20 -15
  70. package/dist/cli/handlers/protocols.js +39 -12
  71. package/dist/cli/handlers/search.d.ts +2 -0
  72. package/dist/cli/handlers/search.js +171 -0
  73. package/dist/cli/handlers/seo-analyze.d.ts +1 -0
  74. package/dist/cli/handlers/seo-analyze.js +666 -0
  75. package/dist/cli/handlers/seo-robots.d.ts +1 -0
  76. package/dist/cli/handlers/seo-robots.js +76 -0
  77. package/dist/cli/handlers/seo-serp.d.ts +54 -0
  78. package/dist/cli/handlers/seo-serp.js +243 -0
  79. package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
  80. package/dist/cli/handlers/seo-sitemap.js +55 -0
  81. package/dist/cli/handlers/seo-spider.d.ts +1 -0
  82. package/dist/cli/handlers/seo-spider.js +334 -0
  83. package/dist/cli/handlers/seo.d.ts +8 -4
  84. package/dist/cli/handlers/seo.js +294 -442
  85. package/dist/cli/handlers/spider.js +94 -17
  86. package/dist/cli/handlers/streaming.js +5 -1
  87. package/dist/cli/index.js +11 -2
  88. package/dist/cli/presets.d.ts +1 -1
  89. package/dist/cli/presets.js +15 -4
  90. package/dist/cli/tui/app.js +6 -1
  91. package/dist/cli/tui/components/rich-response.d.ts +72 -0
  92. package/dist/cli/tui/components/rich-response.js +117 -0
  93. package/dist/cli/tui/executor-commands/background.js +30 -24
  94. package/dist/cli/tui/executor-commands/testing.js +3 -2
  95. package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
  96. package/dist/cli/tui/hooks/useHelp.js +15 -2
  97. package/dist/cli/tui/job-manager.d.ts +4 -4
  98. package/dist/cli/tui/job-manager.js +5 -1
  99. package/dist/cli/tui/spider-tui.d.ts +63 -0
  100. package/dist/cli/tui/spider-tui.js +120 -2
  101. package/dist/cli/types.d.ts +12 -0
  102. package/dist/cli/types.js +1 -0
  103. package/dist/cli/utils/option-helpers.d.ts +10 -0
  104. package/dist/cli/utils/option-helpers.js +63 -0
  105. package/dist/cli/utils/score-color.d.ts +11 -0
  106. package/dist/cli/utils/score-color.js +11 -0
  107. package/dist/cli/utils/serp-campaign.d.ts +53 -0
  108. package/dist/cli/utils/serp-campaign.js +53 -0
  109. package/dist/cli/utils/serp-config.d.ts +26 -0
  110. package/dist/cli/utils/serp-config.js +125 -0
  111. package/dist/core/client.d.ts +15 -10
  112. package/dist/core/client.js +54 -38
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +1 -0
  115. package/dist/mcp/cli.js +35 -34
  116. package/dist/mcp/client.d.ts +2 -2
  117. package/dist/mcp/client.js +20 -2
  118. package/dist/mcp/contract.d.ts +1 -1
  119. package/dist/mcp/profiles.js +5 -1
  120. package/dist/mcp/prompts/index.js +8 -4
  121. package/dist/mcp/resources/index.js +46 -23
  122. package/dist/mcp/server.js +9 -6
  123. package/dist/mcp/tools/protocols.js +9 -2
  124. package/dist/mcp/tools/registry.js +13 -3
  125. package/dist/mcp/tools/seo.js +427 -2
  126. package/dist/mcp/types.d.ts +5 -1
  127. package/dist/plugins/proxy-rotator.d.ts +2 -2
  128. package/dist/plugins/proxy-rotator.js +6 -28
  129. package/dist/raffel/client.d.ts +38 -0
  130. package/dist/raffel/client.js +282 -0
  131. package/dist/raffel/index.d.ts +2 -0
  132. package/dist/raffel/index.js +2 -0
  133. package/dist/raffel/types.d.ts +40 -0
  134. package/dist/raffel/types.js +14 -0
  135. package/dist/recker.d.ts +13 -7
  136. package/dist/recker.js +58 -6
  137. package/dist/scrape/document.js +2 -2
  138. package/dist/scrape/element.js +7 -1
  139. package/dist/scrape/parser/nodes/html.js +1 -1
  140. package/dist/scrape/spider.d.ts +52 -0
  141. package/dist/scrape/spider.js +620 -38
  142. package/dist/scrape/types.d.ts +2 -0
  143. package/dist/search/google.d.ts +26 -1
  144. package/dist/search/google.js +427 -45
  145. package/dist/search/index.d.ts +1 -1
  146. package/dist/seo/analyzer.d.ts +1 -0
  147. package/dist/seo/analyzer.js +144 -1
  148. package/dist/seo/index.d.ts +1 -1
  149. package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
  150. package/dist/seo/keyword-campaign-analyzer.js +538 -0
  151. package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  152. package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
  153. package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
  154. package/dist/seo/keyword-campaign-seed-core.js +525 -0
  155. package/dist/seo/keyword-campaign-shared.d.ts +165 -0
  156. package/dist/seo/keyword-campaign-shared.js +59 -0
  157. package/dist/seo/keyword-campaign.d.ts +4 -107
  158. package/dist/seo/keyword-campaign.js +2 -380
  159. package/dist/seo/keywords.js +5 -22
  160. package/dist/seo/types.d.ts +19 -0
  161. package/dist/template/index.d.ts +1 -1
  162. package/dist/template/types.d.ts +0 -2
  163. package/dist/testing/index.d.ts +1 -22
  164. package/dist/testing/index.js +1 -11
  165. package/dist/transport/curl.d.ts +5 -1
  166. package/dist/transport/curl.js +207 -50
  167. package/dist/transport/undici.d.ts +4 -3
  168. package/dist/transport/undici.js +123 -49
  169. package/dist/types/index.d.ts +9 -3
  170. package/dist/utils/binary-manager.js +26 -3
  171. package/dist/utils/block-detector.d.ts +8 -0
  172. package/dist/utils/block-detector.js +542 -7
  173. package/dist/version.js +1 -1
  174. package/package.json +12 -1
  175. package/dist/testing/mock-dns-server.d.ts +0 -69
  176. package/dist/testing/mock-dns-server.js +0 -269
  177. package/dist/testing/mock-ftp-server.d.ts +0 -89
  178. package/dist/testing/mock-ftp-server.js +0 -562
  179. package/dist/testing/mock-hls-server.d.ts +0 -80
  180. package/dist/testing/mock-hls-server.js +0 -381
  181. package/dist/testing/mock-http-server.d.ts +0 -124
  182. package/dist/testing/mock-http-server.js +0 -343
  183. package/dist/testing/mock-proxy-server.d.ts +0 -108
  184. package/dist/testing/mock-proxy-server.js +0 -615
  185. package/dist/testing/mock-sse-server.d.ts +0 -76
  186. package/dist/testing/mock-sse-server.js +0 -291
  187. package/dist/testing/mock-telnet-server.d.ts +0 -59
  188. package/dist/testing/mock-telnet-server.js +0 -274
  189. package/dist/testing/mock-udp-server.d.ts +0 -43
  190. package/dist/testing/mock-udp-server.js +0 -188
  191. package/dist/testing/mock-websocket-server.d.ts +0 -76
  192. package/dist/testing/mock-websocket-server.js +0 -334
  193. package/dist/testing/mock-whois-server.d.ts +0 -56
  194. package/dist/testing/mock-whois-server.js +0 -234
  195. package/dist/testing/proxy-certs.d.ts +0 -19
  196. package/dist/testing/proxy-certs.js +0 -208
@@ -0,0 +1,59 @@
1
+ export function normalizeKeyword(value) {
2
+ return value
3
+ .toLowerCase()
4
+ .replace(/[^\p{L}\p{N}\s-]+/gu, ' ')
5
+ .replace(/\s+/g, ' ')
6
+ .trim();
7
+ }
8
+ export function normalizeHost(target) {
9
+ try {
10
+ const parsed = new URL(target);
11
+ return parsed.hostname.replace(/^www\./, '').toLowerCase();
12
+ }
13
+ catch {
14
+ return target.replace(/^www\./, '').toLowerCase();
15
+ }
16
+ }
17
+ export function clampNumber(value, fallback) {
18
+ if (value === undefined || !Number.isFinite(value) || value <= 0)
19
+ return fallback;
20
+ return Math.max(1, Math.floor(value));
21
+ }
22
+ export function normalizePlacement(value) {
23
+ if (value === 'ad')
24
+ return 'ad';
25
+ if (value === 'organic')
26
+ return 'organic';
27
+ return 'unknown';
28
+ }
29
+ export function toSeedArray(seed = []) {
30
+ return seed.map((item) => {
31
+ if (typeof item === 'string') {
32
+ return {
33
+ keyword: item,
34
+ normalizedKeyword: normalizeKeyword(item),
35
+ source: 'preset',
36
+ weight: 1,
37
+ };
38
+ }
39
+ return {
40
+ keyword: item.keyword,
41
+ normalizedKeyword: normalizeKeyword(item.keyword),
42
+ source: item.source ?? 'discovered',
43
+ sourcePage: item.sourcePage,
44
+ weight: item.weight ?? 1,
45
+ };
46
+ });
47
+ }
48
+ export const DEFAULT_SERP_MIN_KEYWORD_LENGTH = 2;
49
+ export function tokenizeForLongTail(value) {
50
+ return value
51
+ .toLowerCase()
52
+ .replace(/[^\p{L}\p{N}\s-]+/gu, ' ')
53
+ .replace(/\s+/g, ' ')
54
+ .trim()
55
+ .split(/\s+/)
56
+ .filter((token) => token.length >= DEFAULT_SERP_MIN_KEYWORD_LENGTH)
57
+ .filter((token) => !token.match(/^\d+$/))
58
+ .slice(0, 80);
59
+ }
@@ -1,107 +1,4 @@
1
- import type { SearchTransport } from '../search/google.js';
2
- import type { SeoReport } from './types.js';
3
- export type KeywordCampaignSource = 'discovered' | 'preset';
4
- export type CampaignResultPlacement = 'ad' | 'organic' | 'unknown';
5
- export interface KeywordCampaignSeedInput {
6
- keyword: string;
7
- source?: KeywordCampaignSource;
8
- sourcePage?: string;
9
- weight?: number;
10
- }
11
- export interface KeywordCampaignSeed {
12
- keyword: string;
13
- normalizedKeyword: string;
14
- source: KeywordCampaignSource;
15
- sourcePage?: string;
16
- weight: number;
17
- }
18
- export interface KeywordCampaignOptions {
19
- targetUrl: string;
20
- discoveredKeywords?: KeywordCampaignSeedInput[] | string[];
21
- presetKeywords?: string[];
22
- minKeywordLength?: number;
23
- maxQueries?: number;
24
- maxResultsPerQuery?: number;
25
- transport?: SearchTransport;
26
- timeout?: number;
27
- country?: string;
28
- gl?: string;
29
- hl?: string;
30
- minWeight?: number;
31
- }
32
- export interface KeywordCampaignCompetitorResult {
33
- domain: string;
34
- rank: number;
35
- placement: CampaignResultPlacement;
36
- placementHint?: string;
37
- url: string;
38
- title: string;
39
- }
40
- export interface KeywordCampaignResult {
41
- keyword: string;
42
- source: KeywordCampaignSource;
43
- sourcePage?: string;
44
- sourceWeight: number;
45
- found: boolean;
46
- bestPosition: number | null;
47
- totalChecked: number;
48
- matchedUrl?: string;
49
- matchedTitle?: string;
50
- matchedDisplayUrl?: string;
51
- placement: CampaignResultPlacement;
52
- placementHint?: string;
53
- searchUrl: string;
54
- searchTransport: SearchTransport;
55
- competitors: KeywordCampaignCompetitorResult[];
56
- }
57
- export interface KeywordCampaignPageStats {
58
- pageUrl: string;
59
- tracked: number;
60
- found: number;
61
- avgPosition: number | null;
62
- top3: number;
63
- top10: number;
64
- }
65
- export interface KeywordCampaignSummary {
66
- queriesRequested: number;
67
- queriesExecuted: number;
68
- queriesFound: number;
69
- avgTopPosition: number | null;
70
- top3Count: number;
71
- top10Count: 0 | number;
72
- topOrganicCompetitors: KeywordCampaignCompetitorSummary[];
73
- topPaidCompetitors: KeywordCampaignCompetitorSummary[];
74
- competitorCoverage: {
75
- organicUniqueDomains: number;
76
- paidUniqueDomains: number;
77
- };
78
- }
79
- export interface KeywordCampaignCompetitorSummary {
80
- domain: string;
81
- organicQueries: number;
82
- paidQueries: number;
83
- bestOrganicRank: number | null;
84
- bestPaidRank: number | null;
85
- matchedKeywords: number;
86
- }
87
- export interface CampaignActivitySignal {
88
- active: boolean;
89
- confidence: 'high' | 'medium' | 'low';
90
- evidence: string[];
91
- }
92
- export interface KeywordCampaignReport {
93
- targetUrl: string;
94
- targetDomain: string;
95
- results: KeywordCampaignResult[];
96
- summary: KeywordCampaignSummary;
97
- pageComparison: KeywordCampaignPageStats[];
98
- campaign: CampaignActivitySignal;
99
- }
100
- export interface KeywordCampaignExtractionOptions {
101
- maxKeywords?: number;
102
- minKeywordLength?: number;
103
- }
104
- export declare function extractKeywordCampaignSeedsFromReport(report: SeoReport, options?: KeywordCampaignExtractionOptions & {
105
- sourcePage?: string;
106
- }): KeywordCampaignSeed[];
107
- export declare function analyzeKeywordCampaign(options: KeywordCampaignOptions): Promise<KeywordCampaignReport>;
1
+ export type { CampaignResultPlacement, KeywordCampaignSource } from './keyword-campaign-shared.js';
2
+ export type { CampaignActivitySignal, CompetitorTracker, KeywordCampaignCompetitorResult, KeywordCampaignCompetitorSummary, KeywordCampaignExtractionOptions, KeywordCampaignOptions, KeywordCampaignPageOutperformance, KeywordCampaignPageStats, KeywordCampaignReport, KeywordCampaignResult, KeywordCampaignSeed, KeywordCampaignSeedInput, KeywordCampaignSummary, } from './keyword-campaign-shared.js';
3
+ export { analyzeKeywordCampaign } from './keyword-campaign-analyzer.js';
4
+ export { extractKeywordCampaignSeedsFromReport } from './keyword-campaign-seed-core.js';
@@ -1,380 +1,2 @@
1
- import { searchGoogleAdvanced } from '../search/google.js';
2
- function normalizeKeyword(value) {
3
- return value
4
- .toLowerCase()
5
- .replace(/[^\p{L}\p{N}\s-]+/gu, ' ')
6
- .replace(/\s+/g, ' ')
7
- .trim();
8
- }
9
- function normalizeHost(target) {
10
- try {
11
- const parsed = new URL(target);
12
- return parsed.hostname.replace(/^www\./, '').toLowerCase();
13
- }
14
- catch {
15
- return target.replace(/^www\./, '').toLowerCase();
16
- }
17
- }
18
- function clampNumber(value, fallback) {
19
- if (value === undefined || !Number.isFinite(value) || value <= 0)
20
- return fallback;
21
- return Math.max(1, Math.floor(value));
22
- }
23
- function normalizePlacement(value) {
24
- if (value === 'ad')
25
- return 'ad';
26
- if (value === 'organic')
27
- return 'organic';
28
- return 'unknown';
29
- }
30
- function toSeedArray(seed = []) {
31
- return seed.map((item) => {
32
- if (typeof item === 'string') {
33
- return {
34
- keyword: item,
35
- normalizedKeyword: normalizeKeyword(item),
36
- source: 'preset',
37
- weight: 1,
38
- };
39
- }
40
- return {
41
- keyword: item.keyword,
42
- normalizedKeyword: normalizeKeyword(item.keyword),
43
- source: item.source ?? 'discovered',
44
- sourcePage: item.sourcePage,
45
- weight: item.weight ?? 1,
46
- };
47
- });
48
- }
49
- export function extractKeywordCampaignSeedsFromReport(report, options = {}) {
50
- const maxKeywords = options.maxKeywords ?? 10;
51
- const minKeywordLength = options.minKeywordLength ?? 2;
52
- return (report.keywords?.topKeywords ?? [])
53
- .map((item) => ({
54
- keyword: item.word,
55
- normalizedKeyword: normalizeKeyword(item.word),
56
- source: 'discovered',
57
- sourcePage: options.sourcePage,
58
- weight: Math.max(1, Math.round(item.count)),
59
- }))
60
- .filter(seed => seed.keyword.trim().length >= minKeywordLength
61
- && seed.normalizedKeyword.length >= minKeywordLength
62
- && seed.weight > 0)
63
- .slice(0, maxKeywords);
64
- }
65
- function sortSeedsByWeight(a, b) {
66
- if (a.weight === b.weight) {
67
- return a.normalizedKeyword.localeCompare(b.normalizedKeyword);
68
- }
69
- return b.weight - a.weight;
70
- }
71
- function mergeKeywordSeeds(discovered, preset, minKeywordLength = 2, minWeight = 1) {
72
- const merged = new Map();
73
- const upsert = (seed) => {
74
- if (!seed.normalizedKeyword || seed.normalizedKeyword.length < minKeywordLength)
75
- return;
76
- if (seed.weight < minWeight)
77
- return;
78
- const existing = merged.get(seed.normalizedKeyword);
79
- if (!existing) {
80
- merged.set(seed.normalizedKeyword, seed);
81
- return;
82
- }
83
- if (existing.source === 'preset' && seed.source === 'discovered') {
84
- merged.set(seed.normalizedKeyword, seed);
85
- return;
86
- }
87
- if (seed.source === existing.source && seed.weight > existing.weight) {
88
- merged.set(seed.normalizedKeyword, seed);
89
- }
90
- };
91
- for (const seed of discovered)
92
- upsert(seed);
93
- for (const seed of preset)
94
- upsert(seed);
95
- return [...merged.values()].sort(sortSeedsByWeight);
96
- }
97
- function buildEmptyReport(targetUrl, targetDomain) {
98
- return {
99
- targetUrl,
100
- targetDomain,
101
- results: [],
102
- summary: {
103
- queriesRequested: 0,
104
- queriesExecuted: 0,
105
- queriesFound: 0,
106
- avgTopPosition: null,
107
- top3Count: 0,
108
- top10Count: 0,
109
- topOrganicCompetitors: [],
110
- topPaidCompetitors: [],
111
- competitorCoverage: {
112
- organicUniqueDomains: 0,
113
- paidUniqueDomains: 0,
114
- },
115
- },
116
- pageComparison: [],
117
- campaign: {
118
- active: false,
119
- confidence: 'low',
120
- evidence: [],
121
- },
122
- };
123
- }
124
- function dedupeDomainResults(results, targetDomain) {
125
- const seen = new Set();
126
- const domainResults = [];
127
- for (const result of results) {
128
- if (!result.domain || result.domain === targetDomain)
129
- continue;
130
- if (seen.has(result.domain))
131
- continue;
132
- seen.add(result.domain);
133
- domainResults.push({
134
- domain: result.domain,
135
- rank: result.rank,
136
- placement: result.placement,
137
- placementHint: result.placementHint,
138
- url: result.url,
139
- title: result.title,
140
- });
141
- }
142
- return domainResults;
143
- }
144
- function trackCompetitors(candidates, keyword, targetDomain, trackers) {
145
- for (const competitor of candidates) {
146
- if (competitor.domain === targetDomain)
147
- continue;
148
- const current = trackers.get(competitor.domain) ?? {
149
- organicQueries: 0,
150
- paidQueries: 0,
151
- bestOrganicRank: null,
152
- bestPaidRank: null,
153
- keywords: new Set(),
154
- };
155
- if (competitor.placement === 'ad') {
156
- current.paidQueries += 1;
157
- if (current.bestPaidRank === null || competitor.rank < current.bestPaidRank) {
158
- current.bestPaidRank = competitor.rank;
159
- }
160
- }
161
- else {
162
- current.organicQueries += 1;
163
- if (current.bestOrganicRank === null || competitor.rank < current.bestOrganicRank) {
164
- current.bestOrganicRank = competitor.rank;
165
- }
166
- }
167
- current.keywords.add(keyword);
168
- trackers.set(competitor.domain, current);
169
- }
170
- }
171
- function finalizeCompetitorSummary(trackers) {
172
- const ranked = [...trackers.entries()].map(([domain, tracker]) => ({
173
- domain,
174
- organicQueries: tracker.organicQueries,
175
- paidQueries: tracker.paidQueries,
176
- bestOrganicRank: tracker.bestOrganicRank,
177
- bestPaidRank: tracker.bestPaidRank,
178
- matchedKeywords: tracker.keywords.size,
179
- score: (tracker.organicQueries * 2) + (tracker.paidQueries * 3),
180
- }));
181
- const scoreSort = (a, b) => b.score - a.score || a.domain.localeCompare(b.domain);
182
- const topOrganic = ranked
183
- .filter((entry) => entry.organicQueries > 0)
184
- .sort(scoreSort)
185
- .map(({ score: _ignored, ...entry }) => entry)
186
- .slice(0, 10);
187
- const topPaid = ranked
188
- .filter((entry) => entry.paidQueries > 0)
189
- .sort(scoreSort)
190
- .map(({ score: _ignored, ...entry }) => entry)
191
- .slice(0, 10);
192
- return {
193
- topOrganic,
194
- topPaid,
195
- coverage: {
196
- organicUniqueDomains: ranked.filter((entry) => entry.organicQueries > 0).length,
197
- paidUniqueDomains: ranked.filter((entry) => entry.paidQueries > 0).length,
198
- },
199
- };
200
- }
201
- export async function analyzeKeywordCampaign(options) {
202
- const targetUrl = options.targetUrl.trim();
203
- const targetDomain = normalizeHost(targetUrl);
204
- const queryLimit = clampNumber(options.maxQueries, 20);
205
- const defaultCampaignResultLimit = 25;
206
- const searchResultLimit = Math.max(clampNumber(options.maxResultsPerQuery, defaultCampaignResultLimit), 20);
207
- const minKeywordLength = options.minKeywordLength ?? 2;
208
- const minWeight = Math.max(1, options.minWeight ?? 1);
209
- const discovered = toSeedArray(options.discoveredKeywords ?? []);
210
- const preset = (options.presetKeywords ?? []).map((keyword) => ({
211
- keyword,
212
- normalizedKeyword: normalizeKeyword(keyword),
213
- source: 'preset',
214
- weight: 1,
215
- }));
216
- const seeds = mergeKeywordSeeds(discovered, preset, minKeywordLength, minWeight).slice(0, queryLimit);
217
- const report = buildEmptyReport(targetUrl, targetDomain);
218
- if (seeds.length === 0 || !targetDomain) {
219
- report.summary.queriesRequested = 0;
220
- return report;
221
- }
222
- const pageBuckets = new Map();
223
- const competitorTrackers = new Map();
224
- const campaignEvidence = [];
225
- report.summary.queriesRequested = seeds.length;
226
- for (const seed of seeds) {
227
- const query = seed.keyword;
228
- try {
229
- const response = await searchGoogleAdvanced(query, {
230
- num: searchResultLimit,
231
- transport: options.transport,
232
- timeout: options.timeout,
233
- country: options.country,
234
- gl: options.gl,
235
- hl: options.hl,
236
- });
237
- report.summary.queriesExecuted += 1;
238
- const parsedResults = response.results.map((result) => ({
239
- ...result,
240
- domain: normalizeHost(result.url),
241
- placement: normalizePlacement(result.placement),
242
- }));
243
- const matched = parsedResults.find((result) => {
244
- return result.domain && result.domain === targetDomain;
245
- });
246
- const competitorCandidates = dedupeDomainResults(parsedResults.map((result) => ({
247
- domain: result.domain,
248
- rank: result.rank,
249
- placement: result.placement,
250
- placementHint: result.placementHint,
251
- url: result.url,
252
- title: result.title,
253
- })), targetDomain);
254
- trackCompetitors(competitorCandidates, seed.normalizedKeyword, targetDomain, competitorTrackers);
255
- const searchResult = {
256
- keyword: query,
257
- source: seed.source,
258
- sourcePage: seed.sourcePage,
259
- sourceWeight: seed.weight,
260
- found: Boolean(matched),
261
- bestPosition: matched?.rank ?? null,
262
- totalChecked: parsedResults.length,
263
- matchedUrl: matched?.url,
264
- matchedTitle: matched?.title,
265
- matchedDisplayUrl: matched?.displayedUrl,
266
- placement: matched ? normalizePlacement(matched.placement) : 'unknown',
267
- placementHint: matched?.placementHint,
268
- searchUrl: response.searchUrl,
269
- searchTransport: response.transport.used,
270
- competitors: competitorCandidates,
271
- };
272
- if (searchResult.found && searchResult.bestPosition !== null) {
273
- const position = searchResult.bestPosition;
274
- report.summary.queriesFound += 1;
275
- if (position <= 3)
276
- report.summary.top3Count += 1;
277
- if (position <= 10)
278
- report.summary.top10Count += 1;
279
- if (searchResult.placement === 'ad') {
280
- campaignEvidence.push(`${query} aparece como anúncio em posição #${position}`);
281
- }
282
- else {
283
- campaignEvidence.push(`${query} aparece orgânico em posição #${position}`);
284
- }
285
- const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
286
- const bucket = pageBuckets.get(pageKey) ?? {
287
- tracked: 0,
288
- found: 0,
289
- totalPosition: 0,
290
- positions: [],
291
- top3: 0,
292
- top10: 0,
293
- };
294
- bucket.tracked += 1;
295
- bucket.found += 1;
296
- bucket.totalPosition += position;
297
- bucket.positions.push(position);
298
- if (position <= 3)
299
- bucket.top3 += 1;
300
- if (position <= 10)
301
- bucket.top10 += 1;
302
- pageBuckets.set(pageKey, bucket);
303
- }
304
- else {
305
- const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
306
- const bucket = pageBuckets.get(pageKey) ?? {
307
- tracked: 0,
308
- found: 0,
309
- totalPosition: 0,
310
- positions: [],
311
- top3: 0,
312
- top10: 0,
313
- };
314
- bucket.tracked += 1;
315
- pageBuckets.set(pageKey, bucket);
316
- }
317
- report.results.push(searchResult);
318
- }
319
- catch {
320
- const pageKey = seed.sourcePage ?? (seed.source === 'preset' ? 'preset-queries' : 'unknown');
321
- const bucket = pageBuckets.get(pageKey) ?? {
322
- tracked: 0,
323
- found: 0,
324
- totalPosition: 0,
325
- positions: [],
326
- top3: 0,
327
- top10: 0,
328
- };
329
- bucket.tracked += 1;
330
- pageBuckets.set(pageKey, bucket);
331
- report.summary.queriesExecuted += 1;
332
- report.results.push({
333
- keyword: seed.keyword,
334
- source: seed.source,
335
- sourcePage: seed.sourcePage,
336
- sourceWeight: seed.weight,
337
- found: false,
338
- bestPosition: null,
339
- totalChecked: searchResultLimit,
340
- placement: 'unknown',
341
- searchUrl: '',
342
- searchTransport: options.transport ?? 'undici',
343
- competitors: [],
344
- });
345
- campaignEvidence.push(`Falha ao buscar keyword "${seed.keyword}"`);
346
- }
347
- }
348
- const foundPositions = report.results
349
- .filter((entry) => entry.found && entry.bestPosition !== null)
350
- .map((entry) => entry.bestPosition);
351
- if (foundPositions.length > 0) {
352
- report.summary.avgTopPosition = Math.round(foundPositions.reduce((acc, pos) => acc + pos, 0) / foundPositions.length);
353
- }
354
- const hasTop3 = foundPositions.some((position) => position <= 3);
355
- const hasTop10 = foundPositions.some((position) => position <= 10);
356
- report.campaign = {
357
- active: foundPositions.some((position) => position <= 10),
358
- confidence: hasTop3 ? 'high' : hasTop10 ? 'medium' : foundPositions.length > 0 ? 'low' : 'low',
359
- evidence: campaignEvidence,
360
- };
361
- const competitorSummary = finalizeCompetitorSummary(competitorTrackers);
362
- report.summary.topOrganicCompetitors = competitorSummary.topOrganic;
363
- report.summary.topPaidCompetitors = competitorSummary.topPaid;
364
- report.summary.competitorCoverage = competitorSummary.coverage;
365
- report.pageComparison = [...pageBuckets.entries()].map(([pageUrl, bucket]) => ({
366
- pageUrl,
367
- tracked: bucket.tracked,
368
- found: bucket.found,
369
- avgPosition: bucket.found > 0 ? Math.round(bucket.totalPosition / bucket.found) : null,
370
- top3: bucket.top3,
371
- top10: bucket.top10,
372
- })).sort((a, b) => {
373
- if (a.avgPosition === null)
374
- return 1;
375
- if (b.avgPosition === null)
376
- return -1;
377
- return a.avgPosition - b.avgPosition;
378
- });
379
- return report;
380
- }
1
+ export { analyzeKeywordCampaign } from './keyword-campaign-analyzer.js';
2
+ export { extractKeywordCampaignSeedsFromReport } from './keyword-campaign-seed-core.js';
@@ -1,28 +1,11 @@
1
- const STOP_WORDS = new Set([
2
- 'a', 'an', 'the', 'and', 'or', 'but', 'if', 'because', 'as', 'what',
3
- 'when', 'where', 'how', 'who', 'which', 'this', 'that', 'these', 'those',
4
- 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
5
- 'do', 'does', 'did', 'at', 'by', 'for', 'from', 'in', 'into', 'of', 'off',
6
- 'on', 'onto', 'out', 'over', 'to', 'up', 'with', 'about', 'some', 'any',
7
- 'it', 'its', 'you', 'your', 'we', 'our', 'they', 'their', 'he', 'his',
8
- 'she', 'her', 'i', 'my', 'me', 'not', 'no', 'can', 'will', 'just',
9
- 'o', 'a', 'os', 'as', 'um', 'uma', 'uns', 'umas', 'e', 'ou', 'mas', 'se',
10
- 'porque', 'como', 'quando', 'onde', 'que', 'quem', 'qual', 'este', 'esta',
11
- 'esse', 'essa', 'aquele', 'aquela', 'isto', 'isso', 'aquilo', 'é', 'são',
12
- 'foi', 'foram', 'ser', 'sendo', 'ter', 'tem', 'tinha', 'fazer', 'faz', 'fez',
13
- 'em', 'no', 'na', 'nos', 'nas', 'de', 'do', 'da', 'dos', 'das', 'por',
14
- 'para', 'com', 'sem', 'sobre', 'sob', 'ante', 'até', 'ao', 'aos',
15
- 'eu', 'meu', 'minha', 'você', 'seu', 'sua', 'nós', 'nosso', 'nossa',
16
- 'eles', 'elas', 'dele', 'dela', 'não', 'sim', 'muito', 'muita', 'mais',
17
- 'pelo', 'pela', 'pelos', 'pelas', 'num', 'numa', 'está', 'estão'
18
- ]);
1
+ const RE_NON_WORD = /[^\w\s\u00C0-\u00FF]/g;
2
+ const RE_WHITESPACE = /\s+/;
19
3
  function tokenize(text) {
20
4
  return text
21
5
  .toLowerCase()
22
- .replace(/[^\w\s\u00C0-\u00FF]/g, '')
23
- .split(/\s+/)
24
- .filter(w => w.length > 2)
25
- .filter(w => !STOP_WORDS.has(w));
6
+ .replace(RE_NON_WORD, '')
7
+ .split(RE_WHITESPACE)
8
+ .filter(w => w.length > 2);
26
9
  }
27
10
  export function generateKeywordCloud(sources, limit = 20) {
28
11
  const combinedText = [
@@ -29,12 +29,28 @@ export interface HeadingInfo {
29
29
  text: string;
30
30
  count: number;
31
31
  }
32
+ export interface HeadingNode {
33
+ level: number;
34
+ text: string;
35
+ children: HeadingNode[];
36
+ }
32
37
  export interface HeadingAnalysis {
33
38
  structure: HeadingInfo[];
39
+ tree?: HeadingNode[];
34
40
  h1Count: number;
35
41
  hasProperHierarchy: boolean;
36
42
  issues: string[];
37
43
  }
44
+ export interface SeoContentSectionSignal {
45
+ headingPath: string[];
46
+ source: 'heading' | 'paragraph' | 'list-item' | 'table' | 'figure' | 'meta' | 'other';
47
+ tagName: string;
48
+ headingLevel: number;
49
+ text: string;
50
+ wordCount: number;
51
+ linkDensity: number;
52
+ weight: number;
53
+ }
38
54
  export interface ContentMetrics {
39
55
  wordCount: number;
40
56
  totalWordCount?: number;
@@ -199,6 +215,9 @@ export interface SeoReport {
199
215
  images: ImageAnalysis;
200
216
  social: SocialMetaAnalysis;
201
217
  technical: TechnicalSeo;
218
+ linkAnchorTexts?: string[];
219
+ linkUrlSamples?: string[];
220
+ contentSections?: SeoContentSectionSignal[];
202
221
  }
203
222
  export interface SeoAnalyzerOptions {
204
223
  baseUrl?: string;
@@ -5,4 +5,4 @@ export { escapeRaw, escapeHtml, escapeXml, escapeJson, escapeUrl, escapeUrlCompo
5
5
  export { registerHelper, registerHelpers, unregisterHelper, getHelper, hasHelper, getHelperNames, getAllHelpers, clearHelpers, resetHelpers, registerBuiltinHelpers, getHelperCategories, getHelpersByCategory, searchHelpers, coreHelpers, } from './helpers/index.js';
6
6
  export { isTruthy, isEmpty } from './helpers/core.js';
7
7
  export { TemplateError, TemplateSyntaxError, UnclosedBlockError, MismatchedBlockError, UnexpectedCloseBlockError, TemplateRuntimeError, UndefinedVariableError, UnknownHelperError, UnknownFilterError, UnknownPartialError, HelperError, FilterError, TemplateFileError, TemplateNotFoundError, CircularIncludeError, TemplateValidationError, isTemplateError, wrapError, } from './errors.js';
8
- export type { TemplateContext, TemplateOptions, TemplateFormat, RenderOptions, TemplateEngineInterface, CompiledTemplate, CompiledTemplateSync, ValidationResult, ValidationError, ValidationWarning, ASTNodeType, ASTNode, SourceLocation, Position, ProgramNode, TextNode, ExpressionNode, RawExpressionNode, MustacheNode, RawMustacheNode, BlockNode, PartialNode, CommentNode, PathExpression, Expression, StringLiteral, NumberLiteral, BooleanLiteral, NullLiteral, SubExpression, HashPair, FilterExpression, TemplateHelper, HelperContext, BlockHelperOptions, TemplateFilter, HelperCategory, HelperDefinition, CacheEntry, TemplateCache, ContextBuilderOptions, RequestContext, ResponseContext, } from './types.js';
8
+ export type { TemplateContext, TemplateOptions, TemplateFormat, RenderOptions, TemplateEngineInterface, CompiledTemplate, CompiledTemplateSync, ValidationResult, ValidationError, ValidationWarning, ASTNodeType, ASTNode, SourceLocation, Position, ProgramNode, TextNode, ExpressionNode, RawExpressionNode, BlockNode, PartialNode, CommentNode, PathExpression, Expression, StringLiteral, NumberLiteral, BooleanLiteral, NullLiteral, SubExpression, HashPair, FilterExpression, TemplateHelper, HelperContext, BlockHelperOptions, TemplateFilter, HelperCategory, HelperDefinition, CacheEntry, TemplateCache, ContextBuilderOptions, RequestContext, ResponseContext, } from './types.js';
@@ -50,8 +50,6 @@ export interface RawExpressionNode extends ASTNode {
50
50
  hash: HashPair[];
51
51
  filters: FilterExpression[];
52
52
  }
53
- export type MustacheNode = ExpressionNode;
54
- export type RawMustacheNode = RawExpressionNode;
55
53
  export interface BlockNode extends ASTNode {
56
54
  type: 'BlockNode';
57
55
  path: PathExpression;
@@ -1,24 +1,3 @@
1
1
  export { MockClient, MockTransport, createMockClient, installGlobalMock, uninstallGlobalMock, MockAgent, } from './mock.js';
2
2
  export type { MockResponseOptions, MockInterceptOptions, } from './mock.js';
3
- export { MockUDPServer, createMockUDPServer, } from './mock-udp-server.js';
4
- export type { MockUDPServerOptions, ReceivedMessage, } from './mock-udp-server.js';
5
- export { MockHlsServer, createMockHlsVod, createMockHlsLive, createMockHlsMultiQuality, } from './mock-hls-server.js';
6
- export type { MockHlsServerOptions, MockHlsVariant, MockHlsSegment, MockHlsStats, } from './mock-hls-server.js';
7
- export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocket-server.js';
8
- export type { MockWebSocketServerOptions, MockWebSocketClient, MockWebSocketMessage, MockWebSocketStats, } from './mock-websocket-server.js';
9
- export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
10
- export type { MockSSEServerOptions, SSEEvent, MockSSEClient, MockSSEStats, } from './mock-sse-server.js';
11
- export { MockHttpServer, createMockHttpServer, createWebhookServer, } from './mock-http-server.js';
12
- export type { MockHttpServerOptions, MockHttpResponse, MockHttpRequest, MockHttpHandler, MockHttpStats, WebhookServerOptions, WebhookPayload, } from './mock-http-server.js';
13
- export { MockDnsServer, } from './mock-dns-server.js';
14
- export type { MockDnsServerOptions, DnsRecordType, DnsRecord, DnsMxRecord, DnsSoaRecord, DnsSrvRecord, MockDnsStats, } from './mock-dns-server.js';
15
- export { MockWhoisServer, } from './mock-whois-server.js';
16
- export type { MockWhoisServerOptions, WhoisDomainData, MockWhoisStats, } from './mock-whois-server.js';
17
- export { MockTelnetServer, } from './mock-telnet-server.js';
18
- export type { MockTelnetServerOptions, CommandHandler, TelnetSession, MockTelnetStats, } from './mock-telnet-server.js';
19
- export { MockFtpServer, } from './mock-ftp-server.js';
20
- export type { MockFtpServerOptions, VirtualFile, FtpSession, MockFtpStats, } from './mock-ftp-server.js';
21
- export { MockProxyServer, createForwardProxy, createInterceptProxy, } from './mock-proxy-server.js';
22
- export type { ProxyServerOptions, ProxyMode, ProxyRequest, ProxyResponse, ProxyError, ProxyStats, } from './mock-proxy-server.js';
23
- export { generateCertificate, generateCA, getDefaultCA, } from './proxy-certs.js';
24
- export type { CertificateInfo, CertificateOptions, } from './proxy-certs.js';
3
+ export * from 'raffel/testing';