seo-intel 1.1.7 → 1.1.9
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/.env.example +3 -1
- package/CHANGELOG.md +54 -0
- package/LICENSE +4 -5
- package/README.md +3 -3
- package/cli.js +136 -16
- package/crawler/index.js +11 -2
- package/crawler/subdomain-discovery.js +1 -1
- package/lib/gate.js +5 -5
- package/lib/license.js +150 -69
- package/lib/updater.js +19 -18
- package/package.json +3 -2
- package/seo-audit.js +2 -2
- package/server.js +3 -5
- package/setup/wizard.html +352 -32
package/.env.example
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
# Run `node cli.js setup` to configure interactively
|
|
3
3
|
|
|
4
4
|
# ── License (Pro features) ───────────────────────────────────────────────
|
|
5
|
-
# Get your key at https://
|
|
5
|
+
# Get your key at https://ukkometa.fi/en/seo-intel/
|
|
6
6
|
# SEO_INTEL_LICENSE=SI-xxxx-xxxx-xxxx-xxxx
|
|
7
7
|
|
|
8
8
|
# ── Analysis Model (cloud, pick one) ──────────────────────────────────────
|
|
9
9
|
# Gemini: Best value — 1M context, cheapest (~$0.01-0.05/analysis)
|
|
10
10
|
GEMINI_API_KEY=
|
|
11
|
+
# GEMINI_TIMEOUT_MS=120000
|
|
12
|
+
# OPENCLAW_ANALYSIS_MODEL=default
|
|
11
13
|
|
|
12
14
|
# Claude: Best quality — nuanced strategic reasoning (~$0.10-0.30/analysis)
|
|
13
15
|
# ANTHROPIC_API_KEY=
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.9 (2026-03-27)
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- Fix shell injection risk in Gemini CLI integration (execSync → spawnSync + stdin)
|
|
7
|
+
- Fix SSRF vector in llms.txt URL processing (hostname validation)
|
|
8
|
+
- Fix SSRF: llms.txt URLs now respect robots.txt before enqueue
|
|
9
|
+
- Set license cache file permissions to 0600 (owner-only)
|
|
10
|
+
- SQL injection audit — all queries verified to use parameterised statements
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
- Crawler no longer upgrades http://localhost to https (fixes local/mock testing)
|
|
14
|
+
- Updater cache moved to ~/.seo-intel/ (fixes permission errors on Linux global install)
|
|
15
|
+
- Clear actionable error when Gemini times out and OpenClaw gateway is down
|
|
16
|
+
- Project name passed to error context for timeout messages
|
|
17
|
+
|
|
18
|
+
### Improvements
|
|
19
|
+
- Defensive logging when license variant name is unrecognised
|
|
20
|
+
- Setup wizard: Cloud Analysis column (gemini-3.1-pro, claude-sonnet-4-6, claude-opus-4-6, gpt-5.4, deepseek-r1) with API key input
|
|
21
|
+
- Setup wizard: Agentic Setup picker — OpenClaw, Claude Code, Codex CLI, Perplexity with tailored copy-paste prompts
|
|
22
|
+
- Setup wizard: Step 2 expanded to 1100px, OpenClaw as floating sidebar
|
|
23
|
+
|
|
24
|
+
## 1.1.7 (2026-03-26)
|
|
25
|
+
|
|
26
|
+
### New
|
|
27
|
+
- **Programmatic Template Intelligence** (`seo-intel templates <project>`) — detect URL pattern groups (e.g. `/token/*`, `/blog/*`), stealth-crawl samples, overlay GSC data, and score each group with keep/noindex/improve verdicts
|
|
28
|
+
- **Stale domain auto-pruning** — domains removed from config are automatically cleaned from DB on next crawl
|
|
29
|
+
- **Manual prune** — `seo-intel competitors <project> --prune` to clean stale DB entries on demand
|
|
30
|
+
- **Full body text storage** — crawler stores full page content in DB for richer offline extraction
|
|
31
|
+
|
|
32
|
+
### Improvements
|
|
33
|
+
- **Background crawl/extract** — long-running jobs survive browser tab close
|
|
34
|
+
- **Dashboard terminal** — stealth flag visible, stop button works properly, status bar syncs
|
|
35
|
+
- **Templates button** added to dashboard terminal panel
|
|
36
|
+
- **Dashboard refresh** — crawl and analyze always regenerate full multi-project dashboard
|
|
37
|
+
- **Config remove = DB remove** — `--remove` and `--remove-owned` auto-prune matching DB data
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
- SSE disconnect no longer kills crawl/extract processes
|
|
41
|
+
- Terminal command display shows `--stealth` flag when enabled
|
|
42
|
+
|
|
43
|
+
## 1.1.6 (2026-03-24)
|
|
44
|
+
|
|
45
|
+
- Stop button for crawl/extract jobs in dashboard
|
|
46
|
+
- Stealth toggle sync between status bar and terminal
|
|
47
|
+
- Extraction status bar layout improvements (CSS grid)
|
|
48
|
+
- EADDRINUSE recovery — server opens existing dashboard instead of crashing
|
|
49
|
+
|
|
50
|
+
## 1.1.5 (2026-03-21)
|
|
51
|
+
|
|
52
|
+
- Update checker, job stop API, background analyze
|
|
53
|
+
- LAN Ollama host support with fallback
|
|
54
|
+
- `html` CLI command, wizard UX improvements
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
SEO Intel — Dual License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024-2026
|
|
3
|
+
Copyright (c) 2024-2026 Ukkometa (ukkometa.fi)
|
|
4
4
|
|
|
5
5
|
This project uses a dual license structure:
|
|
6
6
|
|
|
@@ -65,11 +65,10 @@ You MAY:
|
|
|
65
65
|
- Share generated reports and dashboards (outputs are yours)
|
|
66
66
|
|
|
67
67
|
License:
|
|
68
|
-
Solo — €19.99/month or €199/year — full AI analysis, all commands
|
|
69
|
-
Also available at $9.99/month via froggo.pro marketplace
|
|
68
|
+
Solo — €19.99/month or €199.99/year — full AI analysis, all commands
|
|
70
69
|
|
|
71
|
-
Purchase at: https://ukkometa.fi
|
|
70
|
+
Purchase at: https://ukkometa.fi/en/seo-intel/
|
|
72
71
|
|
|
73
72
|
================================================================================
|
|
74
73
|
|
|
75
|
-
For questions:
|
|
74
|
+
For questions: ukko@ukkometa.fi
|
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ seo-intel suggest-usecases myproject --scope docs # infer what pages/docs s
|
|
|
75
75
|
| `schemas <project>` | Schema.org coverage analysis |
|
|
76
76
|
| `update` | Check for updates |
|
|
77
77
|
|
|
78
|
-
### Solo (€19.99/mo · [ukkometa.fi/seo-intel](https://ukkometa.fi/seo-intel))
|
|
78
|
+
### Solo (€19.99/mo · [ukkometa.fi/seo-intel](https://ukkometa.fi/en/seo-intel/))
|
|
79
79
|
|
|
80
80
|
| Command | Description |
|
|
81
81
|
|---------|-------------|
|
|
@@ -194,7 +194,7 @@ Upload your GSC data for ranking insights:
|
|
|
194
194
|
- 1 project, 500 pages/domain
|
|
195
195
|
- Crawl, extract, setup, basic reports
|
|
196
196
|
|
|
197
|
-
###
|
|
197
|
+
### Solo (€19.99/mo · €199.99/yr)
|
|
198
198
|
- Unlimited projects and pages
|
|
199
199
|
- All analysis commands, GSC insights, scheduling
|
|
200
200
|
|
|
@@ -203,7 +203,7 @@ Upload your GSC data for ranking insights:
|
|
|
203
203
|
echo "SEO_INTEL_LICENSE=SI-xxxx-xxxx-xxxx-xxxx" >> .env
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
Get a key at [
|
|
206
|
+
Get a key at [ukkometa.fi/seo-intel](https://ukkometa.fi/en/seo-intel/)
|
|
207
207
|
|
|
208
208
|
## Updates
|
|
209
209
|
|
package/cli.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ── Node.js version guard ───────────────────────────────────────────────
|
|
4
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
5
|
+
if (major < 22 || (major === 22 && minor < 5)) {
|
|
6
|
+
console.error(
|
|
7
|
+
`\n SEO Intel requires Node.js 22.5 or later.\n` +
|
|
8
|
+
` You have Node.js ${process.versions.node}.\n\n` +
|
|
9
|
+
` Install the latest LTS: https://nodejs.org\n`
|
|
10
|
+
);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
2
14
|
import 'dotenv/config';
|
|
3
15
|
import { program } from 'commander';
|
|
4
16
|
import { spawnSync } from 'child_process';
|
|
@@ -47,6 +59,13 @@ checkForUpdates();
|
|
|
47
59
|
try { mkdirSync(join(__dirname, 'reports'), { recursive: true }); } catch { /* ok */ }
|
|
48
60
|
try { mkdirSync(join(__dirname, 'config'), { recursive: true }); } catch { /* ok */ }
|
|
49
61
|
|
|
62
|
+
function defaultSiteUrl(domain) {
|
|
63
|
+
const host = String(domain || '').trim();
|
|
64
|
+
const hostname = host.split(':')[0].replace(/^\[|\]$/g, '');
|
|
65
|
+
const protocol = hostname === 'localhost' || hostname === '127.0.0.1' ? 'http' : 'https';
|
|
66
|
+
return `${protocol}://${host}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
// ── AI AVAILABILITY PREFLIGHT ────────────────────────────────────────────
|
|
51
70
|
/**
|
|
52
71
|
* Check if any AI extraction backend is reachable.
|
|
@@ -435,7 +454,7 @@ program
|
|
|
435
454
|
else if (config.competitors?.includes(site)) site.role = 'competitor';
|
|
436
455
|
else site.role = 'owned';
|
|
437
456
|
}
|
|
438
|
-
if (!site.url && site.domain) site.url =
|
|
457
|
+
if (!site.url && site.domain) site.url = defaultSiteUrl(site.domain);
|
|
439
458
|
}
|
|
440
459
|
|
|
441
460
|
const sites = opts.domain
|
|
@@ -674,7 +693,8 @@ program
|
|
|
674
693
|
console.log(chalk.gray(`Prompt saved: ${promptPath}`));
|
|
675
694
|
|
|
676
695
|
// Call Gemini via gemini CLI (reuse existing auth)
|
|
677
|
-
|
|
696
|
+
process.env._SEO_INTEL_PROJECT = project;
|
|
697
|
+
const result = await callAnalysisModel(prompt, opts.model);
|
|
678
698
|
|
|
679
699
|
if (!result) {
|
|
680
700
|
console.error(chalk.red('No response from model.'));
|
|
@@ -991,17 +1011,117 @@ function loadConfig(project) {
|
|
|
991
1011
|
}
|
|
992
1012
|
|
|
993
1013
|
async function callGemini(prompt) {
|
|
994
|
-
|
|
995
|
-
|
|
1014
|
+
return callAnalysisModel(prompt, 'gemini');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function getOpenClawToken() {
|
|
1018
|
+
const envToken = process.env.OPENCLAW_TOKEN?.trim();
|
|
1019
|
+
if (envToken) return envToken;
|
|
1020
|
+
|
|
996
1021
|
try {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
).
|
|
1001
|
-
|
|
1022
|
+
const configPath = join(process.env.HOME || process.env.USERPROFILE || '', '.openclaw', 'openclaw.json');
|
|
1023
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
1024
|
+
const matches = [...raw.matchAll(/"token":\s*"([a-f0-9]{40,})"/g)];
|
|
1025
|
+
if (matches.length > 0) return matches[matches.length - 1][1];
|
|
1026
|
+
} catch {}
|
|
1027
|
+
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function callOpenClaw(prompt, model = 'default') {
|
|
1032
|
+
const token = getOpenClawToken();
|
|
1033
|
+
if (!token) throw new Error('OpenClaw token not found');
|
|
1034
|
+
|
|
1035
|
+
const timeoutMs = parseInt(process.env.OPENCLAW_TIMEOUT_MS || process.env.GEMINI_TIMEOUT_MS || '120000', 10);
|
|
1036
|
+
const controller = new AbortController();
|
|
1037
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1038
|
+
|
|
1039
|
+
try {
|
|
1040
|
+
const res = await fetch('http://127.0.0.1:18789/v1/chat/completions', {
|
|
1041
|
+
method: 'POST',
|
|
1042
|
+
signal: controller.signal,
|
|
1043
|
+
headers: {
|
|
1044
|
+
'Authorization': `Bearer ${token}`,
|
|
1045
|
+
'Content-Type': 'application/json',
|
|
1046
|
+
},
|
|
1047
|
+
body: JSON.stringify({
|
|
1048
|
+
model: model === 'openclaw' ? 'default' : model,
|
|
1049
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1050
|
+
temperature: 0.2,
|
|
1051
|
+
max_tokens: 4000,
|
|
1052
|
+
}),
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
if (!res.ok) throw new Error(`OpenClaw API error: ${res.status} ${await res.text()}`);
|
|
1056
|
+
|
|
1057
|
+
const data = await res.json();
|
|
1058
|
+
return data.choices?.[0]?.message?.content || null;
|
|
1059
|
+
} finally {
|
|
1060
|
+
clearTimeout(timeout);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function callAnalysisModel(prompt, model = 'gemini') {
|
|
1065
|
+
const requestedModel = String(model || 'gemini').trim();
|
|
1066
|
+
const normalizedModel = requestedModel.toLowerCase();
|
|
1067
|
+
|
|
1068
|
+
if (normalizedModel !== 'gemini') {
|
|
1069
|
+
try {
|
|
1070
|
+
return await callOpenClaw(prompt, requestedModel);
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
console.error('[openclaw]', err.message);
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const timeoutMs = parseInt(process.env.GEMINI_TIMEOUT_MS || '120000', 10);
|
|
1078
|
+
try {
|
|
1079
|
+
const result = spawnSync('gemini', ['-p', '-'], {
|
|
1080
|
+
input: prompt,
|
|
1081
|
+
encoding: 'utf8',
|
|
1082
|
+
timeout: timeoutMs,
|
|
1083
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
if (result.error) throw result.error;
|
|
1087
|
+
if (result.status !== 0) {
|
|
1088
|
+
throw new Error(result.stderr?.trim() || `gemini exited with status ${result.status}`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return result.stdout;
|
|
1002
1092
|
} catch (err) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1093
|
+
const fallbackModel = process.env.OPENCLAW_ANALYSIS_MODEL || 'default';
|
|
1094
|
+
try {
|
|
1095
|
+
console.warn(`[gemini] ${err.message}`);
|
|
1096
|
+
console.log(chalk.yellow(`Gemini CLI unavailable, retrying via OpenClaw (${fallbackModel})...\n`));
|
|
1097
|
+
return await callOpenClaw(prompt, fallbackModel);
|
|
1098
|
+
} catch (fallbackErr) {
|
|
1099
|
+
// Produce clear, actionable error messages
|
|
1100
|
+
const geminiMsg = err.message || '';
|
|
1101
|
+
const ocMsg = fallbackErr.message || '';
|
|
1102
|
+
|
|
1103
|
+
const isTimeout = geminiMsg.includes('ETIMEDOUT') || geminiMsg.includes('timeout') || err.name === 'AbortError';
|
|
1104
|
+
const isGatewayDown = ocMsg.includes('ECONNREFUSED') || ocMsg.includes('token not found') || ocMsg.includes('gateway');
|
|
1105
|
+
|
|
1106
|
+
console.error(chalk.red('\n ✗ Analysis failed — no model available\n'));
|
|
1107
|
+
|
|
1108
|
+
if (isTimeout) {
|
|
1109
|
+
console.error(chalk.yellow(' Gemini timed out.') + chalk.dim(' Try: GEMINI_TIMEOUT_MS=180000 seo-intel analyze ' + (process.env._SEO_INTEL_PROJECT || '<project>')));
|
|
1110
|
+
} else {
|
|
1111
|
+
console.error(chalk.dim(` Gemini: ${geminiMsg}`));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (isGatewayDown) {
|
|
1115
|
+
console.error(chalk.yellow(' OpenClaw gateway is not running.'));
|
|
1116
|
+
console.error(chalk.dim(' Start it: ') + chalk.cyan('openclaw gateway'));
|
|
1117
|
+
console.error(chalk.dim(' Or set key: ') + chalk.cyan('echo "GEMINI_API_KEY=your-key" >> .env'));
|
|
1118
|
+
} else {
|
|
1119
|
+
console.error(chalk.dim(` OpenClaw: ${ocMsg}`));
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
console.error(chalk.dim('\n Docs: https://ukkometa.fi/en/seo-intel/setup/\n'));
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1005
1125
|
}
|
|
1006
1126
|
}
|
|
1007
1127
|
|
|
@@ -1268,8 +1388,8 @@ program
|
|
|
1268
1388
|
if (info.npmVersion) {
|
|
1269
1389
|
console.log(chalk.gray(' npm registry: ') + chalk.white(info.npmVersion));
|
|
1270
1390
|
}
|
|
1271
|
-
if (info.
|
|
1272
|
-
console.log(chalk.gray('
|
|
1391
|
+
if (info.ukkometaVersion) {
|
|
1392
|
+
console.log(chalk.gray(' ukkometa.fi: ') + chalk.white(info.ukkometaVersion));
|
|
1273
1393
|
}
|
|
1274
1394
|
|
|
1275
1395
|
if (!info.hasUpdate) {
|
|
@@ -1428,7 +1548,7 @@ program
|
|
|
1428
1548
|
// ── Add competitor
|
|
1429
1549
|
if (opts.add) {
|
|
1430
1550
|
const domain = domainFromUrl(opts.add);
|
|
1431
|
-
const url = opts.add.startsWith('http') ? opts.add :
|
|
1551
|
+
const url = opts.add.startsWith('http') ? opts.add : defaultSiteUrl(opts.add);
|
|
1432
1552
|
if (config.competitors.some(c => c.domain === domain)) {
|
|
1433
1553
|
console.log(chalk.yellow(` ⚠ ${domain} is already a competitor`));
|
|
1434
1554
|
} else {
|
|
@@ -1455,7 +1575,7 @@ program
|
|
|
1455
1575
|
if (opts.addOwned) {
|
|
1456
1576
|
if (!config.owned) config.owned = [];
|
|
1457
1577
|
const domain = domainFromUrl(opts.addOwned);
|
|
1458
|
-
const url = opts.addOwned.startsWith('http') ? opts.addOwned :
|
|
1578
|
+
const url = opts.addOwned.startsWith('http') ? opts.addOwned : defaultSiteUrl(opts.addOwned);
|
|
1459
1579
|
if (config.owned.some(o => o.domain === domain)) {
|
|
1460
1580
|
console.log(chalk.yellow(` ⚠ ${domain} is already an owned domain`));
|
|
1461
1581
|
} else {
|
|
@@ -1482,7 +1602,7 @@ program
|
|
|
1482
1602
|
// ── Change target
|
|
1483
1603
|
if (opts.setTarget) {
|
|
1484
1604
|
const domain = domainFromUrl(opts.setTarget);
|
|
1485
|
-
const url = opts.setTarget.startsWith('http') ? opts.setTarget :
|
|
1605
|
+
const url = opts.setTarget.startsWith('http') ? opts.setTarget : defaultSiteUrl(opts.setTarget);
|
|
1486
1606
|
config.target = { url, domain, role: 'target' };
|
|
1487
1607
|
config.context.url = url;
|
|
1488
1608
|
console.log(chalk.green(` ✓ Target changed to: ${domain}`));
|
package/crawler/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const MAX_PAGES = parseInt(process.env.CRAWL_MAX_PAGES || '50');
|
|
|
11
11
|
const MAX_DEPTH = parseInt(process.env.CRAWL_MAX_DEPTH || '3');
|
|
12
12
|
const TIMEOUT = parseInt(process.env.CRAWL_TIMEOUT_MS || '12000');
|
|
13
13
|
const PAGE_BUDGET = parseInt(process.env.PAGE_BUDGET_MS || '25000'); // hard per-page wall-clock limit
|
|
14
|
+
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1']);
|
|
14
15
|
|
|
15
16
|
// ── Content quality gate ────────────────────────────────────────────────
|
|
16
17
|
const SHELL_PATTERNS = /id=["'](root|app|__next|__nuxt)["']|<noscript[^>]*>.*enable javascript/i;
|
|
@@ -186,11 +187,14 @@ export async function* crawlDomain(startUrl, opts = {}) {
|
|
|
186
187
|
// When hostname contains "docs.", spoof Googlebot UA to reduce WAF friction.
|
|
187
188
|
const isDocsHostname = base.hostname.toLowerCase().includes('docs.');
|
|
188
189
|
const GOOGLEBOT_UA = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
|
|
189
|
-
const defaultUA = 'Mozilla/5.0 (compatible; SEOIntelBot/1.0; +https://
|
|
190
|
+
const defaultUA = 'Mozilla/5.0 (compatible; SEOIntelBot/1.0; +https://ukkometa.fi/en/seo-intel/bot)';
|
|
190
191
|
const effectiveUA = isDocsHostname ? GOOGLEBOT_UA : defaultUA;
|
|
191
192
|
|
|
192
193
|
async function tryLoadLlmsTxt() {
|
|
193
|
-
const
|
|
194
|
+
const llmsOrigin = base.protocol === 'http:' && !LOOPBACK_HOSTNAMES.has(base.hostname)
|
|
195
|
+
? `https://${base.host}`
|
|
196
|
+
: base.origin;
|
|
197
|
+
const llmsUrl = `${llmsOrigin}/llms.txt`;
|
|
194
198
|
try {
|
|
195
199
|
const controller = new AbortController();
|
|
196
200
|
const t = setTimeout(() => controller.abort(), Math.min(TIMEOUT, 8000));
|
|
@@ -224,6 +228,11 @@ export async function* crawlDomain(startUrl, opts = {}) {
|
|
|
224
228
|
const unique = [...new Set(urls)];
|
|
225
229
|
let added = 0;
|
|
226
230
|
for (const u of unique) {
|
|
231
|
+
try {
|
|
232
|
+
if (new URL(u).hostname !== base.hostname) continue;
|
|
233
|
+
} catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
227
236
|
if (!queue.some(q => q.url === u)) {
|
|
228
237
|
queue.push({ url: u, depth: 1 });
|
|
229
238
|
added++;
|
|
@@ -181,7 +181,7 @@ async function checkHttp(hostname) {
|
|
|
181
181
|
signal: controller.signal,
|
|
182
182
|
redirect: 'follow',
|
|
183
183
|
headers: {
|
|
184
|
-
'User-Agent': 'Mozilla/5.0 (compatible; SEOIntelBot/1.0; +https://
|
|
184
|
+
'User-Agent': 'Mozilla/5.0 (compatible; SEOIntelBot/1.0; +https://ukkometa.fi/en/seo-intel/bot)',
|
|
185
185
|
},
|
|
186
186
|
});
|
|
187
187
|
|
package/lib/gate.js
CHANGED
|
@@ -31,7 +31,7 @@ function printUpgradeMessage(feature) {
|
|
|
31
31
|
console.log(`${GOLD}${BOLD} ⭐ Paid Feature: ${feature}${RESET}`);
|
|
32
32
|
console.log(`${DIM} This feature requires SEO Intel Solo (€19.99/mo).${RESET}`);
|
|
33
33
|
console.log('');
|
|
34
|
-
console.log(`${DIM} Get your license at ${CYAN}https://
|
|
34
|
+
console.log(`${DIM} Get your license at ${CYAN}https://ukkometa.fi/en/seo-intel/${RESET}`);
|
|
35
35
|
console.log(`${DIM} Then add your key: ${CYAN}SEO_INTEL_LICENSE=SI-xxxx-xxxx-xxxx-xxxx${RESET} ${DIM}in .env${RESET}`);
|
|
36
36
|
console.log('');
|
|
37
37
|
}
|
|
@@ -104,7 +104,7 @@ export function getPremiumPlaceholder(section) {
|
|
|
104
104
|
<p style="font-size: 0.72rem; color: var(--text-muted); margin-bottom: 12px;">
|
|
105
105
|
This section requires SEO Intel Solo (€19.99/mo)
|
|
106
106
|
</p>
|
|
107
|
-
<a href="https://
|
|
107
|
+
<a href="https://ukkometa.fi/en/seo-intel/" target="_blank"
|
|
108
108
|
style="color: var(--accent-gold); font-size: 0.72rem; text-decoration: underline;">
|
|
109
109
|
Upgrade to unlock →
|
|
110
110
|
</a>
|
|
@@ -160,7 +160,7 @@ export function capPages(requestedPages) {
|
|
|
160
160
|
export function printLicenseStatus() {
|
|
161
161
|
const license = loadLicense();
|
|
162
162
|
|
|
163
|
-
const sourceLabel = license.source === '
|
|
163
|
+
const sourceLabel = license.source === 'lemon-squeezy' ? ' (LS)' : '';
|
|
164
164
|
|
|
165
165
|
if (license.tier === 'agency') {
|
|
166
166
|
console.log(`${GOLD}${BOLD} ⭐ SEO Intel Agency${RESET}`);
|
|
@@ -181,7 +181,7 @@ export function printLicenseStatus() {
|
|
|
181
181
|
if (license.needsActivation) {
|
|
182
182
|
console.log(`\x1b[33m ⚠ License key found but not yet validated — run any command to activate${RESET}`);
|
|
183
183
|
}
|
|
184
|
-
console.log(`${DIM} Upgrade: ${CYAN}https://
|
|
184
|
+
console.log(`${DIM} Upgrade: ${CYAN}https://ukkometa.fi/en/seo-intel/${RESET} ${DIM}— Solo €19.99/mo · €199.99/yr${RESET}`);
|
|
185
185
|
}
|
|
186
186
|
console.log('');
|
|
187
187
|
}
|
|
@@ -200,6 +200,6 @@ export function getLicenseInfo() {
|
|
|
200
200
|
maxProjects: Number.isFinite(license.maxProjects) ? license.maxProjects : null,
|
|
201
201
|
maxPages: Number.isFinite(license.maxPagesPerDomain) ? license.maxPagesPerDomain : null,
|
|
202
202
|
features: license.features === 'all' ? 'all' : [...license.features],
|
|
203
|
-
upgradeUrl: 'https://
|
|
203
|
+
upgradeUrl: 'https://ukkometa.fi/en/seo-intel/',
|
|
204
204
|
};
|
|
205
205
|
}
|