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.
- package/dist/browser/browser/mini.d.ts +2 -2
- package/dist/browser/browser/recker-mini.d.ts +8 -8
- package/dist/browser/browser/recker.d.ts +11 -8
- package/dist/browser/browser/recker.js +54 -6
- package/dist/browser/core/client.d.ts +15 -10
- package/dist/browser/core/client.js +54 -38
- package/dist/browser/index.iife.min.js +129 -130
- package/dist/browser/index.min.js +129 -130
- package/dist/browser/index.mini.iife.js +5697 -636
- package/dist/browser/index.mini.iife.min.js +47 -48
- package/dist/browser/index.mini.min.js +47 -48
- package/dist/browser/index.mini.umd.js +5697 -636
- package/dist/browser/index.mini.umd.min.js +47 -48
- package/dist/browser/index.umd.min.js +129 -130
- package/dist/browser/mini.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.js +6 -28
- package/dist/browser/recker-mini.d.ts +8 -8
- package/dist/browser/recker.d.ts +11 -8
- package/dist/browser/recker.js +54 -6
- package/dist/browser/scrape/document.js +2 -2
- package/dist/browser/scrape/element.js +7 -1
- package/dist/browser/scrape/parser/nodes/html.js +1 -1
- package/dist/browser/scrape/spider.d.ts +52 -0
- package/dist/browser/scrape/spider.js +620 -38
- package/dist/browser/scrape/types.d.ts +2 -0
- package/dist/browser/search/google.d.ts +26 -1
- package/dist/browser/search/google.js +427 -45
- package/dist/browser/seo/analyzer.d.ts +1 -0
- package/dist/browser/seo/analyzer.js +144 -1
- package/dist/browser/seo/index.d.ts +1 -1
- package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/browser/seo/keyword-campaign-shared.js +59 -0
- package/dist/browser/seo/keyword-campaign.d.ts +4 -107
- package/dist/browser/seo/keyword-campaign.js +2 -380
- package/dist/browser/seo/keywords.js +5 -22
- package/dist/browser/seo/types.d.ts +19 -0
- package/dist/browser/transport/curl.d.ts +5 -1
- package/dist/browser/transport/curl.js +207 -50
- package/dist/browser/transport/undici.d.ts +4 -3
- package/dist/browser/transport/undici.js +123 -49
- package/dist/browser/types/index.d.ts +9 -3
- package/dist/browser/utils/binary-manager.js +26 -3
- package/dist/browser/utils/block-detector.d.ts +8 -0
- package/dist/browser/utils/block-detector.js +542 -7
- package/dist/cli/commands/hls-runner.js +5 -4
- package/dist/cli/commands/live-runner.js +5 -4
- package/dist/cli/commands/loadtest-runner.js +3 -2
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.js +105 -0
- package/dist/cli/commands/seo-runner.js +9 -7
- package/dist/cli/commands/seo.js +140 -1
- package/dist/cli/commands/serve.js +75 -131
- package/dist/cli/commands/server-runner.js +59 -82
- package/dist/cli/commands/spider-runner.d.ts +37 -1
- package/dist/cli/commands/spider-runner.js +134 -10
- package/dist/cli/commands/spider.d.ts +18 -1
- package/dist/cli/commands/spider.js +457 -27
- package/dist/cli/events/handlers/cli.js +30 -1
- package/dist/cli/events/handlers/tui.js +26 -0
- package/dist/cli/events/types.d.ts +27 -0
- package/dist/cli/handler.d.ts +2 -12
- package/dist/cli/handler.js +20 -15
- package/dist/cli/handlers/protocols.js +39 -12
- package/dist/cli/handlers/search.d.ts +2 -0
- package/dist/cli/handlers/search.js +171 -0
- package/dist/cli/handlers/seo-analyze.d.ts +1 -0
- package/dist/cli/handlers/seo-analyze.js +666 -0
- package/dist/cli/handlers/seo-robots.d.ts +1 -0
- package/dist/cli/handlers/seo-robots.js +76 -0
- package/dist/cli/handlers/seo-serp.d.ts +54 -0
- package/dist/cli/handlers/seo-serp.js +243 -0
- package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
- package/dist/cli/handlers/seo-sitemap.js +55 -0
- package/dist/cli/handlers/seo-spider.d.ts +1 -0
- package/dist/cli/handlers/seo-spider.js +334 -0
- package/dist/cli/handlers/seo.d.ts +8 -4
- package/dist/cli/handlers/seo.js +294 -442
- package/dist/cli/handlers/spider.js +94 -17
- package/dist/cli/handlers/streaming.js +5 -1
- package/dist/cli/index.js +11 -2
- package/dist/cli/presets.d.ts +1 -1
- package/dist/cli/presets.js +15 -4
- package/dist/cli/tui/app.js +6 -1
- package/dist/cli/tui/components/rich-response.d.ts +72 -0
- package/dist/cli/tui/components/rich-response.js +117 -0
- package/dist/cli/tui/executor-commands/background.js +30 -24
- package/dist/cli/tui/executor-commands/testing.js +3 -2
- package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
- package/dist/cli/tui/hooks/useHelp.js +15 -2
- package/dist/cli/tui/job-manager.d.ts +4 -4
- package/dist/cli/tui/job-manager.js +5 -1
- package/dist/cli/tui/spider-tui.d.ts +63 -0
- package/dist/cli/tui/spider-tui.js +120 -2
- package/dist/cli/types.d.ts +12 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils/option-helpers.d.ts +10 -0
- package/dist/cli/utils/option-helpers.js +63 -0
- package/dist/cli/utils/score-color.d.ts +11 -0
- package/dist/cli/utils/score-color.js +11 -0
- package/dist/cli/utils/serp-campaign.d.ts +53 -0
- package/dist/cli/utils/serp-campaign.js +53 -0
- package/dist/cli/utils/serp-config.d.ts +26 -0
- package/dist/cli/utils/serp-config.js +125 -0
- package/dist/core/client.d.ts +15 -10
- package/dist/core/client.js +54 -38
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli.js +35 -34
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +20 -2
- package/dist/mcp/contract.d.ts +1 -1
- package/dist/mcp/profiles.js +5 -1
- package/dist/mcp/prompts/index.js +8 -4
- package/dist/mcp/resources/index.js +46 -23
- package/dist/mcp/server.js +9 -6
- package/dist/mcp/tools/protocols.js +9 -2
- package/dist/mcp/tools/registry.js +13 -3
- package/dist/mcp/tools/seo.js +427 -2
- package/dist/mcp/types.d.ts +5 -1
- package/dist/plugins/proxy-rotator.d.ts +2 -2
- package/dist/plugins/proxy-rotator.js +6 -28
- package/dist/raffel/client.d.ts +38 -0
- package/dist/raffel/client.js +282 -0
- package/dist/raffel/index.d.ts +2 -0
- package/dist/raffel/index.js +2 -0
- package/dist/raffel/types.d.ts +40 -0
- package/dist/raffel/types.js +14 -0
- package/dist/recker.d.ts +13 -7
- package/dist/recker.js +58 -6
- package/dist/scrape/document.js +2 -2
- package/dist/scrape/element.js +7 -1
- package/dist/scrape/parser/nodes/html.js +1 -1
- package/dist/scrape/spider.d.ts +52 -0
- package/dist/scrape/spider.js +620 -38
- package/dist/scrape/types.d.ts +2 -0
- package/dist/search/google.d.ts +26 -1
- package/dist/search/google.js +427 -45
- package/dist/search/index.d.ts +1 -1
- package/dist/seo/analyzer.d.ts +1 -0
- package/dist/seo/analyzer.js +144 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/seo/keyword-campaign-shared.js +59 -0
- package/dist/seo/keyword-campaign.d.ts +4 -107
- package/dist/seo/keyword-campaign.js +2 -380
- package/dist/seo/keywords.js +5 -22
- package/dist/seo/types.d.ts +19 -0
- package/dist/template/index.d.ts +1 -1
- package/dist/template/types.d.ts +0 -2
- package/dist/testing/index.d.ts +1 -22
- package/dist/testing/index.js +1 -11
- package/dist/transport/curl.d.ts +5 -1
- package/dist/transport/curl.js +207 -50
- package/dist/transport/undici.d.ts +4 -3
- package/dist/transport/undici.js +123 -49
- package/dist/types/index.d.ts +9 -3
- package/dist/utils/binary-manager.js +26 -3
- package/dist/utils/block-detector.d.ts +8 -0
- package/dist/utils/block-detector.js +542 -7
- package/dist/version.js +1 -1
- package/package.json +12 -1
- package/dist/testing/mock-dns-server.d.ts +0 -69
- package/dist/testing/mock-dns-server.js +0 -269
- package/dist/testing/mock-ftp-server.d.ts +0 -89
- package/dist/testing/mock-ftp-server.js +0 -562
- package/dist/testing/mock-hls-server.d.ts +0 -80
- package/dist/testing/mock-hls-server.js +0 -381
- package/dist/testing/mock-http-server.d.ts +0 -124
- package/dist/testing/mock-http-server.js +0 -343
- package/dist/testing/mock-proxy-server.d.ts +0 -108
- package/dist/testing/mock-proxy-server.js +0 -615
- package/dist/testing/mock-sse-server.d.ts +0 -76
- package/dist/testing/mock-sse-server.js +0 -291
- package/dist/testing/mock-telnet-server.d.ts +0 -59
- package/dist/testing/mock-telnet-server.js +0 -274
- package/dist/testing/mock-udp-server.d.ts +0 -43
- package/dist/testing/mock-udp-server.js +0 -188
- package/dist/testing/mock-websocket-server.d.ts +0 -76
- package/dist/testing/mock-websocket-server.js +0 -334
- package/dist/testing/mock-whois-server.d.ts +0 -56
- package/dist/testing/mock-whois-server.js +0 -234
- package/dist/testing/proxy-certs.d.ts +0 -19
- package/dist/testing/proxy-certs.js +0 -208
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { DEFAULT_SERP_MIN_KEYWORD_LENGTH, normalizeKeyword, tokenizeForLongTail, } from './keyword-campaign-shared.js';
|
|
2
|
+
function isLowValueLongTail(tokens) {
|
|
3
|
+
const normalized = tokens.map((value) => normalizeKeyword(value)).filter(Boolean);
|
|
4
|
+
if (normalized.length === 0)
|
|
5
|
+
return true;
|
|
6
|
+
if (normalized.length === 1)
|
|
7
|
+
return true;
|
|
8
|
+
if (new Set(normalized).size === 1)
|
|
9
|
+
return true;
|
|
10
|
+
if (normalized.every((token) => token.length <= 3))
|
|
11
|
+
return true;
|
|
12
|
+
if (normalized.length <= 3 && normalized.every((token) => token.length <= 4))
|
|
13
|
+
return true;
|
|
14
|
+
const shortTokenCount = normalized.filter((token) => token.length <= 2).length;
|
|
15
|
+
if (shortTokenCount >= Math.max(1, Math.floor(normalized.length / 2)))
|
|
16
|
+
return true;
|
|
17
|
+
return normalized.some((token) => token.match(/^\d+$/));
|
|
18
|
+
}
|
|
19
|
+
export function deduplicateKeywordPermutations(seeds) {
|
|
20
|
+
const deduplicated = new Map();
|
|
21
|
+
const pickBetterSeed = (candidate, existing) => {
|
|
22
|
+
if (candidate.weight !== existing.weight) {
|
|
23
|
+
return candidate.weight > existing.weight ? candidate : existing;
|
|
24
|
+
}
|
|
25
|
+
const candidateWords = getSeedWordCount(candidate);
|
|
26
|
+
const existingWords = getSeedWordCount(existing);
|
|
27
|
+
if (candidateWords !== existingWords) {
|
|
28
|
+
return candidateWords < existingWords ? candidate : existing;
|
|
29
|
+
}
|
|
30
|
+
return candidate.normalizedKeyword.localeCompare(existing.normalizedKeyword) < 0
|
|
31
|
+
? candidate
|
|
32
|
+
: existing;
|
|
33
|
+
};
|
|
34
|
+
for (const seed of seeds) {
|
|
35
|
+
const key = normalizeSeedPermutationKey(seed.normalizedKeyword);
|
|
36
|
+
const existing = deduplicated.get(key);
|
|
37
|
+
if (!existing) {
|
|
38
|
+
deduplicated.set(key, seed);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
deduplicated.set(key, pickBetterSeed(seed, existing));
|
|
42
|
+
}
|
|
43
|
+
return [...deduplicated.values()];
|
|
44
|
+
}
|
|
45
|
+
function normalizeSeedPermutationKey(value) {
|
|
46
|
+
const normalized = normalizeKeyword(value);
|
|
47
|
+
const tokens = normalized.split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
48
|
+
if (tokens.length === 0)
|
|
49
|
+
return '';
|
|
50
|
+
if (tokens.length === 1)
|
|
51
|
+
return tokens[0];
|
|
52
|
+
return [...tokens].sort().join(' ');
|
|
53
|
+
}
|
|
54
|
+
export function getSeedWordCount(seed) {
|
|
55
|
+
return seed.normalizedKeyword.split(/\s+/).filter(Boolean).length;
|
|
56
|
+
}
|
|
57
|
+
export function extractSchemaOrgLongTailSeeds(report, options) {
|
|
58
|
+
const items = report.structuredData?.items;
|
|
59
|
+
if (!items?.length)
|
|
60
|
+
return [];
|
|
61
|
+
const candidates = [];
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const type = (item['@type'] ?? '').toLowerCase();
|
|
64
|
+
if (type === 'faqpage') {
|
|
65
|
+
const entities = item['mainEntity'];
|
|
66
|
+
if (Array.isArray(entities)) {
|
|
67
|
+
for (const entity of entities) {
|
|
68
|
+
const q = entity['name']?.trim();
|
|
69
|
+
const normalized = q ? normalizeKeyword(q) : '';
|
|
70
|
+
const words = normalized.length > 0 ? normalized.split(/\s+/).filter(Boolean).length : 0;
|
|
71
|
+
if (q
|
|
72
|
+
&& q.length >= options.minKeywordLength
|
|
73
|
+
&& words >= 2
|
|
74
|
+
&& words <= 4) {
|
|
75
|
+
candidates.push({ phrase: q, weight: 3.5 });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (type === 'howto') {
|
|
81
|
+
const steps = item['step'];
|
|
82
|
+
if (Array.isArray(steps)) {
|
|
83
|
+
for (const step of steps) {
|
|
84
|
+
const s = step;
|
|
85
|
+
const text = (s['name'] ?? s['text'])?.trim();
|
|
86
|
+
const normalized = text ? normalizeKeyword(text) : '';
|
|
87
|
+
const words = normalized.length > 0 ? normalized.split(/\s+/).filter(Boolean).length : 0;
|
|
88
|
+
if (text
|
|
89
|
+
&& text.length >= options.minKeywordLength
|
|
90
|
+
&& words >= 2
|
|
91
|
+
&& words <= 4) {
|
|
92
|
+
candidates.push({ phrase: text, weight: 2.8 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (type === 'product') {
|
|
98
|
+
const name = item['name']?.trim();
|
|
99
|
+
if (name && name.length >= options.minKeywordLength)
|
|
100
|
+
candidates.push({ phrase: name, weight: 3.0 });
|
|
101
|
+
const category = item['category']?.trim();
|
|
102
|
+
if (category && category.length >= options.minKeywordLength)
|
|
103
|
+
candidates.push({ phrase: category, weight: 2.5 });
|
|
104
|
+
const brand = item['brand']?.['name']?.trim();
|
|
105
|
+
if (brand && brand.length >= options.minKeywordLength)
|
|
106
|
+
candidates.push({ phrase: brand, weight: 2.0 });
|
|
107
|
+
}
|
|
108
|
+
if (type === 'breadcrumblist') {
|
|
109
|
+
const elements = item['itemListElement'];
|
|
110
|
+
if (Array.isArray(elements)) {
|
|
111
|
+
const names = elements
|
|
112
|
+
.map((el) => el['name']?.trim())
|
|
113
|
+
.filter((n) => typeof n === 'string' && n.length >= options.minKeywordLength);
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
candidates.push({ phrase: name, weight: 2.0 });
|
|
116
|
+
}
|
|
117
|
+
for (let i = 0; i < names.length - 1; i++) {
|
|
118
|
+
const composed = `${names[i]} ${names[i + 1]}`;
|
|
119
|
+
if (tokenizeForLongTail(composed).length <= 5) {
|
|
120
|
+
candidates.push({ phrase: composed, weight: 2.2 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (type === 'article' || type === 'blogposting' || type === 'newsarticle') {
|
|
126
|
+
const headline = item['headline']?.trim();
|
|
127
|
+
if (headline && headline.length >= options.minKeywordLength)
|
|
128
|
+
candidates.push({ phrase: headline, weight: 2.8 });
|
|
129
|
+
const keywords = item['keywords'];
|
|
130
|
+
const kwList = typeof keywords === 'string'
|
|
131
|
+
? keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean)
|
|
132
|
+
: Array.isArray(keywords)
|
|
133
|
+
? keywords.filter((k) => typeof k === 'string')
|
|
134
|
+
: [];
|
|
135
|
+
for (const kw of kwList) {
|
|
136
|
+
if (kw.length >= options.minKeywordLength)
|
|
137
|
+
candidates.push({ phrase: kw, weight: 2.5 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
const seeds = [];
|
|
143
|
+
for (const { phrase, weight } of candidates.sort((a, b) => b.weight - a.weight)) {
|
|
144
|
+
const normalized = normalizeKeyword(phrase);
|
|
145
|
+
if (!normalized || normalized.length < options.minKeywordLength)
|
|
146
|
+
continue;
|
|
147
|
+
const words = normalized.split(/\s+/).filter(Boolean).length;
|
|
148
|
+
if (words > 4 || words < 2)
|
|
149
|
+
continue;
|
|
150
|
+
if (seen.has(normalized))
|
|
151
|
+
continue;
|
|
152
|
+
if (seeds.length >= options.maxPhrases)
|
|
153
|
+
break;
|
|
154
|
+
seen.add(normalized);
|
|
155
|
+
seeds.push({
|
|
156
|
+
keyword: phrase,
|
|
157
|
+
normalizedKeyword: normalized,
|
|
158
|
+
source: 'discovered',
|
|
159
|
+
sourcePage: options.sourcePage ? `${options.sourcePage}#schema` : undefined,
|
|
160
|
+
weight: Math.max(1, Math.round(weight * 100)),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return seeds;
|
|
164
|
+
}
|
|
165
|
+
export function extractHeadingPathCompositionSeeds(report, options) {
|
|
166
|
+
const sections = report.contentSections;
|
|
167
|
+
if (!sections?.length)
|
|
168
|
+
return [];
|
|
169
|
+
const phraseCounts = new Map();
|
|
170
|
+
const upsert = (phrase, weight) => {
|
|
171
|
+
const tokens = tokenizeForLongTail(phrase);
|
|
172
|
+
if (tokens.length < 2 || tokens.length > 4)
|
|
173
|
+
return;
|
|
174
|
+
if (isLowValueLongTail(tokens))
|
|
175
|
+
return;
|
|
176
|
+
const normalized = normalizeKeyword(tokens.join(' '));
|
|
177
|
+
if (!normalized || normalized.length < options.minKeywordLength)
|
|
178
|
+
return;
|
|
179
|
+
const current = phraseCounts.get(normalized);
|
|
180
|
+
if (!current) {
|
|
181
|
+
phraseCounts.set(normalized, { phrase: tokens.join(' '), normalized, weight });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
current.weight += weight * 0.5;
|
|
185
|
+
};
|
|
186
|
+
const levelWeight = (level) => level <= 1 ? 3.0 : level === 2 ? 2.2 : level === 3 ? 1.8 : 1.2;
|
|
187
|
+
for (const section of sections) {
|
|
188
|
+
const path = section.headingPath;
|
|
189
|
+
if (!path?.length)
|
|
190
|
+
continue;
|
|
191
|
+
if (section.linkDensity > 0.6)
|
|
192
|
+
continue;
|
|
193
|
+
const w = section.weight * levelWeight(section.headingLevel);
|
|
194
|
+
const pathAnchors = path.map(heading => tokenizeForLongTail(heading).filter(t => options.topKeywordSet.has(t)).slice(0, 2));
|
|
195
|
+
const activeAnchors = pathAnchors[pathAnchors.length - 1] ?? [];
|
|
196
|
+
if (pathAnchors.length >= 2) {
|
|
197
|
+
const parentAnchors = pathAnchors[pathAnchors.length - 2];
|
|
198
|
+
for (const pa of parentAnchors) {
|
|
199
|
+
for (const ca of activeAnchors) {
|
|
200
|
+
if (pa !== ca)
|
|
201
|
+
upsert(`${pa} ${ca}`, w);
|
|
202
|
+
}
|
|
203
|
+
if (activeAnchors.length >= 2) {
|
|
204
|
+
upsert(`${pa} ${activeAnchors[0]} ${activeAnchors[1]}`, w * 0.9);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const allAnchors = pathAnchors.flat().slice(0, 4);
|
|
209
|
+
if (allAnchors.length >= 2 && new Set(allAnchors).size === allAnchors.length) {
|
|
210
|
+
upsert(allAnchors.join(' '), w * 0.85);
|
|
211
|
+
}
|
|
212
|
+
if (section.source !== 'heading' && activeAnchors.length > 0) {
|
|
213
|
+
const bodyAnchors = tokenizeForLongTail(section.text)
|
|
214
|
+
.filter(t => options.topKeywordSet.has(t) && !activeAnchors.includes(t))
|
|
215
|
+
.slice(0, 5);
|
|
216
|
+
for (const headAnchor of activeAnchors) {
|
|
217
|
+
for (const bodyAnchor of bodyAnchors) {
|
|
218
|
+
upsert(`${headAnchor} ${bodyAnchor}`, section.weight * 1.3);
|
|
219
|
+
const parentAnchor = pathAnchors.length >= 2 ? pathAnchors[pathAnchors.length - 2][0] : undefined;
|
|
220
|
+
if (parentAnchor && parentAnchor !== headAnchor && parentAnchor !== bodyAnchor) {
|
|
221
|
+
upsert(`${parentAnchor} ${headAnchor} ${bodyAnchor}`, section.weight * 1.1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [...phraseCounts.values()]
|
|
228
|
+
.sort((a, b) => b.weight - a.weight || a.normalized.localeCompare(b.normalized))
|
|
229
|
+
.slice(0, options.maxPhrases)
|
|
230
|
+
.map((entry, index) => ({
|
|
231
|
+
keyword: entry.phrase,
|
|
232
|
+
normalizedKeyword: entry.normalized,
|
|
233
|
+
source: 'discovered',
|
|
234
|
+
sourcePage: options.sourcePage ? `${options.sourcePage}#path-${index}` : undefined,
|
|
235
|
+
weight: Math.max(1, Math.round(entry.weight * 20)),
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
export function sortSeedsByWeight(a, b) {
|
|
239
|
+
if (a.weight === b.weight) {
|
|
240
|
+
return a.normalizedKeyword.localeCompare(b.normalizedKeyword);
|
|
241
|
+
}
|
|
242
|
+
return b.weight - a.weight;
|
|
243
|
+
}
|
|
244
|
+
export function mergeKeywordSeeds(discovered, preset, minKeywordLength = DEFAULT_SERP_MIN_KEYWORD_LENGTH, minWeight = 1) {
|
|
245
|
+
const merged = new Map();
|
|
246
|
+
const upsert = (seed) => {
|
|
247
|
+
if (!seed.normalizedKeyword || seed.normalizedKeyword.length < minKeywordLength)
|
|
248
|
+
return;
|
|
249
|
+
if (seed.weight < minWeight)
|
|
250
|
+
return;
|
|
251
|
+
const existing = merged.get(seed.normalizedKeyword);
|
|
252
|
+
if (!existing) {
|
|
253
|
+
merged.set(seed.normalizedKeyword, seed);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (existing.source === 'preset' && seed.source === 'discovered') {
|
|
257
|
+
merged.set(seed.normalizedKeyword, seed);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (seed.source === existing.source && seed.weight > existing.weight) {
|
|
261
|
+
merged.set(seed.normalizedKeyword, seed);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
for (const seed of discovered)
|
|
265
|
+
upsert(seed);
|
|
266
|
+
for (const seed of preset)
|
|
267
|
+
upsert(seed);
|
|
268
|
+
return [...merged.values()].sort(sortSeedsByWeight);
|
|
269
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SeoReport } from './types.js';
|
|
2
|
+
import type { KeywordCampaignExtractionOptions, KeywordCampaignSeed } from './keyword-campaign-shared.js';
|
|
3
|
+
export declare function extractKeywordCampaignSeedsFromReport(report: SeoReport, options?: KeywordCampaignExtractionOptions & {
|
|
4
|
+
sourcePage?: string;
|
|
5
|
+
}): KeywordCampaignSeed[];
|
|
6
|
+
export declare const SERP_BLOCK_CONFIDENCE = 0.6;
|
|
7
|
+
export declare const SERP_CAPTCHA_CONFIDENCE = 0.6;
|
|
8
|
+
export declare const DEFAULT_SERP_SEARCH_DELAY_MS = 450;
|
|
9
|
+
export declare const DEFAULT_SERP_SEARCH_JITTER_MS = 250;
|
|
10
|
+
export declare const DEFAULT_SERP_SEARCH_CAPTCHA_COOLDOWN_MS = 1200;
|
|
11
|
+
export declare const DEFAULT_SERP_SEARCH_RETRY_COUNT = 1;
|
|
12
|
+
export declare const DEFAULT_SERP_SEARCH_RETRY_DELAY_MS = 900;
|
|
13
|
+
export declare const DEFAULT_SERP_MAX_CONSECUTIVE_BLOCKS = 3;
|
|
14
|
+
export declare function isLikelyBlocked(block: {
|
|
15
|
+
blocked?: boolean;
|
|
16
|
+
confidence?: number;
|
|
17
|
+
} | undefined): boolean;
|
|
18
|
+
export declare function isLikelyCaptcha(captcha: {
|
|
19
|
+
detected?: boolean;
|
|
20
|
+
confidence?: number;
|
|
21
|
+
} | undefined): boolean;
|
|
22
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
23
|
+
export declare function buildSerpBlockReason(block: {
|
|
24
|
+
reason?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
} | undefined, captcha: {
|
|
27
|
+
provider?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
} | undefined): string | undefined;
|