@houtini/seo-audit-console 0.2.0
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/LICENSE +92 -0
- package/README.md +211 -0
- package/dist/audit/checks.d.ts +35 -0
- package/dist/audit/checks.d.ts.map +1 -0
- package/dist/audit/checks.js +1475 -0
- package/dist/audit/checks.js.map +1 -0
- package/dist/audit/drift.d.ts +40 -0
- package/dist/audit/drift.d.ts.map +1 -0
- package/dist/audit/drift.js +148 -0
- package/dist/audit/drift.js.map +1 -0
- package/dist/audit/engine.d.ts +33 -0
- package/dist/audit/engine.d.ts.map +1 -0
- package/dist/audit/engine.js +186 -0
- package/dist/audit/engine.js.map +1 -0
- package/dist/audit/opportunities.d.ts +23 -0
- package/dist/audit/opportunities.d.ts.map +1 -0
- package/dist/audit/opportunities.js +149 -0
- package/dist/audit/opportunities.js.map +1 -0
- package/dist/audit/report.d.ts +11 -0
- package/dist/audit/report.d.ts.map +1 -0
- package/dist/audit/report.js +59 -0
- package/dist/audit/report.js.map +1 -0
- package/dist/audit/schema-validate.d.ts +35 -0
- package/dist/audit/schema-validate.d.ts.map +1 -0
- package/dist/audit/schema-validate.js +293 -0
- package/dist/audit/schema-validate.js.map +1 -0
- package/dist/audit/templates.d.ts +43 -0
- package/dist/audit/templates.d.ts.map +1 -0
- package/dist/audit/templates.js +129 -0
- package/dist/audit/templates.js.map +1 -0
- package/dist/audit/topicGaps.d.ts +42 -0
- package/dist/audit/topicGaps.d.ts.map +1 -0
- package/dist/audit/topicGaps.js +181 -0
- package/dist/audit/topicGaps.js.map +1 -0
- package/dist/core/AuditDatabase.d.ts +33 -0
- package/dist/core/AuditDatabase.d.ts.map +1 -0
- package/dist/core/AuditDatabase.js +481 -0
- package/dist/core/AuditDatabase.js.map +1 -0
- package/dist/core/Backlinks.d.ts +33 -0
- package/dist/core/Backlinks.d.ts.map +1 -0
- package/dist/core/Backlinks.js +110 -0
- package/dist/core/Backlinks.js.map +1 -0
- package/dist/core/Crawler.d.ts +23 -0
- package/dist/core/Crawler.d.ts.map +1 -0
- package/dist/core/Crawler.js +588 -0
- package/dist/core/Crawler.js.map +1 -0
- package/dist/core/DataForSeoClient.d.ts +86 -0
- package/dist/core/DataForSeoClient.d.ts.map +1 -0
- package/dist/core/DataForSeoClient.js +232 -0
- package/dist/core/DataForSeoClient.js.map +1 -0
- package/dist/core/Entities.d.ts +23 -0
- package/dist/core/Entities.d.ts.map +1 -0
- package/dist/core/Entities.js +62 -0
- package/dist/core/Entities.js.map +1 -0
- package/dist/core/GscClient.d.ts +22 -0
- package/dist/core/GscClient.d.ts.map +1 -0
- package/dist/core/GscClient.js +93 -0
- package/dist/core/GscClient.js.map +1 -0
- package/dist/core/GscSync.d.ts +20 -0
- package/dist/core/GscSync.d.ts.map +1 -0
- package/dist/core/GscSync.js +133 -0
- package/dist/core/GscSync.js.map +1 -0
- package/dist/core/JobManager.d.ts +30 -0
- package/dist/core/JobManager.d.ts.map +1 -0
- package/dist/core/JobManager.js +68 -0
- package/dist/core/JobManager.js.map +1 -0
- package/dist/core/RankTracker.d.ts +25 -0
- package/dist/core/RankTracker.d.ts.map +1 -0
- package/dist/core/RankTracker.js +78 -0
- package/dist/core/RankTracker.js.map +1 -0
- package/dist/core/Refresh.d.ts +32 -0
- package/dist/core/Refresh.d.ts.map +1 -0
- package/dist/core/Refresh.js +73 -0
- package/dist/core/Refresh.js.map +1 -0
- package/dist/core/UrlInspector.d.ts +22 -0
- package/dist/core/UrlInspector.d.ts.map +1 -0
- package/dist/core/UrlInspector.js +92 -0
- package/dist/core/UrlInspector.js.map +1 -0
- package/dist/core/WikidataClient.d.ts +19 -0
- package/dist/core/WikidataClient.d.ts.map +1 -0
- package/dist/core/WikidataClient.js +59 -0
- package/dist/core/WikidataClient.js.map +1 -0
- package/dist/core/agentReadiness.d.ts +34 -0
- package/dist/core/agentReadiness.d.ts.map +1 -0
- package/dist/core/agentReadiness.js +119 -0
- package/dist/core/agentReadiness.js.map +1 -0
- package/dist/core/ctrModel.d.ts +2 -0
- package/dist/core/ctrModel.d.ts.map +1 -0
- package/dist/core/ctrModel.js +6 -0
- package/dist/core/ctrModel.js.map +1 -0
- package/dist/core/dashboardData.d.ts +312 -0
- package/dist/core/dashboardData.d.ts.map +1 -0
- package/dist/core/dashboardData.js +550 -0
- package/dist/core/dashboardData.js.map +1 -0
- package/dist/core/dataStorage.d.ts +42 -0
- package/dist/core/dataStorage.d.ts.map +1 -0
- package/dist/core/dataStorage.js +193 -0
- package/dist/core/dataStorage.js.map +1 -0
- package/dist/core/draftBrief.d.ts +27 -0
- package/dist/core/draftBrief.d.ts.map +1 -0
- package/dist/core/draftBrief.js +69 -0
- package/dist/core/draftBrief.js.map +1 -0
- package/dist/core/extract.d.ts +67 -0
- package/dist/core/extract.d.ts.map +1 -0
- package/dist/core/extract.js +262 -0
- package/dist/core/extract.js.map +1 -0
- package/dist/core/gscFreshness.d.ts +18 -0
- package/dist/core/gscFreshness.d.ts.map +1 -0
- package/dist/core/gscFreshness.js +32 -0
- package/dist/core/gscFreshness.js.map +1 -0
- package/dist/core/linkGraph.d.ts +19 -0
- package/dist/core/linkGraph.d.ts.map +1 -0
- package/dist/core/linkGraph.js +125 -0
- package/dist/core/linkGraph.js.map +1 -0
- package/dist/core/passageScore.d.ts +19 -0
- package/dist/core/passageScore.d.ts.map +1 -0
- package/dist/core/passageScore.js +59 -0
- package/dist/core/passageScore.js.map +1 -0
- package/dist/core/paths.d.ts +5 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +15 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/queryData.d.ts +35 -0
- package/dist/core/queryData.d.ts.map +1 -0
- package/dist/core/queryData.js +200 -0
- package/dist/core/queryData.js.map +1 -0
- package/dist/core/reranker.d.ts +8 -0
- package/dist/core/reranker.d.ts.map +1 -0
- package/dist/core/reranker.js +68 -0
- package/dist/core/reranker.js.map +1 -0
- package/dist/core/robots.d.ts +11 -0
- package/dist/core/robots.d.ts.map +1 -0
- package/dist/core/robots.js +76 -0
- package/dist/core/robots.js.map +1 -0
- package/dist/core/sitemap.d.ts +15 -0
- package/dist/core/sitemap.d.ts.map +1 -0
- package/dist/core/sitemap.js +100 -0
- package/dist/core/sitemap.js.map +1 -0
- package/dist/core/sql.d.ts +6 -0
- package/dist/core/sql.d.ts.map +1 -0
- package/dist/core/sql.js +6 -0
- package/dist/core/sql.js.map +1 -0
- package/dist/core/types.d.ts +22 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/url-key.d.ts +39 -0
- package/dist/core/url-key.d.ts.map +1 -0
- package/dist/core/url-key.js +106 -0
- package/dist/core/url-key.js.map +1 -0
- package/dist/generators/index.d.ts +45 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +184 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1387 -0
- package/dist/server.js.map +1 -0
- package/dist/src/ui/dashboard.html +347 -0
- package/dist/src/ui/sync-progress.html +104 -0
- package/package.json +101 -0
- package/server.json +57 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
import { urlKey } from './url-key.js';
|
|
3
|
+
/**
|
|
4
|
+
* Internal = the seed host or its www/apex twin ONLY — not arbitrary sibling subdomains.
|
|
5
|
+
* accounts./shop./blog. etc. are separate properties; crawling (or following redirects into)
|
|
6
|
+
* them pulls in junk like Shopify's accounts.<domain> OAuth login pages. Shared by the
|
|
7
|
+
* link-classifier here and the crawler's redirect-target guard.
|
|
8
|
+
*/
|
|
9
|
+
export const isInternalHost = (targetHost, baseHost) => {
|
|
10
|
+
const apex = (h) => (h.startsWith('www.') ? h.slice(4) : h);
|
|
11
|
+
const a = apex(baseHost.toLowerCase());
|
|
12
|
+
const t = targetHost.toLowerCase();
|
|
13
|
+
return t === a || t === `www.${a}`;
|
|
14
|
+
};
|
|
15
|
+
function placementOf($, el) {
|
|
16
|
+
const $el = $(el);
|
|
17
|
+
if ($el.closest('nav, header').length)
|
|
18
|
+
return 'navigation';
|
|
19
|
+
if ($el.closest('footer').length)
|
|
20
|
+
return 'footer';
|
|
21
|
+
return 'body';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract the SEO-relevant surface from raw HTML. Pure function of bytes (D).
|
|
25
|
+
* `baseHost` is the seed apex host for internal/external classification;
|
|
26
|
+
* `keyOpts` is the url-key normalisation config for this property.
|
|
27
|
+
*/
|
|
28
|
+
export function extractPage(html, pageUrl, baseHost, keyOpts, xRobotsTag) {
|
|
29
|
+
const $ = cheerio.load(html);
|
|
30
|
+
const title = $('head > title').first().text().trim() || null;
|
|
31
|
+
const metaDescription = $('meta[name="description"]').attr('content')?.trim() ?? null;
|
|
32
|
+
const h1Els = $('h1');
|
|
33
|
+
const h1 = h1Els.first().text().trim() || null;
|
|
34
|
+
const lang = $('html').attr('lang')?.trim() ?? null;
|
|
35
|
+
const charset = $('meta[charset]').attr('charset')?.trim()
|
|
36
|
+
?? ($('meta[http-equiv="Content-Type"]').attr('content')?.match(/charset=([\w-]+)/i)?.[1] ?? null);
|
|
37
|
+
const canonicalEls = $('link[rel="canonical"]');
|
|
38
|
+
const canonicalRaw = canonicalEls.first().attr('href')?.trim() ?? null;
|
|
39
|
+
// A malformed canonical href (e.g. `href="http://"`) must not make the whole page
|
|
40
|
+
// extraction throw — that would record a fine 200 page as a fetch failure.
|
|
41
|
+
const canonicalUrl = canonicalRaw ? (() => { try {
|
|
42
|
+
return new URL(canonicalRaw, pageUrl).toString();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
} })() : null;
|
|
47
|
+
const canonicalRelative = canonicalRaw != null && !/^https?:\/\//i.test(canonicalRaw);
|
|
48
|
+
const robots = $('meta[name="robots"]').attr('content')?.trim() ?? null;
|
|
49
|
+
const viewport = $('meta[name="viewport"]').attr('content')?.trim() ?? null;
|
|
50
|
+
const noindex = /noindex/i.test(robots ?? '') || /noindex/i.test(xRobotsTag ?? '');
|
|
51
|
+
// word count excluding script/style/noscript/template (fixes inline-script inflation)
|
|
52
|
+
const $body = cheerio.load(html);
|
|
53
|
+
$body('script, style, noscript, template').remove();
|
|
54
|
+
const bodyText = $body('body').text().replace(/\s+/g, ' ').trim();
|
|
55
|
+
const wordCount = bodyText ? bodyText.split(' ').length : 0;
|
|
56
|
+
// Body chunks: visible content segmented by heading, boilerplate stripped — the data layer for the
|
|
57
|
+
// content-cluster checks (phrase gaps, RAG alignment, extractability). Depth-first reading-order
|
|
58
|
+
// walk: a heading starts a new chunk; other text accrues to the current one. No DOM mutation.
|
|
59
|
+
const bodyChunks = [];
|
|
60
|
+
{
|
|
61
|
+
const $c = cheerio.load(html);
|
|
62
|
+
$c('script, style, noscript, template, nav, header, footer, aside').remove();
|
|
63
|
+
const root = ($c('main').first()[0] ?? $c('article').first()[0] ?? $c('body')[0]);
|
|
64
|
+
const ws = (t) => t.replace(/\s+/g, ' ').trim();
|
|
65
|
+
let cur = { heading: '', level: 0, parts: [] };
|
|
66
|
+
const flush = () => {
|
|
67
|
+
const text = ws(cur.parts.join(' '));
|
|
68
|
+
if (cur.heading || text)
|
|
69
|
+
bodyChunks.push({ heading: cur.heading, level: cur.level, text: text.slice(0, 1500) });
|
|
70
|
+
};
|
|
71
|
+
const walk = (node) => {
|
|
72
|
+
for (const child of node.children ?? []) {
|
|
73
|
+
if (child.type === 'tag') {
|
|
74
|
+
if (/^h[1-6]$/.test(child.name)) {
|
|
75
|
+
flush();
|
|
76
|
+
cur = { heading: ws($c(child).text()), level: Number(child.name[1]), parts: [] };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
walk(child);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (child.type === 'text' && child.data && child.data.trim()) {
|
|
83
|
+
cur.parts.push(child.data);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
if (root) {
|
|
88
|
+
walk(root);
|
|
89
|
+
flush();
|
|
90
|
+
}
|
|
91
|
+
const kept = bodyChunks.filter(c => c.heading || c.text.length > 20).slice(0, 40);
|
|
92
|
+
bodyChunks.length = 0;
|
|
93
|
+
bodyChunks.push(...kept);
|
|
94
|
+
}
|
|
95
|
+
const jsonLdBlocks = [];
|
|
96
|
+
$('script[type="application/ld+json"]').each((_, el) => {
|
|
97
|
+
const t = $(el).contents().text().trim();
|
|
98
|
+
if (t)
|
|
99
|
+
jsonLdBlocks.push(t);
|
|
100
|
+
});
|
|
101
|
+
const og = {};
|
|
102
|
+
$('meta[property^="og:"]').each((_, el) => {
|
|
103
|
+
const p = $(el).attr('property');
|
|
104
|
+
const c = $(el).attr('content');
|
|
105
|
+
if (p && c)
|
|
106
|
+
og[p] = c;
|
|
107
|
+
});
|
|
108
|
+
const hreflang = [];
|
|
109
|
+
$('link[rel="alternate"][hreflang]').each((_, el) => {
|
|
110
|
+
const l = $(el).attr('hreflang');
|
|
111
|
+
const href = $(el).attr('href');
|
|
112
|
+
if (l && href)
|
|
113
|
+
hreflang.push({ lang: l, href });
|
|
114
|
+
});
|
|
115
|
+
// Twitter Card tags (alongside Open Graph) — share-appearance completeness.
|
|
116
|
+
const tw = {};
|
|
117
|
+
$('meta[name^="twitter:"]').each((_, el) => {
|
|
118
|
+
const n = $(el).attr('name');
|
|
119
|
+
const c = $(el).attr('content');
|
|
120
|
+
if (n && c)
|
|
121
|
+
tw[n] = c;
|
|
122
|
+
});
|
|
123
|
+
// Heading hierarchy: levels in document order → count skipped jumps (h1→h3 etc.).
|
|
124
|
+
const headingLevels = [];
|
|
125
|
+
$('h1, h2, h3, h4, h5, h6').each((_, el) => {
|
|
126
|
+
const lvl = Number(el.name?.[1]);
|
|
127
|
+
if (lvl >= 1 && lvl <= 6)
|
|
128
|
+
headingLevels.push(lvl);
|
|
129
|
+
});
|
|
130
|
+
let headingSkips = 0;
|
|
131
|
+
for (let i = 1; i < headingLevels.length; i++)
|
|
132
|
+
if (headingLevels[i] - headingLevels[i - 1] > 1)
|
|
133
|
+
headingSkips++;
|
|
134
|
+
const h2Count = headingLevels.filter(l => l === 2).length;
|
|
135
|
+
// rel link relations Google still reads / we don't otherwise capture.
|
|
136
|
+
const relNext = $('link[rel="next"]').length > 0;
|
|
137
|
+
const relPrev = $('link[rel="prev"]').length > 0;
|
|
138
|
+
const relAmphtml = $('link[rel="amphtml"]').attr('href')?.trim() ?? null;
|
|
139
|
+
// Mixed content: insecure (http) subresources on an https page.
|
|
140
|
+
const isHttps = /^https:/i.test(pageUrl);
|
|
141
|
+
let mixedContentCount = 0;
|
|
142
|
+
if (isHttps) {
|
|
143
|
+
$('img[src], script[src], link[rel="stylesheet"][href], iframe[src], source[src], video[src], audio[src]').each((_, el) => {
|
|
144
|
+
const u = $(el).attr('src') || $(el).attr('href') || '';
|
|
145
|
+
if (/^http:\/\//i.test(u))
|
|
146
|
+
mixedContentCount++;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Structured-data formats beyond JSON-LD (RDFa via typeof/vocab, not property — OG meta uses property).
|
|
150
|
+
const hasMicrodata = $('[itemscope]').length > 0;
|
|
151
|
+
const hasRdfa = $('[typeof], [vocab]').length > 0;
|
|
152
|
+
// Favicon: any icon link relation (icon / shortcut icon / apple-touch-icon).
|
|
153
|
+
const hasFavicon = $('link[rel~="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]').length > 0;
|
|
154
|
+
// Client-side analytics: a known snippet/host anywhere in the raw HTML. Deliberately a tight
|
|
155
|
+
// allow-list of real analytics products — absence means "no CLIENT-SIDE analytics detected",
|
|
156
|
+
// not "no analytics" (server-side measurement is invisible to a crawl).
|
|
157
|
+
const hasAnalytics = /googletagmanager\.com|google-analytics\.com|gtag\(|plausible\.io|matomo|_paq\b|usefathom\.com|clarity\.ms|cloudflareinsights\.com|mixpanel|heap-\d|segment\.com\/analytics/i.test(html);
|
|
158
|
+
// Images: count those lacking a non-empty alt (decorative alt="" is intentional, not flagged),
|
|
159
|
+
// and those lacking both width & height attributes (a deterministic CLS signal).
|
|
160
|
+
const imgEls = $('img');
|
|
161
|
+
let imagesWithoutAlt = 0;
|
|
162
|
+
let imagesMissingDimensions = 0;
|
|
163
|
+
// Distinct absolute image URLs, capped per page — the crawler samples their content-length
|
|
164
|
+
// after the crawl for the image-weight report (CDN-hosted images count; SEO cares about
|
|
165
|
+
// what the page loads, not where it's hosted).
|
|
166
|
+
const imageSrcSet = new Set();
|
|
167
|
+
imgEls.each((_, el) => {
|
|
168
|
+
const $e = $(el);
|
|
169
|
+
if ($e.attr('alt') === undefined)
|
|
170
|
+
imagesWithoutAlt++;
|
|
171
|
+
if ($e.attr('width') === undefined && $e.attr('height') === undefined)
|
|
172
|
+
imagesMissingDimensions++;
|
|
173
|
+
const src = ($e.attr('src') ?? '').trim();
|
|
174
|
+
if (src && !src.startsWith('data:') && imageSrcSet.size < 15) {
|
|
175
|
+
try {
|
|
176
|
+
const u = new URL(src, pageUrl);
|
|
177
|
+
if (u.protocol === 'http:' || u.protocol === 'https:')
|
|
178
|
+
imageSrcSet.add(u.toString());
|
|
179
|
+
}
|
|
180
|
+
catch { /* skip bad src */ }
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const links = [];
|
|
184
|
+
let internalLinks = 0;
|
|
185
|
+
let externalLinks = 0;
|
|
186
|
+
$('a[href]').each((_, el) => {
|
|
187
|
+
const href = $(el).attr('href');
|
|
188
|
+
if (!href)
|
|
189
|
+
return;
|
|
190
|
+
let target;
|
|
191
|
+
try {
|
|
192
|
+
target = new URL(href, pageUrl);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:')
|
|
198
|
+
return;
|
|
199
|
+
// Cloudflare-reserved infrastructure (/cdn-cgi/l/email-protection, rocket-loader, …) —
|
|
200
|
+
// never real content and 404s to bots by design; skip so it's neither crawled nor counted
|
|
201
|
+
// as an internal link (otherwise it shows up as a high-equity "broken link" false positive).
|
|
202
|
+
if (/^\/cdn-cgi\//i.test(target.pathname))
|
|
203
|
+
return;
|
|
204
|
+
// Internal = the seed host or its www/apex twin ONLY — not arbitrary sibling subdomains.
|
|
205
|
+
// (accounts./shop./blog. etc. are separate properties; crawling them pulls in junk like
|
|
206
|
+
// Shopify's accounts.<domain> OAuth login pages — infinite param URLs, no SEO value.)
|
|
207
|
+
const isInternal = isInternalHost(target.hostname, baseHost);
|
|
208
|
+
if (isInternal)
|
|
209
|
+
internalLinks++;
|
|
210
|
+
else
|
|
211
|
+
externalLinks++;
|
|
212
|
+
links.push({
|
|
213
|
+
targetUrl: target.toString(),
|
|
214
|
+
targetKey: urlKey(target.toString(), keyOpts),
|
|
215
|
+
anchor: $(el).text().replace(/\s+/g, ' ').trim().slice(0, 300),
|
|
216
|
+
placement: placementOf($, el),
|
|
217
|
+
rel: $(el).attr('rel')?.trim() ?? '',
|
|
218
|
+
isInternal,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
title,
|
|
223
|
+
titleLength: title?.length ?? 0,
|
|
224
|
+
metaDescription,
|
|
225
|
+
metaDescriptionLength: metaDescription?.length ?? 0,
|
|
226
|
+
h1,
|
|
227
|
+
h1Count: h1Els.length,
|
|
228
|
+
wordCount,
|
|
229
|
+
lang,
|
|
230
|
+
charset,
|
|
231
|
+
canonicalUrl,
|
|
232
|
+
canonicalKey: canonicalUrl ? urlKey(canonicalUrl, keyOpts) : null,
|
|
233
|
+
robots,
|
|
234
|
+
viewport,
|
|
235
|
+
noindex,
|
|
236
|
+
jsonLd: jsonLdBlocks.length ? JSON.stringify(jsonLdBlocks) : null,
|
|
237
|
+
ogTags: Object.keys(og).length ? JSON.stringify(og) : null,
|
|
238
|
+
hreflang: hreflang.length ? JSON.stringify(hreflang) : null,
|
|
239
|
+
internalLinks,
|
|
240
|
+
externalLinks,
|
|
241
|
+
imageCount: imgEls.length,
|
|
242
|
+
imagesWithoutAlt,
|
|
243
|
+
imagesMissingDimensions,
|
|
244
|
+
canonicalCount: canonicalEls.length,
|
|
245
|
+
canonicalRelative,
|
|
246
|
+
h2Count,
|
|
247
|
+
headingSkips,
|
|
248
|
+
relNext,
|
|
249
|
+
relPrev,
|
|
250
|
+
relAmphtml,
|
|
251
|
+
mixedContentCount,
|
|
252
|
+
twitterTags: Object.keys(tw).length ? JSON.stringify(tw) : null,
|
|
253
|
+
hasMicrodata,
|
|
254
|
+
hasRdfa,
|
|
255
|
+
hasFavicon,
|
|
256
|
+
hasAnalytics,
|
|
257
|
+
bodyChunks,
|
|
258
|
+
links,
|
|
259
|
+
imageSrcs: [...imageSrcSet],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/core/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export interface GscFreshness {
|
|
3
|
+
/** Latest date present in search_analytics (may be a partial-data day). */
|
|
4
|
+
rawMax: string | null;
|
|
5
|
+
/** Latest date whose data looks finalised — use this for windows and charts. */
|
|
6
|
+
effectiveMax: string | null;
|
|
7
|
+
/** How many trailing days were trimmed as still-being-finalised. */
|
|
8
|
+
trimmedDays: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Google Search Console finalises data ~2–3 days late, so the most recent day(s) in the DB are
|
|
12
|
+
* partial — a sharp downward cliff that reads as "traffic is tanking" and panics SEOs. Detect and
|
|
13
|
+
* trim those trailing partial days *data-drivenly*: drop any trailing day whose impressions fall
|
|
14
|
+
* below 50% of the recent (stable-window) median, capped at 3 days. This catches the crater on
|
|
15
|
+
* busy sites without over-trimming a genuinely low-traffic site whose every day is small.
|
|
16
|
+
*/
|
|
17
|
+
export declare function gscFreshness(db: Database.Database): GscFreshness;
|
|
18
|
+
//# sourceMappingURL=gscFreshness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gscFreshness.d.ts","sourceRoot":"","sources":["../../src/core/gscFreshness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gFAAgF;IAChF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,YAAY,CAoBhE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Search Console finalises data ~2–3 days late, so the most recent day(s) in the DB are
|
|
3
|
+
* partial — a sharp downward cliff that reads as "traffic is tanking" and panics SEOs. Detect and
|
|
4
|
+
* trim those trailing partial days *data-drivenly*: drop any trailing day whose impressions fall
|
|
5
|
+
* below 50% of the recent (stable-window) median, capped at 3 days. This catches the crater on
|
|
6
|
+
* busy sites without over-trimming a genuinely low-traffic site whose every day is small.
|
|
7
|
+
*/
|
|
8
|
+
export function gscFreshness(db) {
|
|
9
|
+
const rows = db
|
|
10
|
+
.prepare(`SELECT date, SUM(impressions) i FROM search_analytics WHERE date IS NOT NULL GROUP BY date ORDER BY date`)
|
|
11
|
+
.all();
|
|
12
|
+
if (rows.length === 0)
|
|
13
|
+
return { rawMax: null, effectiveMax: null, trimmedDays: 0 };
|
|
14
|
+
const rawMax = rows[rows.length - 1].date;
|
|
15
|
+
if (rows.length < 8)
|
|
16
|
+
return { rawMax, effectiveMax: rawMax, trimmedDays: 0 };
|
|
17
|
+
// Reference level from a recent stable window (last ~21 days, excluding the last 3 that may be partial).
|
|
18
|
+
const stable = rows.slice(Math.max(0, rows.length - 24), rows.length - 3).map(r => r.i).sort((a, b) => a - b);
|
|
19
|
+
const median = stable.length ? stable[Math.floor(stable.length / 2)] : 0;
|
|
20
|
+
if (median <= 0)
|
|
21
|
+
return { rawMax, effectiveMax: rawMax, trimmedDays: 0 };
|
|
22
|
+
let trimmed = 0;
|
|
23
|
+
for (let k = rows.length - 1; k >= 0 && trimmed < 3; k--) {
|
|
24
|
+
if (rows[k].i < median * 0.5)
|
|
25
|
+
trimmed++;
|
|
26
|
+
else
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const effectiveMax = rows[rows.length - 1 - trimmed].date;
|
|
30
|
+
return { rawMax, effectiveMax, trimmedDays: trimmed };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=gscFreshness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gscFreshness.js","sourceRoot":"","sources":["../../src/core/gscFreshness.ts"],"names":[],"mappings":"AAWA;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,EAAqB;IAChD,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CAAC,0GAA0G,CAAC;SACnH,GAAG,EAAmC,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACnF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAE7E,yGAAyG;IACzG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9G,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAEzE,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,GAAG;YAAE,OAAO,EAAE,CAAC;;YACnC,MAAM;IACb,CAAC;IACD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC;IAC1D,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Post-crawl link-graph metrics, computed in-process from the `links` table (no new infra):
|
|
4
|
+
* - **iPR** — internal PageRank (weighted: nav/footer links count ×0.2, body ×1), log-
|
|
5
|
+
* normalised to 0–100. Surfaces where internal authority concentrates (research/14 §1).
|
|
6
|
+
* - **click_depth** — shortest path from the homepage following BODY links only (the
|
|
7
|
+
* "honest" depth; nav/footer would make everything depth-1). null = unreachable via body.
|
|
8
|
+
* Writes both back to pages.ipr / pages.click_depth for the crawl.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Full post-crawl link-graph finalization for a crawl: in-degree (inlink_count) from the
|
|
12
|
+
* `links` table, then iPR + click_depth. Idempotent — safe to re-run. Used by the crawler at
|
|
13
|
+
* the end of a crawl AND by the audit as a self-heal when a crawl's link-graph was left
|
|
14
|
+
* unpopulated (e.g. the crawl process was killed before it could finalize). When seedKey is
|
|
15
|
+
* null the homepage is inferred as the highest-in-degree page (computed first, above).
|
|
16
|
+
*/
|
|
17
|
+
export declare function finalizeLinkGraph(db: Database.Database, crawlId: string, seedKey: string | null): void;
|
|
18
|
+
export declare function computeLinkGraph(db: Database.Database, crawlId: string, seedKey: string | null): void;
|
|
19
|
+
//# sourceMappingURL=linkGraph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkGraph.d.ts","sourceRoot":"","sources":["../../src/core/linkGraph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;GAOG;AACH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CActG;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CA2ErG"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-crawl link-graph metrics, computed in-process from the `links` table (no new infra):
|
|
3
|
+
* - **iPR** — internal PageRank (weighted: nav/footer links count ×0.2, body ×1), log-
|
|
4
|
+
* normalised to 0–100. Surfaces where internal authority concentrates (research/14 §1).
|
|
5
|
+
* - **click_depth** — shortest path from the homepage following BODY links only (the
|
|
6
|
+
* "honest" depth; nav/footer would make everything depth-1). null = unreachable via body.
|
|
7
|
+
* Writes both back to pages.ipr / pages.click_depth for the crawl.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Full post-crawl link-graph finalization for a crawl: in-degree (inlink_count) from the
|
|
11
|
+
* `links` table, then iPR + click_depth. Idempotent — safe to re-run. Used by the crawler at
|
|
12
|
+
* the end of a crawl AND by the audit as a self-heal when a crawl's link-graph was left
|
|
13
|
+
* unpopulated (e.g. the crawl process was killed before it could finalize). When seedKey is
|
|
14
|
+
* null the homepage is inferred as the highest-in-degree page (computed first, above).
|
|
15
|
+
*/
|
|
16
|
+
export function finalizeLinkGraph(db, crawlId, seedKey) {
|
|
17
|
+
db.prepare(`UPDATE pages SET inlink_count = (
|
|
18
|
+
SELECT COUNT(*) FROM links
|
|
19
|
+
WHERE links.target_key = pages.url_key AND links.is_internal = 1
|
|
20
|
+
AND links.source_key != pages.url_key AND links.crawl_id = pages.crawl_id
|
|
21
|
+
) WHERE crawl_id = ?`).run(crawlId);
|
|
22
|
+
let seed = seedKey;
|
|
23
|
+
if (!seed) {
|
|
24
|
+
const r = db.prepare(`SELECT url_key FROM pages WHERE crawl_id=? ORDER BY inlink_count DESC LIMIT 1`).get(crawlId);
|
|
25
|
+
seed = r?.url_key ?? null;
|
|
26
|
+
}
|
|
27
|
+
computeLinkGraph(db, crawlId, seed);
|
|
28
|
+
}
|
|
29
|
+
export function computeLinkGraph(db, crawlId, seedKey) {
|
|
30
|
+
const nodes = db.prepare('SELECT url_key FROM pages WHERE crawl_id=?').all(crawlId).map(r => r.url_key);
|
|
31
|
+
const N = nodes.length;
|
|
32
|
+
if (N === 0)
|
|
33
|
+
return;
|
|
34
|
+
const idx = new Map(nodes.map((n, i) => [n, i]));
|
|
35
|
+
const edges = db.prepare('SELECT source_key s, target_key t, placement p FROM links WHERE crawl_id=? AND is_internal=1 AND source_key!=target_key').all(crawlId);
|
|
36
|
+
// ── iPR: weighted power iteration ──
|
|
37
|
+
const outW = new Float64Array(N);
|
|
38
|
+
const adj = Array.from({ length: N }, () => []);
|
|
39
|
+
for (const e of edges) {
|
|
40
|
+
const si = idx.get(e.s), ti = idx.get(e.t);
|
|
41
|
+
if (si === undefined || ti === undefined)
|
|
42
|
+
continue; // target not in this crawl
|
|
43
|
+
const w = e.p === 'body' ? 1 : 0.2;
|
|
44
|
+
adj[si].push({ t: ti, w });
|
|
45
|
+
outW[si] += w;
|
|
46
|
+
}
|
|
47
|
+
const d = 0.85;
|
|
48
|
+
let pr = new Float64Array(N).fill(1 / N);
|
|
49
|
+
for (let iter = 0; iter < 30; iter++) {
|
|
50
|
+
const next = new Float64Array(N).fill((1 - d) / N);
|
|
51
|
+
let dangling = 0;
|
|
52
|
+
for (let i = 0; i < N; i++)
|
|
53
|
+
if (outW[i] === 0)
|
|
54
|
+
dangling += pr[i];
|
|
55
|
+
const danglingShare = (d * dangling) / N;
|
|
56
|
+
for (let i = 0; i < N; i++) {
|
|
57
|
+
if (outW[i] === 0)
|
|
58
|
+
continue;
|
|
59
|
+
const share = (d * pr[i]) / outW[i];
|
|
60
|
+
for (const { t, w } of adj[i])
|
|
61
|
+
next[t] += share * w;
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < N; i++)
|
|
64
|
+
next[i] += danglingShare;
|
|
65
|
+
// L1-delta early exit — PageRank usually converges well before 30 iterations; stop once the
|
|
66
|
+
// vector barely moves so we don't spend the back half of the iterations for no change.
|
|
67
|
+
let delta = 0;
|
|
68
|
+
for (let i = 0; i < N; i++)
|
|
69
|
+
delta += Math.abs(next[i] - pr[i]);
|
|
70
|
+
pr = next;
|
|
71
|
+
if (delta < 1e-6)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
// log-normalise to 0–100 (pr values are all > 0 thanks to the (1-d)/N base)
|
|
75
|
+
let lo = Infinity, hi = -Infinity;
|
|
76
|
+
const logpr = new Float64Array(N);
|
|
77
|
+
for (let i = 0; i < N; i++) {
|
|
78
|
+
const l = Math.log(pr[i]);
|
|
79
|
+
logpr[i] = l;
|
|
80
|
+
if (l < lo)
|
|
81
|
+
lo = l;
|
|
82
|
+
if (l > hi)
|
|
83
|
+
hi = l;
|
|
84
|
+
}
|
|
85
|
+
const span = hi - lo || 1;
|
|
86
|
+
const ipr = new Float64Array(N);
|
|
87
|
+
for (let i = 0; i < N; i++)
|
|
88
|
+
ipr[i] = Math.round(((logpr[i] - lo) / span) * 100);
|
|
89
|
+
// ── click_depth: BFS from homepage over BODY links only ──
|
|
90
|
+
const bodyAdj = Array.from({ length: N }, () => []);
|
|
91
|
+
for (const e of edges) {
|
|
92
|
+
if (e.p !== 'body')
|
|
93
|
+
continue;
|
|
94
|
+
const si = idx.get(e.s), ti = idx.get(e.t);
|
|
95
|
+
if (si !== undefined && ti !== undefined)
|
|
96
|
+
bodyAdj[si].push(ti);
|
|
97
|
+
}
|
|
98
|
+
const depth = new Int32Array(N).fill(-1);
|
|
99
|
+
// BFS root: the seed's key, but if the bare homepage redirected (final URL keyed
|
|
100
|
+
// differently), fall back to the shallowest crawl-discovered page (depth 0 = seed).
|
|
101
|
+
let start = seedKey != null ? idx.get(seedKey) : undefined;
|
|
102
|
+
if (start === undefined) {
|
|
103
|
+
const root = db.prepare('SELECT url_key FROM pages WHERE crawl_id=? ORDER BY depth ASC, inlink_count DESC LIMIT 1').get(crawlId);
|
|
104
|
+
if (root)
|
|
105
|
+
start = idx.get(root.url_key);
|
|
106
|
+
}
|
|
107
|
+
if (start !== undefined) {
|
|
108
|
+
depth[start] = 0;
|
|
109
|
+
const q = [start];
|
|
110
|
+
for (let h = 0; h < q.length; h++) {
|
|
111
|
+
const u = q[h];
|
|
112
|
+
for (const v of bodyAdj[u])
|
|
113
|
+
if (depth[v] === -1) {
|
|
114
|
+
depth[v] = depth[u] + 1;
|
|
115
|
+
q.push(v);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const upd = db.prepare('UPDATE pages SET ipr=?, click_depth=? WHERE crawl_id=? AND url_key=?');
|
|
120
|
+
db.transaction(() => {
|
|
121
|
+
for (let i = 0; i < N; i++)
|
|
122
|
+
upd.run(ipr[i], depth[i] < 0 ? null : depth[i], crawlId, nodes[i]);
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=linkGraph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkGraph.js","sourceRoot":"","sources":["../../src/core/linkGraph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For each ranking page (top GSC query, has body chunks), score every chunk against that query with
|
|
3
|
+
* the local cross-encoder and persist the single best-passage score — the proxy for how neural /
|
|
4
|
+
* AI search judges whether the page has a dense, extractable answer. On-demand (ML inference cost);
|
|
5
|
+
* bounded to pages that actually rank. Powers the `weak-passage-answer` check.
|
|
6
|
+
*/
|
|
7
|
+
export declare function scoreSitePassages(dataDir: string, siteUrl: string, opts?: {
|
|
8
|
+
limit?: number;
|
|
9
|
+
minImpressions?: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
scored: number;
|
|
12
|
+
flagged: number;
|
|
13
|
+
weakest: {
|
|
14
|
+
url: string;
|
|
15
|
+
query: string;
|
|
16
|
+
score: number;
|
|
17
|
+
}[];
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=passageScore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passageScore.d.ts","sourceRoot":"","sources":["../../src/core/passageScore.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACrD,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAC,CAwCxG"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AuditDatabase } from './AuditDatabase.js';
|
|
2
|
+
import { dbPathFor } from './paths.js';
|
|
3
|
+
import { gscFreshness } from './gscFreshness.js';
|
|
4
|
+
import { maxPassageScore } from './reranker.js';
|
|
5
|
+
/**
|
|
6
|
+
* For each ranking page (top GSC query, has body chunks), score every chunk against that query with
|
|
7
|
+
* the local cross-encoder and persist the single best-passage score — the proxy for how neural /
|
|
8
|
+
* AI search judges whether the page has a dense, extractable answer. On-demand (ML inference cost);
|
|
9
|
+
* bounded to pages that actually rank. Powers the `weak-passage-answer` check.
|
|
10
|
+
*/
|
|
11
|
+
export async function scoreSitePassages(dataDir, siteUrl, opts = {}) {
|
|
12
|
+
const db = new AuditDatabase(dbPathFor(dataDir, siteUrl));
|
|
13
|
+
try {
|
|
14
|
+
const maxDate = gscFreshness(db.db).effectiveMax;
|
|
15
|
+
if (!maxDate)
|
|
16
|
+
return { scored: 0, flagged: 0, weakest: [] };
|
|
17
|
+
const minImpr = opts.minImpressions ?? 50;
|
|
18
|
+
const win = `date > date('${maxDate}', '-28 days') AND date <= '${maxDate}'`;
|
|
19
|
+
const pages = db.db.prepare(`SELECT s.page_key urlKey, s.query query, s.impr impressions, p.body_chunks bc FROM
|
|
20
|
+
(SELECT page_key, query, SUM(impressions) impr, ROW_NUMBER() OVER (PARTITION BY page_key ORDER BY SUM(impressions) DESC) rn
|
|
21
|
+
FROM search_analytics WHERE query IS NOT NULL AND page_key IS NOT NULL AND ${win} GROUP BY page_key, query) s
|
|
22
|
+
JOIN pages p ON p.url_key = s.page_key
|
|
23
|
+
WHERE s.rn = 1 AND p.indexable = 1 AND p.body_chunks IS NOT NULL AND s.impr >= ${minImpr}
|
|
24
|
+
ORDER BY s.impr DESC LIMIT ${opts.limit ? Math.floor(opts.limit) : 500}`).all();
|
|
25
|
+
const upd = db.db.prepare(`UPDATE pages SET max_passage_score=@s, max_passage_query=@q, max_passage_impr=@i, max_passage_at=datetime('now') WHERE url_key=@u`);
|
|
26
|
+
let scored = 0, flagged = 0;
|
|
27
|
+
const weakest = [];
|
|
28
|
+
for (const pg of pages) {
|
|
29
|
+
let chunks;
|
|
30
|
+
try {
|
|
31
|
+
chunks = JSON.parse(pg.bc);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const passages = chunks
|
|
37
|
+
.map(c => `${c.heading ? `${c.heading}. ` : ''}${c.text || ''}`.trim())
|
|
38
|
+
.filter(t => t.length > 20);
|
|
39
|
+
if (!passages.length)
|
|
40
|
+
continue;
|
|
41
|
+
const { max } = await maxPassageScore(pg.query, passages);
|
|
42
|
+
if (!Number.isFinite(max))
|
|
43
|
+
continue; // empty/whitespace query → no scores; don't persist -Infinity
|
|
44
|
+
const score = Math.round(max * 100) / 100;
|
|
45
|
+
upd.run({ s: score, q: pg.query, i: pg.impressions, u: pg.urlKey });
|
|
46
|
+
scored++;
|
|
47
|
+
if (score < 3) {
|
|
48
|
+
flagged++;
|
|
49
|
+
weakest.push({ url: pg.urlKey, query: pg.query, score });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
weakest.sort((a, b) => a.score - b.score);
|
|
53
|
+
return { scored, flagged, weakest: weakest.slice(0, 20) };
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=passageScore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passageScore.js","sourceRoot":"","sources":["../../src/core/passageScore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Turn a GSC property identifier into a safe filename stem. */
|
|
2
|
+
export declare function sanitizeProperty(siteUrl: string): string;
|
|
3
|
+
/** Absolute path to a property's SQLite database (one DB per property). */
|
|
4
|
+
export declare function dbPathFor(dataDir: string, siteUrl: string): string;
|
|
5
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** Turn a GSC property identifier into a safe filename stem. */
|
|
3
|
+
export function sanitizeProperty(siteUrl) {
|
|
4
|
+
return siteUrl
|
|
5
|
+
.replace(/^sc-domain:/, '')
|
|
6
|
+
.replace(/^https?:\/\//, '')
|
|
7
|
+
.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
8
|
+
.replace(/_+/g, '_')
|
|
9
|
+
.replace(/^_|_$/g, '');
|
|
10
|
+
}
|
|
11
|
+
/** Absolute path to a property's SQLite database (one DB per property). */
|
|
12
|
+
export function dbPathFor(dataDir, siteUrl) {
|
|
13
|
+
return path.join(dataDir, `${sanitizeProperty(siteUrl)}.db`);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,OAAO;SACX,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC;SAC1B,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;SAC3B,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,OAAe;IACxD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC"}
|