@houtini/seo-audit-console 0.8.0 → 0.9.1
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 +308 -306
- package/dist/audit/checks.d.ts +0 -7
- package/dist/audit/checks.d.ts.map +1 -1
- package/dist/audit/checks.js +130 -242
- package/dist/audit/checks.js.map +1 -1
- package/dist/audit/drift.d.ts +0 -12
- package/dist/audit/drift.d.ts.map +1 -1
- package/dist/audit/drift.js +0 -15
- package/dist/audit/drift.js.map +1 -1
- package/dist/audit/engine.d.ts +0 -3
- package/dist/audit/engine.d.ts.map +1 -1
- package/dist/audit/engine.js +7 -42
- package/dist/audit/engine.js.map +1 -1
- package/dist/audit/keywordList.d.ts +0 -14
- package/dist/audit/keywordList.d.ts.map +1 -1
- package/dist/audit/keywordList.js +0 -5
- package/dist/audit/keywordList.js.map +1 -1
- package/dist/audit/opportunities.js +3 -26
- package/dist/audit/opportunities.js.map +1 -1
- package/dist/audit/recon.d.ts +0 -31
- package/dist/audit/recon.d.ts.map +1 -1
- package/dist/audit/recon.js +0 -27
- package/dist/audit/recon.js.map +1 -1
- package/dist/audit/report.d.ts +0 -4
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/report.js +0 -7
- package/dist/audit/report.js.map +1 -1
- package/dist/audit/schema-validate.d.ts +0 -26
- package/dist/audit/schema-validate.d.ts.map +1 -1
- package/dist/audit/schema-validate.js +8 -64
- package/dist/audit/schema-validate.js.map +1 -1
- package/dist/audit/templates.d.ts +0 -24
- package/dist/audit/templates.d.ts.map +1 -1
- package/dist/audit/templates.js +1 -21
- package/dist/audit/templates.js.map +1 -1
- package/dist/audit/topicGaps.d.ts +0 -1
- package/dist/audit/topicGaps.d.ts.map +1 -1
- package/dist/audit/topicGaps.js +3 -24
- package/dist/audit/topicGaps.js.map +1 -1
- package/dist/core/AuditDatabase.d.ts +0 -14
- package/dist/core/AuditDatabase.d.ts.map +1 -1
- package/dist/core/AuditDatabase.js +8 -61
- package/dist/core/AuditDatabase.js.map +1 -1
- package/dist/core/Backlinks.d.ts +0 -7
- package/dist/core/Backlinks.d.ts.map +1 -1
- package/dist/core/Backlinks.js +1 -23
- package/dist/core/Backlinks.js.map +1 -1
- package/dist/core/Crawler.d.ts +0 -1
- package/dist/core/Crawler.d.ts.map +1 -1
- package/dist/core/Crawler.js +24 -119
- package/dist/core/Crawler.js.map +1 -1
- package/dist/core/DataForSeoClient.d.ts +0 -74
- package/dist/core/DataForSeoClient.d.ts.map +1 -1
- package/dist/core/DataForSeoClient.js +1 -72
- package/dist/core/DataForSeoClient.js.map +1 -1
- package/dist/core/Entities.d.ts +0 -6
- package/dist/core/Entities.d.ts.map +1 -1
- package/dist/core/Entities.js +0 -8
- package/dist/core/Entities.js.map +1 -1
- package/dist/core/FirecrawlClient.d.ts +0 -18
- package/dist/core/FirecrawlClient.d.ts.map +1 -1
- package/dist/core/FirecrawlClient.js +1 -13
- package/dist/core/FirecrawlClient.js.map +1 -1
- package/dist/core/GscClient.d.ts +0 -7
- package/dist/core/GscClient.d.ts.map +1 -1
- package/dist/core/GscClient.js +1 -10
- package/dist/core/GscClient.js.map +1 -1
- package/dist/core/GscSync.d.ts +0 -4
- package/dist/core/GscSync.d.ts.map +1 -1
- package/dist/core/GscSync.js +2 -33
- package/dist/core/GscSync.js.map +1 -1
- package/dist/core/JobManager.d.ts +0 -5
- package/dist/core/JobManager.d.ts.map +1 -1
- package/dist/core/JobManager.js +0 -8
- package/dist/core/JobManager.js.map +1 -1
- package/dist/core/LinkIntersect.d.ts +0 -1
- package/dist/core/LinkIntersect.d.ts.map +1 -1
- package/dist/core/LinkIntersect.js +1 -37
- package/dist/core/LinkIntersect.js.map +1 -1
- package/dist/core/MajesticClient.d.ts +0 -26
- package/dist/core/MajesticClient.d.ts.map +1 -1
- package/dist/core/MajesticClient.js +0 -15
- package/dist/core/MajesticClient.js.map +1 -1
- package/dist/core/RankTracker.d.ts +0 -4
- package/dist/core/RankTracker.d.ts.map +1 -1
- package/dist/core/RankTracker.js +0 -9
- package/dist/core/RankTracker.js.map +1 -1
- package/dist/core/Refresh.d.ts +0 -6
- package/dist/core/Refresh.d.ts.map +1 -1
- package/dist/core/Refresh.js +0 -6
- package/dist/core/Refresh.js.map +1 -1
- package/dist/core/SupadataClient.d.ts +0 -11
- package/dist/core/SupadataClient.d.ts.map +1 -1
- package/dist/core/SupadataClient.js +0 -5
- package/dist/core/SupadataClient.js.map +1 -1
- package/dist/core/UrlInspector.d.ts +0 -5
- package/dist/core/UrlInspector.d.ts.map +1 -1
- package/dist/core/UrlInspector.js +0 -6
- package/dist/core/UrlInspector.js.map +1 -1
- package/dist/core/WikidataClient.d.ts +0 -2
- package/dist/core/WikidataClient.d.ts.map +1 -1
- package/dist/core/WikidataClient.js +0 -7
- package/dist/core/WikidataClient.js.map +1 -1
- package/dist/core/agentReadiness.d.ts +0 -9
- package/dist/core/agentReadiness.d.ts.map +1 -1
- package/dist/core/agentReadiness.js +0 -13
- package/dist/core/agentReadiness.js.map +1 -1
- package/dist/core/ctrModel.js +0 -3
- package/dist/core/ctrModel.js.map +1 -1
- package/dist/core/dashboardData.d.ts +0 -1
- package/dist/core/dashboardData.d.ts.map +1 -1
- package/dist/core/dashboardData.js +13 -95
- package/dist/core/dashboardData.js.map +1 -1
- package/dist/core/dataStorage.d.ts +0 -2
- package/dist/core/dataStorage.d.ts.map +1 -1
- package/dist/core/dataStorage.js +6 -19
- package/dist/core/dataStorage.js.map +1 -1
- package/dist/core/draftBrief.d.ts +0 -7
- package/dist/core/draftBrief.d.ts.map +1 -1
- package/dist/core/draftBrief.js +0 -10
- package/dist/core/draftBrief.js.map +1 -1
- package/dist/core/extract.d.ts +0 -11
- package/dist/core/extract.d.ts.map +1 -1
- package/dist/core/extract.js +1 -38
- package/dist/core/extract.js.map +1 -1
- package/dist/core/googleNews.d.ts +0 -11
- package/dist/core/googleNews.d.ts.map +1 -1
- package/dist/core/googleNews.js +0 -7
- package/dist/core/googleNews.js.map +1 -1
- package/dist/core/gscFreshness.d.ts +0 -10
- package/dist/core/gscFreshness.d.ts.map +1 -1
- package/dist/core/gscFreshness.js +0 -11
- package/dist/core/gscFreshness.js.map +1 -1
- package/dist/core/linkGraph.d.ts +0 -15
- package/dist/core/linkGraph.d.ts.map +1 -1
- package/dist/core/linkGraph.js +1 -23
- package/dist/core/linkGraph.js.map +1 -1
- package/dist/core/marketSizing.d.ts +0 -13
- package/dist/core/marketSizing.d.ts.map +1 -1
- package/dist/core/marketSizing.js +1 -1
- package/dist/core/marketSizing.js.map +1 -1
- package/dist/core/passageScore.d.ts +0 -13
- package/dist/core/passageScore.d.ts.map +1 -1
- package/dist/core/passageScore.js +1 -16
- package/dist/core/passageScore.js.map +1 -1
- package/dist/core/paths.d.ts +0 -3
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +0 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/queryData.d.ts +0 -1
- package/dist/core/queryData.d.ts.map +1 -1
- package/dist/core/queryData.js +0 -18
- package/dist/core/queryData.js.map +1 -1
- package/dist/core/reconFetch.d.ts +0 -6
- package/dist/core/reconFetch.d.ts.map +1 -1
- package/dist/core/reconFetch.js +1 -4
- package/dist/core/reconFetch.js.map +1 -1
- package/dist/core/reconResearch.d.ts +0 -20
- package/dist/core/reconResearch.d.ts.map +1 -1
- package/dist/core/reconResearch.js +1 -16
- package/dist/core/reconResearch.js.map +1 -1
- package/dist/core/reranker.d.ts +0 -2
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +1 -11
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/robots.d.ts +0 -6
- package/dist/core/robots.d.ts.map +1 -1
- package/dist/core/robots.js +1 -7
- package/dist/core/robots.js.map +1 -1
- package/dist/core/serpFootprint.d.ts +0 -12
- package/dist/core/serpFootprint.d.ts.map +1 -1
- package/dist/core/serpFootprint.js +0 -1
- package/dist/core/serpFootprint.js.map +1 -1
- package/dist/core/serpRecon.d.ts +0 -14
- package/dist/core/serpRecon.d.ts.map +1 -1
- package/dist/core/serpRecon.js +1 -13
- package/dist/core/serpRecon.js.map +1 -1
- package/dist/core/sitemap.js +3 -16
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/sql.d.ts +0 -4
- package/dist/core/sql.d.ts.map +1 -1
- package/dist/core/sql.js +0 -4
- package/dist/core/sql.js.map +1 -1
- package/dist/core/url-key.d.ts +0 -31
- package/dist/core/url-key.d.ts.map +1 -1
- package/dist/core/url-key.js +1 -38
- package/dist/core/url-key.js.map +1 -1
- package/dist/core/webHandlers.d.ts +0 -6
- package/dist/core/webHandlers.d.ts.map +1 -1
- package/dist/core/webHandlers.js +2 -5
- package/dist/core/webHandlers.js.map +1 -1
- package/dist/core/webServer.d.ts +0 -16
- package/dist/core/webServer.d.ts.map +1 -1
- package/dist/core/webServer.js +3 -17
- package/dist/core/webServer.js.map +1 -1
- package/dist/dashboard.js +0 -23
- package/dist/dashboard.js.map +1 -1
- package/dist/generators/index.d.ts +0 -7
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +1 -26
- package/dist/generators/index.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/server.js +16 -166
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/audit/engine.js
CHANGED
|
@@ -6,25 +6,18 @@ import { gscFreshness } from '../core/gscFreshness.js';
|
|
|
6
6
|
import { brandToken } from '../core/url-key.js';
|
|
7
7
|
import { snapshotCrawl } from './drift.js';
|
|
8
8
|
import { CHECKS, expectedCtr } from './checks.js';
|
|
9
|
-
const MAX_PER_CHECK = 500;
|
|
10
|
-
// ── 6e priority model: Priority = (T × Y × C) / E — "expected clicks per dev-hour" ──
|
|
11
|
-
// Y (yield): expected % traffic uplift from the fix. Default derived from category×severity;
|
|
12
|
-
// a check can override via yieldCoef. Indexation/crawlability fixes recover most traffic;
|
|
13
|
-
// on-page tweaks are incremental; low-severity = cosmetic.
|
|
9
|
+
const MAX_PER_CHECK = 500;
|
|
14
10
|
const YIELD_BY_CATEGORY = {
|
|
15
11
|
indexation: 0.8, crawlability: 0.7, merged: 0.4, security: 0.5,
|
|
16
12
|
schema: 0.3, content: 0.25, onpage: 0.2, performance: 0.5, 'war-stories': 0.5,
|
|
17
13
|
};
|
|
18
|
-
// Low/info are cosmetic — near-zero real traffic uplift, so they don't ride a high-traffic page up.
|
|
19
14
|
const SEVERITY_YIELD_ADJ = { crit: 1.1, high: 1, med: 0.7, low: 0.15, info: 0.05 };
|
|
20
15
|
function yieldOf(chk) {
|
|
21
16
|
if (chk.yieldCoef != null)
|
|
22
17
|
return Math.max(0, Math.min(1, chk.yieldCoef));
|
|
23
18
|
return Math.min(1, (YIELD_BY_CATEGORY[chk.category] ?? 0.25) * SEVERITY_YIELD_ADJ[chk.severity]);
|
|
24
19
|
}
|
|
25
|
-
// Site-wide (null-url) findings: stake = a severity-scaled fraction of total site clicks.
|
|
26
20
|
const SITEWIDE_FRACTION = { crit: 0.25, high: 0.12, med: 0.04, low: 0.01, info: 0.005 };
|
|
27
|
-
// Floor so a high-severity finding on a zero-GSC page (e.g. backlinks-to-404) never scores 0.
|
|
28
21
|
const SEVERITY_FLOOR = { crit: 5, high: 2, med: 0.5, low: 0.1, info: 0.05 };
|
|
29
22
|
export function listChecks() {
|
|
30
23
|
return CHECKS.map(({ run: _run, ...meta }) => meta);
|
|
@@ -34,26 +27,20 @@ function effortScale(fixType, n) {
|
|
|
34
27
|
return 1;
|
|
35
28
|
if (fixType === 'automated')
|
|
36
29
|
return 1.2;
|
|
37
|
-
return Math.log10(n + 9);
|
|
30
|
+
return Math.log10(n + 9);
|
|
38
31
|
}
|
|
39
32
|
export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
40
33
|
const t0 = Date.now();
|
|
41
34
|
const db = new AuditDatabase(dbPathFor(dataDir, siteUrl));
|
|
42
35
|
try {
|
|
43
|
-
// Finalised GSC date (trailing partial days trimmed) — keeps the audit's 28d windows and the
|
|
44
|
-
// per-page traffic map consistent with the dashboard, and free of unfinalised-data distortion.
|
|
45
36
|
const maxDate = gscFreshness(db.db).effectiveMax;
|
|
46
37
|
const pageCount = db.db.prepare('SELECT COUNT(*) c FROM pages').get().c;
|
|
47
|
-
// Self-heal: if the latest crawl's link-graph was left unpopulated (e.g. the crawl process
|
|
48
|
-
// was killed before finalizing), recompute inlink_count/iPR/click_depth so graph-dependent
|
|
49
|
-
// checks (deep-pages, orphans, iPR-bleed, underlinked-high-demand…) aren't silently no-op.
|
|
50
38
|
if (pageCount > 0) {
|
|
51
39
|
const cr = db.db.prepare('SELECT crawl_id FROM pages LIMIT 1').get();
|
|
52
40
|
if (cr) {
|
|
53
41
|
const g = db.db.prepare('SELECT MAX(ipr) mi, MAX(inlink_count) mc FROM pages WHERE crawl_id=?').get(cr.crawl_id);
|
|
54
42
|
if ((g.mi ?? 0) === 0 && (g.mc ?? 0) === 0)
|
|
55
43
|
finalizeLinkGraph(db.db, cr.crawl_id, null);
|
|
56
|
-
// Drift snapshot (idempotent per crawl) so change-detection has cross-crawl history.
|
|
57
44
|
const at = db.db.prepare(`SELECT COALESCE(finished_at, datetime('now')) at FROM crawl_metadata WHERE crawl_id=?`).get(cr.crawl_id)?.at ?? new Date().toISOString();
|
|
58
45
|
snapshotCrawl(db.db, cr.crawl_id, at);
|
|
59
46
|
}
|
|
@@ -65,8 +52,6 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
65
52
|
checks = checks.filter(c => opts.categories.includes(c.category));
|
|
66
53
|
if (!opts.includeJudgement)
|
|
67
54
|
checks = checks.filter(c => c.certainty >= 1);
|
|
68
|
-
// Precompute per-page traffic for the window in ONE covering scan, instead of a query per
|
|
69
|
-
// finding (which was hundreds of windowed aggregations on a 1.8M-row GSC table — seconds).
|
|
70
55
|
const trafficMap = new Map();
|
|
71
56
|
if (maxDate) {
|
|
72
57
|
for (const row of db.db.prepare(`SELECT page_key, COALESCE(SUM(clicks),0) clicks, COALESCE(SUM(impressions),0) impressions,
|
|
@@ -75,7 +60,6 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
75
60
|
trafficMap.set(row.page_key, { clicks: row.clicks, impressions: row.impressions, position: row.position });
|
|
76
61
|
}
|
|
77
62
|
}
|
|
78
|
-
// Total site clicks in the window — the baseline for site-wide (null-url) findings (6e).
|
|
79
63
|
const siteClicks = maxDate
|
|
80
64
|
? db.db.prepare(`SELECT COALESCE(SUM(clicks),0) c FROM search_analytics WHERE date <= '${maxDate}' AND date > date(?, '-28 days')`).get(maxDate).c
|
|
81
65
|
: 0;
|
|
@@ -86,37 +70,24 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
86
70
|
let total = 0;
|
|
87
71
|
const bySeverity = {};
|
|
88
72
|
const byCategory = {};
|
|
89
|
-
const truncated = new Map();
|
|
73
|
+
const truncated = new Map();
|
|
90
74
|
const tx = db.db.transaction(() => {
|
|
91
75
|
for (const chk of checks) {
|
|
92
76
|
const all = chk.run(ctx);
|
|
93
|
-
// Effort scales with the REAL number of affected pages, not the stored sample. Scaling on
|
|
94
|
-
// the capped length understates E (log10(509) vs log10(5009)) and therefore INFLATES
|
|
95
|
-
// priority = (T×Y×C)/E on exactly the biggest checks — the cap was reordering the report.
|
|
96
77
|
const scale = effortScale(chk.fixType, all.length);
|
|
97
78
|
const findings = all.slice(0, MAX_PER_CHECK);
|
|
98
79
|
if (all.length > MAX_PER_CHECK)
|
|
99
80
|
truncated.set(chk.id, all.length);
|
|
100
|
-
const E = Math.max(chk.effortBase * scale, 0.0001);
|
|
81
|
+
const E = Math.max(chk.effortBase * scale, 0.0001);
|
|
101
82
|
const Y = yieldOf(chk);
|
|
102
83
|
for (const f of findings) {
|
|
103
84
|
const ev = (f.evidence ?? {});
|
|
104
85
|
const evImpr = Number(ev.impressions) || 0;
|
|
105
86
|
const evClicks = Number(ev.clicks) || 0;
|
|
106
|
-
// `positions` can be a range string ("1.1–15.3", cannibalisation). parseFloat would
|
|
107
|
-
// take the BEST position and price the whole query's impressions at its CTR (wildly
|
|
108
|
-
// inflating T when the leader sits at #1) — use the midpoint of the range instead.
|
|
109
87
|
const posNums = String(ev.positions ?? ev.position ?? '').match(/\d+(?:\.\d+)?/g)?.map(Number) ?? [];
|
|
110
88
|
const evPos = posNums.length ? posNums.reduce((a, b) => a + b, 0) / posNums.length : 0;
|
|
111
89
|
const hasQuery = ev.query != null && String(ev.query).length > 0;
|
|
112
90
|
const pageTraf = f.urlKey ? trafficMap.get(f.urlKey) : null;
|
|
113
|
-
// Choose the right stake per finding shape:
|
|
114
|
-
// • per-query findings (evidence has a `query`: striking-distance, cannibalisation) →
|
|
115
|
-
// the query's OWN impressions/clicks (so 2.7k shows 2.7k, not the page's 257k total,
|
|
116
|
-
// and multiple per-query findings on one page get DISTINCT priorities);
|
|
117
|
-
// • page-level findings on a known URL (ctr-below-expected, missing-meta, …) →
|
|
118
|
-
// the page's real GSC traffic (keeps its actual clicks, not 0);
|
|
119
|
-
// • null-url findings that still carry evidence traffic → that evidence.
|
|
120
91
|
let traf;
|
|
121
92
|
if (hasQuery) {
|
|
122
93
|
traf = { clicks: evClicks, impressions: evImpr, position: evPos || (pageTraf?.position ?? 0) };
|
|
@@ -130,9 +101,6 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
130
101
|
else {
|
|
131
102
|
traf = { clicks: 0, impressions: 0, position: 0 };
|
|
132
103
|
}
|
|
133
|
-
// T = expected monthly clicks at stake: max(current clicks recovered, potential from
|
|
134
|
-
// impressions × CTR@position). Site-wide findings with no traffic signal fall back to a
|
|
135
|
-
// severity fraction of total site clicks.
|
|
136
104
|
let T;
|
|
137
105
|
if (traf.impressions > 0 || traf.clicks > 0) {
|
|
138
106
|
const potential = traf.impressions > 0 ? traf.impressions * expectedCtr(traf.position || 10) : 0;
|
|
@@ -142,10 +110,10 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
142
110
|
T = siteClicks * SITEWIDE_FRACTION[chk.severity];
|
|
143
111
|
}
|
|
144
112
|
else {
|
|
145
|
-
T = 0;
|
|
113
|
+
T = 0;
|
|
146
114
|
}
|
|
147
|
-
T = Math.max(T, SEVERITY_FLOOR[chk.severity]);
|
|
148
|
-
const priority = (T * Y * chk.certainty) / E;
|
|
115
|
+
T = Math.max(T, SEVERITY_FLOOR[chk.severity]);
|
|
116
|
+
const priority = (T * Y * chk.certainty) / E;
|
|
149
117
|
insert.run({
|
|
150
118
|
run_id: runId, check_id: chk.id, category: chk.category, severity: chk.severity,
|
|
151
119
|
labels: JSON.stringify(chk.labels), certainty: chk.certainty, url_key: f.urlKey ?? null,
|
|
@@ -161,8 +129,6 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
161
129
|
});
|
|
162
130
|
tx();
|
|
163
131
|
db.db.prepare(`UPDATE audit_runs SET finished_at=datetime('now'), finding_count=? WHERE run_id=?`).run(total, runId);
|
|
164
|
-
// Stored counts, annotated with the true total wherever the per-check cap bit — a bare
|
|
165
|
-
// "count: 500" reads as "that's all there was" when it may be 5,000.
|
|
166
132
|
const byCheck = db.db
|
|
167
133
|
.prepare(`SELECT check_id checkId, category, severity, COUNT(*) count, AVG(priority) priority FROM findings WHERE run_id=? GROUP BY check_id ORDER BY count DESC`)
|
|
168
134
|
.all(runId)
|
|
@@ -188,7 +154,6 @@ export function runAudit(dataDir, siteUrl, opts = {}) {
|
|
|
188
154
|
db.close();
|
|
189
155
|
}
|
|
190
156
|
}
|
|
191
|
-
/** Run one named check and return its findings (no persistence) — for query_audit. */
|
|
192
157
|
export function runSingleCheck(dataDir, siteUrl, checkId, limit = 100, offset = 0) {
|
|
193
158
|
const chk = CHECKS.find(c => c.id === checkId);
|
|
194
159
|
if (!chk)
|
package/dist/audit/engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/audit/engine.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/audit/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,4 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
|
-
/**
|
|
3
|
-
* Demand-first keyword clustering ("list mode") - group a keyword list into topics and
|
|
4
|
-
* give each cluster a verdict: OWN / WEAK / ABSENT, with the ranking URL.
|
|
5
|
-
*
|
|
6
|
-
* Clustering is GSC-bipartite first (free, deterministic, first-party): two keywords
|
|
7
|
-
* belong together when Google already answers both with the SAME page - the strongest
|
|
8
|
-
* possible clustering signal, and it costs nothing. Keywords the site has no GSC rows
|
|
9
|
-
* for are grouped lexically (token Jaccard) - first into the nearest GSC cluster, else
|
|
10
|
-
* among themselves.
|
|
11
|
-
*
|
|
12
|
-
* Verdicts are deterministic from GSC positions: OWN = best position <= 3, WEAK = 4-20,
|
|
13
|
-
* ABSENT = no ranking page. Impressions stand in for demand (real, first-party); pass
|
|
14
|
-
* the clusters through keyword_volume for market-level volumes when needed.
|
|
15
|
-
*/
|
|
16
2
|
export interface KeywordCluster {
|
|
17
3
|
head: string;
|
|
18
4
|
verdict: 'own' | 'weak' | 'absent';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keywordList.d.ts","sourceRoot":"","sources":["../../src/audit/keywordList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"keywordList.d.ts","sourceRoot":"","sources":["../../src/audit/keywordList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAiB3C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CAC/F;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAYD,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,iBAAiB,CAoEvK"}
|
|
@@ -12,12 +12,10 @@ export function clusterKeywordList(db, keywordsIn, opts) {
|
|
|
12
12
|
const cap = opts?.maxKeywords ?? 2000;
|
|
13
13
|
const derived = !keywordsIn?.length;
|
|
14
14
|
const ub = opts?.maxDate ? `AND date <= '${opts.maxDate}'` : '';
|
|
15
|
-
// No list supplied → derive the demand list from the site's own GSC queries (top by impressions).
|
|
16
15
|
const keywords = derived
|
|
17
16
|
? db.prepare(`SELECT query FROM search_analytics WHERE query IS NOT NULL AND date > date('now', '-90 days') ${ub}
|
|
18
17
|
GROUP BY query ORDER BY SUM(impressions) DESC LIMIT ?`).all(Math.min(cap, 500)).map(r => r.query)
|
|
19
18
|
: [...new Set(keywordsIn.map(k => k.trim().toLowerCase()).filter(Boolean))].slice(0, cap);
|
|
20
|
-
// Best ranking page per keyword (90d, impression-weighted position, top page by impressions).
|
|
21
19
|
const stmt = db.prepare(`
|
|
22
20
|
SELECT page_key url, SUM(impressions) imp, SUM(clicks) clk,
|
|
23
21
|
SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
|
|
@@ -30,7 +28,6 @@ export function clusterKeywordList(db, keywordsIn, opts) {
|
|
|
30
28
|
? { keyword: k, url: r.url, pos: r.pos != null ? Math.round(r.pos * 10) / 10 : null, imp: r.imp, clk: r.clk, inGsc: true }
|
|
31
29
|
: { keyword: k, url: null, pos: null, imp: 0, clk: 0, inGsc: false };
|
|
32
30
|
});
|
|
33
|
-
// 1) Bipartite: cluster by shared ranking URL.
|
|
34
31
|
const byUrl = new Map();
|
|
35
32
|
const orphans = [];
|
|
36
33
|
for (const r of rows)
|
|
@@ -38,8 +35,6 @@ export function clusterKeywordList(db, keywordsIn, opts) {
|
|
|
38
35
|
const working = [...byUrl.entries()].map(([url, members]) => ({
|
|
39
36
|
url, members, tok: new Set(members.flatMap(m => [...tokens(m.keyword)])),
|
|
40
37
|
}));
|
|
41
|
-
// 2) Lexical assignment for non-ranking keywords: nearest GSC cluster at Jaccard >= 0.4,
|
|
42
|
-
// else greedy-cluster among themselves (same threshold).
|
|
43
38
|
const lexical = [];
|
|
44
39
|
for (const o of orphans) {
|
|
45
40
|
const tok = tokens(o.keyword);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keywordList.js","sourceRoot":"","sources":["../../src/audit/keywordList.ts"],"names":[],"mappings":"AAkCA,MAAM,MAAM,GAAG,CAAC,CAAS,EAAe,EAAE,CACxC,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,OAAO,GAAG,CAAC,CAAc,EAAE,CAAc,EAAU,EAAE;IACzD,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACjC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,KAAK,EAAE,CAAC;IACzC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;AAC3C,CAAC,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAAC,EAAqB,EAAE,UAAgC,EAAE,IAAwD;IAClJ,MAAM,GAAG,GAAG,IAAI,EAAE,WAAW,IAAI,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC;IACpC,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"keywordList.js","sourceRoot":"","sources":["../../src/audit/keywordList.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
import { gscFreshness } from '../core/gscFreshness.js';
|
|
2
|
-
/**
|
|
3
|
-
* New-page opportunity engine (Phase 6c). Grounded in REAL demand (GSC), with ruthless
|
|
4
|
-
* subtraction so we never propose a page that already exists. Computable from stored
|
|
5
|
-
* GSC + crawl data — no paid calls. (SERP-overlap clustering and search-volume/intent
|
|
6
|
-
* scoring are refinements layered on via DataForSEO when available.)
|
|
7
|
-
*
|
|
8
|
-
* Pipeline: demand universe (queries we get impressions for but rank 11+ → no winning page)
|
|
9
|
-
* → subtract (A: we already rank ≤10; B: a page already covers ≥70% of the query's words;
|
|
10
|
-
* C: one URL owns >80% of the query's impressions → that's an optimisation, not a new page)
|
|
11
|
-
* → cluster surviving queries lexically → score by impressions × intent → attach evidence.
|
|
12
|
-
*/
|
|
13
2
|
const STOP = new Set(['the', 'a', 'an', 'and', 'or', 'of', 'for', 'to', 'in', 'on', 'with', 'your', 'you', 'is', 'are', 'best', 'how', 'what', 'vs', 'why', 'can', 'my', 'i', 'it']);
|
|
14
3
|
const tokenize = (s) => (s || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').split(/\s+/).filter(t => t.length >= 2 && !STOP.has(t));
|
|
15
4
|
const jaccard = (a, b) => {
|
|
@@ -25,13 +14,10 @@ const INTENT_MULT = { transactional: 1, commercial: 0.8, informational: 0.3, nav
|
|
|
25
14
|
export function suggestPages(db, opts = {}) {
|
|
26
15
|
const minImpr = opts.minImpressions ?? 50;
|
|
27
16
|
const clusterT = opts.clusterThreshold ?? 0.5;
|
|
28
|
-
// Window off the FINALISED max date (raw MAX(date) includes 1–3 unfinalised, systematically
|
|
29
|
-
// low days that deflate impressions and can shift a query across the rank/dominance thresholds).
|
|
30
17
|
const maxDate = gscFreshness(db).effectiveMax;
|
|
31
18
|
if (!maxDate)
|
|
32
19
|
return { proposals: [], consideredQueries: 0, afterDedup: 0 };
|
|
33
20
|
const win = `date > date('${maxDate}', '-28 days') AND date <= '${maxDate}'`;
|
|
34
|
-
// Demand: per query × page (impression-weighted position).
|
|
35
21
|
const qp = db.prepare(`SELECT query, page_key, SUM(impressions) impr, SUM(clicks) clicks,
|
|
36
22
|
SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
|
|
37
23
|
FROM search_analytics WHERE query IS NOT NULL AND page_key IS NOT NULL AND ${win}
|
|
@@ -48,7 +34,6 @@ export function suggestPages(db, opts = {}) {
|
|
|
48
34
|
q.best = Math.min(q.best, r.pos);
|
|
49
35
|
q.pages.push({ key: r.page_key, impr: r.impr, pos: r.pos });
|
|
50
36
|
}
|
|
51
|
-
// Existing-page vocabulary (title + h1 + url slug) → inverted index for the 70% dedup.
|
|
52
37
|
const pages = db.prepare(`SELECT url_key, title, h1, url FROM pages WHERE status_code=200`).all();
|
|
53
38
|
const pageTokens = pages.map(p => {
|
|
54
39
|
const slug = (() => { try {
|
|
@@ -62,30 +47,24 @@ export function suggestPages(db, opts = {}) {
|
|
|
62
47
|
const invIndex = new Map();
|
|
63
48
|
pageTokens.forEach((pt, i) => { for (const t of pt.toks)
|
|
64
49
|
(invIndex.get(t) ?? invIndex.set(t, []).get(t)).push(i); });
|
|
65
|
-
// Persisted intent (optional) for scoring.
|
|
66
50
|
const intentOf = new Map();
|
|
67
51
|
try {
|
|
68
52
|
for (const r of db.prepare('SELECT keyword, intent FROM keyword_intent').all())
|
|
69
53
|
intentOf.set(r.keyword, r.intent);
|
|
70
54
|
}
|
|
71
|
-
catch {
|
|
72
|
-
// Subtraction.
|
|
55
|
+
catch { }
|
|
73
56
|
const survivors = [];
|
|
74
57
|
for (const [query, q] of byQuery) {
|
|
75
58
|
if (q.totalImpr < minImpr)
|
|
76
59
|
continue;
|
|
77
60
|
if (q.best <= 10)
|
|
78
|
-
continue;
|
|
79
|
-
// C: one URL owns >80% of impressions AND is a genuine contender (ranks ≤15) → optimise
|
|
80
|
-
// that page, don't spawn a new one. (Gated on rank so an incidental single-page mention at
|
|
81
|
-
// position 30 doesn't suppress a real gap — long-tail queries usually surface one page.)
|
|
61
|
+
continue;
|
|
82
62
|
const dominant = q.pages.reduce((a, b) => (b.impr > a.impr ? b : a));
|
|
83
63
|
if (dominant.impr / q.totalImpr > 0.8 && dominant.pos <= 15)
|
|
84
64
|
continue;
|
|
85
65
|
const qt = tokenize(query);
|
|
86
66
|
if (!qt.length)
|
|
87
67
|
continue;
|
|
88
|
-
// B: does any existing page already cover ≥70% of the query's words?
|
|
89
68
|
const hits = new Map();
|
|
90
69
|
for (const t of qt)
|
|
91
70
|
for (const i of invIndex.get(t) ?? [])
|
|
@@ -97,10 +76,9 @@ export function suggestPages(db, opts = {}) {
|
|
|
97
76
|
maxCover = cover;
|
|
98
77
|
}
|
|
99
78
|
if (maxCover >= 0.7)
|
|
100
|
-
continue;
|
|
79
|
+
continue;
|
|
101
80
|
survivors.push({ query, toks: new Set(qt), impr: q.totalImpr, clicks: q.totalClicks, best: q.best });
|
|
102
81
|
}
|
|
103
|
-
// Cluster survivors lexically (greedy Jaccard); head term = highest-impression query.
|
|
104
82
|
survivors.sort((a, b) => b.impr - a.impr);
|
|
105
83
|
const clusters = [];
|
|
106
84
|
for (const s of survivors) {
|
|
@@ -119,7 +97,6 @@ export function suggestPages(db, opts = {}) {
|
|
|
119
97
|
const proposals = clusters.map(c => {
|
|
120
98
|
const intent = intentOf.get(c.headTerm.toLowerCase()) ?? null;
|
|
121
99
|
const mult = intent ? (INTENT_MULT[intent] ?? 0.5) : 0.5;
|
|
122
|
-
// nearest existing page: best partial token coverage of the head term (where to add a link).
|
|
123
100
|
const ht = tokenize(c.headTerm);
|
|
124
101
|
let cover = 0, nearestKey = null;
|
|
125
102
|
const hits = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunities.js","sourceRoot":"","sources":["../../src/audit/opportunities.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"opportunities.js","sourceRoot":"","sources":["../../src/audit/opportunities.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAavD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AACrL,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAY,EAAE,CACvC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/G,MAAM,OAAO,GAAG,CAAC,CAAc,EAAE,CAAc,EAAU,EAAE;IACzD,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACjC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,KAAK,EAAE,CAAC;IACzC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;AAC3C,CAAC,CAAC;AACF,MAAM,WAAW,GAA2B,EAAE,aAAa,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC;AAezH,MAAM,UAAU,YAAY,CAAC,EAAqB,EAAE,OAAa,EAAE;IACjE,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,IAAI,GAAG,CAAC;IAG9C,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC9C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAC5E,MAAM,GAAG,GAAG,gBAAgB,OAAO,+BAA+B,OAAO,GAAG,CAAC;IAG7E,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CACnB;;kFAE8E,GAAG;8BACvD,CAC3B,CAAC,GAAG,EAAsF,CAAC;IAE5F,MAAM,OAAO,GAAG,IAAI,GAAG,EAA+I,CAAC;IACvK,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACnB,IAAI,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,CAAC,EAAE,CAAC;YAAC,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAAC,CAAC;QACjH,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC;QAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;QACjD,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9D,CAAC;IAGD,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,iEAAiE,CAAC,CAAC,GAAG,EAAiF,CAAC;IACjL,MAAM,UAAU,GAAyC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACrE,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;YAAC,OAAO,kBAAkB,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,EAAE,CAAC;QAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpI,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;IAC/F,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC7C,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI;QAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAGtH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,IAAI,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC,GAAG,EAA2C;YAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7J,CAAC;IAAC,MAAM,CAAC,CAA2C,CAAC;IAGrD,MAAM,SAAS,GAAuF,EAAE,CAAC;IACzG,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;QACjC,IAAI,CAAC,CAAC,SAAS,GAAG,OAAO;YAAE,SAAS;QACpC,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE;YAAE,SAAS;QAI3B,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,GAAG,GAAG,IAAI,QAAQ,CAAC,GAAG,IAAI,EAAE;YAAE,SAAS;QACtE,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,CAAC,EAAE,CAAC,MAAM;YAAE,SAAS;QAEzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,EAAE;YAAE,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;gBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/F,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YAAC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC;YAAC,IAAI,KAAK,GAAG,QAAQ;gBAAE,QAAQ,GAAG,KAAK,CAAC;QAAC,CAAC;QAClG,IAAI,QAAQ,IAAI,GAAG;YAAE,SAAS;QAC9B,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvG,CAAC;IAGD,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAE1C,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,CAAC;QAClE,IAAI,CAAC,EAAE,CAAC;YAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI;gBAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC;YAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC;YAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;;YAChJ,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACrI,CAAC;IAED,MAAM,SAAS,GAAmB,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACjD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC;QAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAEzD,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,KAAK,GAAG,CAAC,EAAE,UAAU,GAAkB,IAAI,CAAC;QACjF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,EAAE;YAAE,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;gBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/F,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAAC,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC;gBAAC,KAAK,GAAG,EAAE,CAAC;gBAAC,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YAAC,CAAC;QAAC,CAAC;QACrI,OAAO;YACL,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;YAC/B,gBAAgB,EAAE,CAAC,CAAC,IAAI;YACxB,WAAW,EAAE,CAAC,CAAC,MAAM;YACrB,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,EAAE;YAC1C,MAAM;YACN,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;YAChC,mBAAmB,EAAE,UAAU;SAChC,CAAC;IACJ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IAEvE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;AACtF,CAAC"}
|
package/dist/audit/recon.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
2
|
import type { OwnPageView } from '../core/reconFetch.js';
|
|
3
3
|
import type { SerpReconResult, ReconVerdict } from '../core/serpRecon.js';
|
|
4
|
-
/**
|
|
5
|
-
* Content recon core: pick the pages worth a deep competitive look, and turn what we can
|
|
6
|
-
* determine WITHOUT judgement (schema gaps, freshness, cannibalisation, SERP format) into
|
|
7
|
-
* a trackable to-do ledger. The judgement to-dos (what competitors cover that we don't)
|
|
8
|
-
* are written back by the research session via save_recon_todo.
|
|
9
|
-
*/
|
|
10
4
|
export interface ReconTarget {
|
|
11
5
|
urlKey: string;
|
|
12
6
|
query: string;
|
|
@@ -17,16 +11,11 @@ export interface ReconTarget {
|
|
|
17
11
|
slipped: boolean;
|
|
18
12
|
competingUrls: number;
|
|
19
13
|
}
|
|
20
|
-
/** Pick declining / striking-distance pages: high impressions, currently pos 3-15, top query per page. */
|
|
21
14
|
export declare function selectReconTargets(db: Database.Database, opts: {
|
|
22
15
|
limit?: number;
|
|
23
16
|
minImpressions?: number;
|
|
24
17
|
maxDate: string;
|
|
25
18
|
}): ReconTarget[];
|
|
26
|
-
/** What the CRAWL says about this URL — the reality check the SERP + GSC read has to survive.
|
|
27
|
-
* GSC history is legacy: it keeps reporting impressions for a URL that has since been
|
|
28
|
-
* canonicalised away or set noindex, and classifying off that alone produced a
|
|
29
|
-
* "defend-and-deepen, produce a companion video" verdict for a non-indexable duplicate. */
|
|
30
19
|
export interface PageState {
|
|
31
20
|
urlKey: string;
|
|
32
21
|
crawled: boolean;
|
|
@@ -40,7 +29,6 @@ export interface PageState {
|
|
|
40
29
|
cannotRank: boolean;
|
|
41
30
|
}
|
|
42
31
|
export declare function pageState(db: Database.Database, urlKey: string): PageState;
|
|
43
|
-
/** A crawl row that contradicts the SERP/GSC read WINS. Returns null when there's no conflict. */
|
|
44
32
|
export declare function crawlRealityOverride(v: {
|
|
45
33
|
verdict: ReconVerdict;
|
|
46
34
|
note: string;
|
|
@@ -55,12 +43,6 @@ export interface DraftTodo {
|
|
|
55
43
|
evidence: Record<string, unknown>;
|
|
56
44
|
priority: number;
|
|
57
45
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Priority = OPPORTUNITY, not impressions. Raw impressions ranked a #4 defend-and-deepen page
|
|
60
|
-
* above the genuinely broken one, i.e. roughly the wrong working order. This prices the clicks
|
|
61
|
-
* actually available: impressions x (CTR at a realistic target position - CTR where we are now),
|
|
62
|
-
* damped by the verdict's headroom, then weighted by to-do type.
|
|
63
|
-
*/
|
|
64
46
|
export interface PriorityBasis {
|
|
65
47
|
model: string;
|
|
66
48
|
impressions: number;
|
|
@@ -71,8 +53,6 @@ export interface PriorityBasis {
|
|
|
71
53
|
verdictFactor: number;
|
|
72
54
|
}
|
|
73
55
|
export declare function opportunityBasis(impressions: number, organicRank: number | null, gscPosition: number, verdict: string): PriorityBasis;
|
|
74
|
-
/** The entity-ish terms of a query: the tokens a sibling page would have to share to be competing.
|
|
75
|
-
* Minimum length 2 so short but load-bearing terms ("vr", "ai") survive. */
|
|
76
56
|
export declare function queryCore(query: string): string[];
|
|
77
57
|
export interface ClusterCannibalisation {
|
|
78
58
|
core: string[];
|
|
@@ -86,16 +66,7 @@ export interface ClusterCannibalisation {
|
|
|
86
66
|
}[];
|
|
87
67
|
cannibalising: boolean;
|
|
88
68
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Competition is judged PER QUERY and then unioned across the cluster. Judging it on cluster
|
|
91
|
-
* TOTALS is what hid the overlap: the leader accumulates across every query in the topic, so a
|
|
92
|
-
* genuine rival page falls under the 10%-of-leader guard and disappears. Here a URL qualifies if
|
|
93
|
-
* it competes on any single cluster query (>=10 impressions, >=10% of that query's leader, avg
|
|
94
|
-
* position inside 20) — and we separately count the queries where two or more of them collide,
|
|
95
|
-
* which is what distinguishes cannibalisation from ordinary topical coverage.
|
|
96
|
-
*/
|
|
97
69
|
export declare function clusterCannibalisation(db: Database.Database, query: string, maxDate: string, minImpressions?: number): ClusterCannibalisation;
|
|
98
|
-
/** The to-dos we can generate deterministically from our page + the SERP (no judgement). */
|
|
99
70
|
export declare function deterministicTodos(target: ReconTarget, own: OwnPageView, serp: SerpReconResult, verdict: {
|
|
100
71
|
verdict: ReconVerdict;
|
|
101
72
|
note: string;
|
|
@@ -118,7 +89,5 @@ export declare function persistReconPage(db: Database.Database, p: {
|
|
|
118
89
|
schemaTypes: string[];
|
|
119
90
|
competitors: unknown;
|
|
120
91
|
}): void;
|
|
121
|
-
/** Insert to-dos for a page. `baseline` snapshots the rank/citation state so outcome is measurable.
|
|
122
|
-
* De-dupes on (url_key, action) so a re-run doesn't pile up identical open items. */
|
|
123
92
|
export declare function insertTodos(db: Database.Database, urlKey: string, query: string, todos: DraftTodo[], baseline: Record<string, unknown>, source: 'auto' | 'research'): number;
|
|
124
93
|
//# sourceMappingURL=recon.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recon.d.ts","sourceRoot":"","sources":["../../src/audit/recon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"recon.d.ts","sourceRoot":"","sources":["../../src/audit/recon.ts"],"names":[],"mappings":"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"}
|
package/dist/audit/recon.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { expectedCtr } from '../core/ctrModel.js';
|
|
2
|
-
/** Pick declining / striking-distance pages: high impressions, currently pos 3-15, top query per page. */
|
|
3
2
|
export function selectReconTargets(db, opts) {
|
|
4
3
|
const { limit = 5, minImpressions = 300, maxDate } = opts;
|
|
5
4
|
const rows = db.prepare(`
|
|
@@ -57,7 +56,6 @@ export function pageState(db, urlKey) {
|
|
|
57
56
|
cannotRank: indexable === false || canonicalisedAway,
|
|
58
57
|
};
|
|
59
58
|
}
|
|
60
|
-
/** A crawl row that contradicts the SERP/GSC read WINS. Returns null when there's no conflict. */
|
|
61
59
|
export function crawlRealityOverride(v, state) {
|
|
62
60
|
if (!state.cannotRank)
|
|
63
61
|
return null;
|
|
@@ -73,8 +71,6 @@ export function crawlRealityOverride(v, state) {
|
|
|
73
71
|
const TYPE_WEIGHT = {
|
|
74
72
|
'content-gap': 1.0, schema: 0.9, research: 0.8, cannibalisation: 0.7, format: 0.6, freshness: 0.3,
|
|
75
73
|
};
|
|
76
|
-
/** How much headroom the verdict implies. A page already sitting at #4 with nobody editorial
|
|
77
|
-
* above it has far less to gain than one losing a competitive gap, even at higher impressions. */
|
|
78
74
|
const VERDICT_FACTOR = {
|
|
79
75
|
'competitive-gap': 1.0, 'accuracy-or-freshness': 1.0, 'consolidate-weak-page': 0.9,
|
|
80
76
|
'page-cannot-rank': 0.8, 'defend-and-deepen': 0.4,
|
|
@@ -93,22 +89,10 @@ export function opportunityBasis(impressions, organicRank, gscPosition, verdict)
|
|
|
93
89
|
verdictFactor: VERDICT_FACTOR[verdict] ?? 1,
|
|
94
90
|
};
|
|
95
91
|
}
|
|
96
|
-
/** Cannibalisation at CLUSTER level, not one query at a time. Keying off a single query missed a
|
|
97
|
-
* four-URL overlap that only showed up across the whole "<entity>" query family. */
|
|
98
92
|
const CLUSTER_STOP = new Set(['the', 'and', 'for', 'with', 'best', 'top', 'vs', 'versus', 'review', 'reviews', 'guide', 'how', 'what', 'which', 'why', 'you', 'your', 'cheap', 'budget', 'buy', 'compare', 'comparison', '2024', '2025', '2026', '2027']);
|
|
99
|
-
/** The entity-ish terms of a query: the tokens a sibling page would have to share to be competing.
|
|
100
|
-
* Minimum length 2 so short but load-bearing terms ("vr", "ai") survive. */
|
|
101
93
|
export function queryCore(query) {
|
|
102
94
|
return [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2 && !CLUSTER_STOP.has(t)))].slice(0, 4);
|
|
103
95
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Competition is judged PER QUERY and then unioned across the cluster. Judging it on cluster
|
|
106
|
-
* TOTALS is what hid the overlap: the leader accumulates across every query in the topic, so a
|
|
107
|
-
* genuine rival page falls under the 10%-of-leader guard and disappears. Here a URL qualifies if
|
|
108
|
-
* it competes on any single cluster query (>=10 impressions, >=10% of that query's leader, avg
|
|
109
|
-
* position inside 20) — and we separately count the queries where two or more of them collide,
|
|
110
|
-
* which is what distinguishes cannibalisation from ordinary topical coverage.
|
|
111
|
-
*/
|
|
112
96
|
export function clusterCannibalisation(db, query, maxDate, minImpressions = 10) {
|
|
113
97
|
const core = queryCore(query);
|
|
114
98
|
const none = { core, clusterQueries: 0, collidingQueries: 0, urls: [], cannibalising: false };
|
|
@@ -143,13 +127,10 @@ export function clusterCannibalisation(db, query, maxDate, minImpressions = 10)
|
|
|
143
127
|
cannibalising: urls.length >= 2 && colliding >= 1,
|
|
144
128
|
};
|
|
145
129
|
}
|
|
146
|
-
/** The to-dos we can generate deterministically from our page + the SERP (no judgement). */
|
|
147
130
|
export function deterministicTodos(target, own, serp, verdict, todayIso, ctx = {}) {
|
|
148
131
|
const out = [];
|
|
149
132
|
const basis = opportunityBasis(target.impressions, serp.ourOrganicRank, target.position, verdict.verdict);
|
|
150
133
|
const w = (type) => Math.max(1, Math.round(basis.opportunityClicks * basis.verdictFactor * (TYPE_WEIGHT[type] ?? 0.5)));
|
|
151
|
-
// A URL that cannot rank gets ONE to-do: resolve it. Everything downstream (schema, freshness,
|
|
152
|
-
// a companion video) is work against a page Google will not serve.
|
|
153
134
|
const state = ctx.state;
|
|
154
135
|
if (state?.cannotRank) {
|
|
155
136
|
return [{
|
|
@@ -165,19 +146,16 @@ export function deterministicTodos(target, own, serp, verdict, todayIso, ctx = {
|
|
|
165
146
|
priority: w('cannibalisation'),
|
|
166
147
|
}];
|
|
167
148
|
}
|
|
168
|
-
// Verdict-driven headline action.
|
|
169
149
|
if (verdict.verdict === 'accuracy-or-freshness') {
|
|
170
150
|
out.push({ action: 'Update the facts to current and mark them up so they are machine-liftable', type: 'schema',
|
|
171
151
|
rationale: `You rank organic #${serp.ourOrganicRank} but the AI Overview does not cite you — a data-accuracy/freshness signal.`,
|
|
172
152
|
evidence: { organicRank: serp.ourOrganicRank, aioCitesUs: false, aioReferencesChecked: serp.aioReferences.length }, priority: w('schema') + 1 });
|
|
173
153
|
}
|
|
174
|
-
// Schema: commercial/listicle page without Product/Review markup.
|
|
175
154
|
if (!own.hasProductOrReview && looksCommercial(target.query)) {
|
|
176
155
|
out.push({ action: 'Add Product + Review/AggregateRating schema per item, with current specs and price',
|
|
177
156
|
type: 'schema', rationale: 'A comparison/review page with no item-level markup cannot be lifted into rich results or an AI Overview citation cleanly.',
|
|
178
157
|
evidence: { schemaTypes: own.jsonLdTypes, hasProductOrReview: false }, priority: w('schema') });
|
|
179
158
|
}
|
|
180
|
-
// Freshness: stale dateModified (older than ~90 days from today).
|
|
181
159
|
if (own.dateModified) {
|
|
182
160
|
const ageDays = Math.round((Date.parse(todayIso) - Date.parse(own.dateModified)) / 86400000);
|
|
183
161
|
if (ageDays > 90) {
|
|
@@ -186,7 +164,6 @@ export function deterministicTodos(target, own, serp, verdict, todayIso, ctx = {
|
|
|
186
164
|
evidence: { dateModified: own.dateModified, ageDays }, priority: w('freshness') });
|
|
187
165
|
}
|
|
188
166
|
}
|
|
189
|
-
// Cannibalisation — cluster first (the real overlap), falling back to the single query.
|
|
190
167
|
const cluster = ctx.cluster;
|
|
191
168
|
if (cluster?.cannibalising) {
|
|
192
169
|
out.push({ action: `Consolidate cannibalisation — ${cluster.urls.length} of your URLs compete across the "${cluster.core.join(' ')}" cluster, colliding on ${cluster.collidingQueries} quer${cluster.collidingQueries === 1 ? 'y' : 'ies'}`,
|
|
@@ -200,14 +177,12 @@ export function deterministicTodos(target, own, serp, verdict, todayIso, ctx = {
|
|
|
200
177
|
type: 'cannibalisation', rationale: 'Multiple own URLs competing for one query split signals; pick a canonical winner.',
|
|
201
178
|
evidence: { competingUrls: target.competingUrls, query: target.query }, priority: w('cannibalisation') });
|
|
202
179
|
}
|
|
203
|
-
// Format: AIO/SERP leans on video.
|
|
204
180
|
const ytRefs = serp.aioReferences.filter(r => /youtube|youtu\.be/.test(r.domain)).length;
|
|
205
181
|
if ((serp.videoPresent || ytRefs >= 2)) {
|
|
206
182
|
out.push({ action: 'Produce a companion video',
|
|
207
183
|
type: 'format', rationale: `The SERP favours video${ytRefs ? ` and ${ytRefs} of the AI Overview's references are YouTube` : ''}.`,
|
|
208
184
|
evidence: { videoPresent: serp.videoPresent, youtubeAioRefs: ytRefs }, priority: w('format') });
|
|
209
185
|
}
|
|
210
|
-
// Research pointer: hand the session the competitor set to diff (coverage + originality).
|
|
211
186
|
const competitors = [
|
|
212
187
|
...serp.organicAbove.map(o => ({ rank: o.rank, url: o.url, kind: 'organic-above' })),
|
|
213
188
|
...serp.aioReferences.slice(0, 6).map(r => ({ rank: r.rank, url: r.url, kind: 'aio-reference' })),
|
|
@@ -236,8 +211,6 @@ export function persistReconPage(db, p) {
|
|
|
236
211
|
competitors: JSON.stringify(p.competitors),
|
|
237
212
|
});
|
|
238
213
|
}
|
|
239
|
-
/** Insert to-dos for a page. `baseline` snapshots the rank/citation state so outcome is measurable.
|
|
240
|
-
* De-dupes on (url_key, action) so a re-run doesn't pile up identical open items. */
|
|
241
214
|
export function insertTodos(db, urlKey, query, todos, baseline, source) {
|
|
242
215
|
const exists = db.prepare(`SELECT 1 FROM recon_todo WHERE url_key=? AND action=? AND status NOT IN ('shipped','dismissed')`);
|
|
243
216
|
const ins = db.prepare(`INSERT INTO recon_todo (url_key, query, action, type, rationale, evidence, priority, source, baseline)
|